Skip to content

Commit 5ce8e80

Browse files
committed
Merge branch 'release/23.2.0'
2 parents 4639e47 + b375444 commit 5ce8e80

11 files changed

+126
-49
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
23.2.0 (06-25-2023)
6+
===================
7+
8+
* Institution support email for selective SSO
9+
510
23.1.0 (01-25-2023)
611
===================
712

src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ public static boolean validateInstitutionForLogin(final JpaOsfDao jpaOsfDao, fin
2929
return institution != null && institution.getDelegationProtocol() != null;
3030
}
3131

32+
public static String getInstitutionSupportEmail(final JpaOsfDao jpaOsfDao, final String id) {
33+
final OsfInstitution institution = jpaOsfDao.findOneInstitutionById(id);
34+
return institution != null ? institution.getSupportEmail() : null;
35+
}
36+
3237
public static Map<String, String> getInstitutionLoginUrlMap(
3338
final JpaOsfDao jpaOsfDao,
3439
final String target,

src/main/java/io/cos/cas/osf/model/OsfInstitution.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public class OsfInstitution extends AbstractOsfModel {
5050
@Column(name = "deactivated")
5151
private Date dateDeactivated;
5252

53+
@Column(name = "support_email", nullable = false)
54+
private String supportEmail;
55+
5356
public DelegationProtocol getDelegationProtocol() {
5457
try {
5558
return DelegationProtocol.getType(delegationProtocol);

src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public Action osfNonInteractiveAuthenticationCheckAction() {
100100
serviceTicketRequestWebflowEventResolver.getObject(),
101101
adaptiveAuthenticationPolicy.getObject(),
102102
centralAuthenticationService.getObject(),
103+
jpaOsfDao.getObject(),
103104
casProperties.getAuthn().getOsfUrl(),
104105
casProperties.getAuthn().getOsfApi(),
105106
authnDelegationClients

src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
import org.springframework.webflow.execution.RequestContext;
1818

1919
import java.io.Serializable;
20-
import java.net.URLEncoder;
21-
import java.nio.charset.StandardCharsets;
2220
import java.util.Optional;
2321
import java.util.Set;
2422

@@ -55,8 +53,6 @@ protected Event doExecute(RequestContext context) {
5553
final boolean unsupportedInstitutionLogin = isUnsupportedInstitutionLogin(context);
5654
final boolean orcidRedirect = isOrcidLoginAutoRedirect(context);
5755
final String orcidLoginUrl = getOrcidLoginUrlFromFlowScope(context);
58-
59-
final String encodedServiceUrl = getEncodedServiceUrlFromRequestContext(context);
6056
final boolean defaultService = isFromFlowlessErrorPage(context);
6157
final OsfUrlProperties osfUrl = Optional.of(context).map(
6258
requestContext -> (OsfUrlProperties) requestContext.getFlowScope().get(OsfCasWebflowConstants.FLOW_PARAMETER_OSF_URL)
@@ -72,19 +68,19 @@ protected Event doExecute(RequestContext context) {
7268
-> (OsfCasLoginContext) requestContext.getFlowScope().get(PARAMETER_LOGIN_CONTEXT)).orElse(null);
7369
if (loginContext == null) {
7470
loginContext = new OsfCasLoginContext(
75-
encodedServiceUrl,
7671
institutionLogin,
7772
institutionId,
73+
StringUtils.EMPTY,
7874
unsupportedInstitutionLogin,
7975
orcidRedirect,
8076
orcidLoginUrl,
8177
defaultService,
8278
defaultServiceUrl
8379
);
8480
} else {
85-
loginContext.setEncodedServiceUrl(encodedServiceUrl);
8681
loginContext.setInstitutionLogin(institutionLogin);
8782
loginContext.setInstitutionId(institutionId);
83+
loginContext.setInstitutionSupportEmail(StringUtils.EMPTY);
8884
loginContext.setUnsupportedInstitutionLogin(unsupportedInstitutionLogin);
8985
loginContext.setOrcidLoginUrl(orcidLoginUrl);
9086
loginContext.setOrcidRedirect(false);
@@ -148,14 +144,6 @@ private String getOrcidLoginUrlFromFlowScope(final RequestContext context) {
148144
return null;
149145
}
150146

151-
private String getEncodedServiceUrlFromRequestContext(final RequestContext context) throws AssertionError {
152-
final String serviceUrl = context.getRequestParameters().get(PARAMETER_SERVICE);
153-
if (StringUtils.isBlank(serviceUrl)) {
154-
return null;
155-
}
156-
return URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8);
157-
}
158-
159147
private boolean isFromFlowlessErrorPage(final RequestContext context) {
160148
final String errorCode = context.getRequestParameters().get(PARAMETER_REDIRECT_SOURCE);
161149
return !StringUtils.isBlank(errorCode) && EXPECTED_REDIRECT_CODES.contains(errorCode);

src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ protected Event doExecute(RequestContext context) {
8080
loginContext.setInstitutionId(null);
8181
context.getFlowScope().put(PARAMETER_LOGIN_CONTEXT, loginContext);
8282
institutionId = null;
83+
} else {
84+
final String institutionSupportEmail = OsfInstitutionUtils.getInstitutionSupportEmail(jpaOsfDao, institutionId);
85+
if (institutionSupportEmail != null) {
86+
loginContext.setInstitutionSupportEmail(institutionSupportEmail);
87+
}
8388
}
8489
}
8590

src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException;
1616
import io.cos.cas.osf.authentication.support.DelegationProtocol;
1717
import io.cos.cas.osf.authentication.support.OsfApiPermissionDenied;
18+
import io.cos.cas.osf.authentication.support.OsfInstitutionUtils;
1819
import io.cos.cas.osf.configuration.model.OsfApiProperties;
1920
import io.cos.cas.osf.configuration.model.OsfUrlProperties;
21+
import io.cos.cas.osf.dao.JpaOsfDao;
2022
import io.cos.cas.osf.web.support.OsfApiInstitutionAuthenticationResult;
23+
import io.cos.cas.osf.web.support.OsfCasSsoErrorContext;
2124

2225
import com.nimbusds.jose.crypto.DirectEncrypter;
2326
import com.nimbusds.jose.crypto.MACSigner;
@@ -141,6 +144,8 @@
141144
@Getter
142145
public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNonInteractiveCredentialsAction {
143146

147+
private static final String PARAMETER_SSO_ERROR_CONTEXT = "osfCasSsoErrorContext";
148+
144149
private static final String USERNAME_PARAMETER_NAME = "username";
145150

146151
private static final String VERIFICATION_KEY_PARAMETER_NAME = "verification_key";
@@ -179,6 +184,9 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon
179184
@NotNull
180185
private CentralAuthenticationService centralAuthenticationService;
181186

187+
@NotNull
188+
private final JpaOsfDao jpaOsfDao;
189+
182190
@NotNull
183191
private OsfUrlProperties osfUrlProperties;
184192

@@ -195,6 +203,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction(
195203
final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver,
196204
final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy,
197205
final CentralAuthenticationService centralAuthenticationService,
206+
final JpaOsfDao jpaOsfDao,
198207
final OsfUrlProperties osfUrlProperties,
199208
final OsfApiProperties osfApiProperties,
200209
final Map<String, List<String>> authnDelegationClients
@@ -205,6 +214,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction(
205214
adaptiveAuthenticationPolicy
206215
);
207216
this.centralAuthenticationService = centralAuthenticationService;
217+
this.jpaOsfDao = jpaOsfDao;
208218
this.osfUrlProperties = osfUrlProperties;
209219
this.osfApiProperties = osfApiProperties;
210220
this.authnDelegationClients = authnDelegationClients;
@@ -240,7 +250,8 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex
240250
);
241251
final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromPac4jAuthentication(context, clientName);
242252
if (osfPostgresCredential != null) {
243-
final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential);
253+
final OsfApiInstitutionAuthenticationResult remoteUserInfo
254+
= notifyOsfApiOfInstnAuthnSuccess(context, osfPostgresCredential);
244255
osfPostgresCredential.setUsername(remoteUserInfo.getSsoEmail());
245256
osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId());
246257
WebUtils.removeCredential(context);
@@ -263,7 +274,8 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex
263274
// Type 3: institution sso via Shibboleth authentication using the SAML protocol
264275
LOGGER.debug("Shibboleth session / header found in request context.");
265276
final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromShibbolethAuthentication(context, request);
266-
final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential);
277+
final OsfApiInstitutionAuthenticationResult remoteUserInfo
278+
= notifyOsfApiOfInstnAuthnSuccess(context, osfPostgresCredential);
267279
final String ssoIdentity = osfPostgresCredential.getSsoIdentity();
268280
final String eppn = osfPostgresCredential.getDelegationAttributes().get("eppn");
269281
final String mail = osfPostgresCredential.getDelegationAttributes().get("mail");
@@ -568,6 +580,7 @@ private JSONObject extractInstnAuthnDataFromCredential(final OsfPostgresCredenti
568580
* @throws AccountException if there is an issue with authentication data or if the OSF API request has failed
569581
*/
570582
private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess(
583+
final RequestContext context,
571584
final OsfPostgresCredential credential
572585
) throws AccountException {
573586

@@ -758,6 +771,15 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess(
758771
final String errorDetail = ((JsonObject) error).get("detail").getAsString();
759772
if (OsfApiPermissionDenied.INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED.getId().equals(errorDetail)) {
760773
LOGGER.error("[OSF API] Failure - Institution Selective SSO Not Allowed: {}, filter={}", ssoUser, selectiveSsoFilter);
774+
setSsoErrorContext(
775+
context,
776+
InstitutionSsoSelectiveLoginDeniedException.class.getSimpleName(),
777+
String.format("Institution Selective SSO Not Allowed: %s", ssoUser),
778+
ssoEmail,
779+
ssoIdentity,
780+
institutionId,
781+
OsfInstitutionUtils.getInstitutionSupportEmail(this.jpaOsfDao, institutionId)
782+
);
761783
throw new InstitutionSsoSelectiveLoginDeniedException("OSF API denies selective SSO login");
762784
}
763785
if (OsfApiPermissionDenied.INSTITUTION_SSO_DUPLICATE_IDENTITY.getId().equals(errorDetail)) {
@@ -810,4 +832,35 @@ private String retrieveDepartment(final String departmentRaw, final boolean eduP
810832
}
811833
return "";
812834
}
835+
836+
/**
837+
* Prepare {@link OsfCasSsoErrorContext} and put it in flow.
838+
*
839+
* @param context the request context
840+
* @param handleErrorName the error name
841+
* @param errorMessage the error message
842+
* @param ssoEmail user's SSO email
843+
* @param ssoIdentity user's SSO identity
844+
* @param institutionId institution ID
845+
* @param institutionSupportEmail institution support email
846+
*/
847+
private void setSsoErrorContext(
848+
final RequestContext context,
849+
final String handleErrorName,
850+
final String errorMessage,
851+
final String ssoEmail,
852+
final String ssoIdentity,
853+
final String institutionId,
854+
final String institutionSupportEmail
855+
) {
856+
OsfCasSsoErrorContext ssoErrorContext = new OsfCasSsoErrorContext(
857+
handleErrorName,
858+
errorMessage,
859+
ssoEmail,
860+
ssoIdentity,
861+
institutionId,
862+
institutionSupportEmail
863+
);
864+
context.getFlowScope().put(PARAMETER_SSO_ERROR_CONTEXT, ssoErrorContext);
865+
}
813866
}

src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,12 @@ public class OsfCasLoginContext implements Serializable {
2626

2727
private static final long serialVersionUID = 7523144720609509742L;
2828

29-
/**
30-
* The encoded service URL provided by the "service=" query param in the request URL.
31-
*
32-
* This attribute is deprecated and should be removed since 1) ThymeLeaf handles URL building elegantly in the template and 2) both of
33-
* the flow parameters "service.originalUrl" and "originalUrl" stores the current service information.
34-
*/
35-
private String encodedServiceUrl;
36-
37-
private String handleErrorName;
38-
3929
private boolean institutionLogin;
4030

4131
private String institutionId;
4232

33+
private String institutionSupportEmail;
34+
4335
private boolean unsupportedInstitutionLogin;
4436

4537
private boolean orcidRedirect;
@@ -54,25 +46,4 @@ public class OsfCasLoginContext implements Serializable {
5446
* e.g. http(s)://[OSF Domain]/login?next=[encoded version of http(s)://[OSF Domain]/]
5547
*/
5648
private String defaultServiceUrl;
57-
58-
public OsfCasLoginContext (
59-
final String encodedServiceUrl,
60-
final boolean institutionLogin,
61-
final String institutionId,
62-
final boolean unsupportedInstitutionLogin,
63-
final boolean orcidRedirect,
64-
final String orcidLoginUrl,
65-
final boolean defaultService,
66-
final String defaultServiceUrl
67-
) {
68-
this.encodedServiceUrl = encodedServiceUrl;
69-
this.handleErrorName = null;
70-
this.institutionLogin = institutionLogin;
71-
this.institutionId = institutionId;
72-
this.unsupportedInstitutionLogin = unsupportedInstitutionLogin;
73-
this.orcidRedirect = orcidRedirect;
74-
this.orcidLoginUrl = orcidLoginUrl;
75-
this.defaultService = defaultService;
76-
this.defaultServiceUrl = defaultServiceUrl;
77-
}
7849
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.cos.cas.osf.web.support;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
import lombok.Setter;
7+
import lombok.ToString;
8+
9+
import java.io.Serializable;
10+
11+
/**
12+
* This is {@link OsfCasSsoErrorContext}.
13+
*
14+
* Stores detailed error information, which can be prepared and put into flow before raising an exception. Extends {@link Serializable}
15+
* so that it can be put into and retrieved from the flow context conveniently.
16+
*
17+
* @author Longze Chen
18+
* @since 23.2.0
19+
*/
20+
@AllArgsConstructor
21+
@Getter
22+
@NoArgsConstructor
23+
@ToString
24+
@Setter
25+
public class OsfCasSsoErrorContext implements Serializable {
26+
27+
private static final long serialVersionUID = -1366351087792035267L;
28+
29+
private String handleErrorName;
30+
31+
private String errorMessage;
32+
33+
private String ssoEmail;
34+
35+
private String ssoIdentity;
36+
37+
private String institutionId;
38+
39+
private String institutionSupportEmail;
40+
}

src/main/resources/messages.properties

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,10 +708,15 @@ screen.institutionssoattributeparsingfailed.message=\
708708
screen.institutionssoduplicateidentity.message=\
709709
Your request cannot be completed at this time due to an error caused by duplicate SSO identity. \
710710
Please contact <a style="white-space: nowrap" href="mailto:[email protected]">Support</a> for help.
711-
screen.institutionssoselectivelogindenied.message=\
711+
screen.institutionssoselectivelogindenied.standard.message=\
712712
Your institutional account is unable to authenticate to OSF. Please check with your institution. \
713713
If your institution believes this is in error, \
714714
contact <a style="white-space: nowrap" href="mailto:[email protected]">Support</a> for help.
715+
screen.institutionssoselectivelogindenied.support.message=\
716+
Your institutional account is unable to authenticate to OSF. \
717+
Please contact <a style="white-space: nowrap" href="mailto:{0}">support at your institution</a> first. \
718+
If your institution believes this is in error, \
719+
contact <a style="white-space: nowrap" href="mailto:[email protected]">OSF Support</a> for help.
715720
screen.institutionssoosfapifailed.message=\
716721
Your request cannot be completed at this time due to an unexpected error. \
717722
Please <a style="white-space: nowrap" href="{0}">return to OSF</a> and try again later. \

0 commit comments

Comments
 (0)