diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f7285b4..9112fb75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +23.2.0 (06-25-2023) +=================== + +* Institution support email for selective SSO + 23.1.0 (01-25-2023) =================== diff --git a/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java b/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java index fa30a53e..89b28147 100644 --- a/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java +++ b/src/main/java/io/cos/cas/osf/authentication/support/OsfInstitutionUtils.java @@ -29,6 +29,11 @@ public static boolean validateInstitutionForLogin(final JpaOsfDao jpaOsfDao, fin return institution != null && institution.getDelegationProtocol() != null; } + public static String getInstitutionSupportEmail(final JpaOsfDao jpaOsfDao, final String id) { + final OsfInstitution institution = jpaOsfDao.findOneInstitutionById(id); + return institution != null ? institution.getSupportEmail() : null; + } + public static Map getInstitutionLoginUrlMap( final JpaOsfDao jpaOsfDao, final String target, diff --git a/src/main/java/io/cos/cas/osf/model/OsfInstitution.java b/src/main/java/io/cos/cas/osf/model/OsfInstitution.java index b085d232..15ad821d 100644 --- a/src/main/java/io/cos/cas/osf/model/OsfInstitution.java +++ b/src/main/java/io/cos/cas/osf/model/OsfInstitution.java @@ -50,6 +50,9 @@ public class OsfInstitution extends AbstractOsfModel { @Column(name = "deactivated") private Date dateDeactivated; + @Column(name = "support_email", nullable = false) + private String supportEmail; + public DelegationProtocol getDelegationProtocol() { try { return DelegationProtocol.getType(delegationProtocol); diff --git a/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java b/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java index bb2e55f9..a8f309ca 100644 --- a/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java +++ b/src/main/java/io/cos/cas/osf/web/config/OsfCasSupportActionsConfiguration.java @@ -100,6 +100,7 @@ public Action osfNonInteractiveAuthenticationCheckAction() { serviceTicketRequestWebflowEventResolver.getObject(), adaptiveAuthenticationPolicy.getObject(), centralAuthenticationService.getObject(), + jpaOsfDao.getObject(), casProperties.getAuthn().getOsfUrl(), casProperties.getAuthn().getOsfApi(), authnDelegationClients diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java index 1c94d413..280718a4 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfDefaultLoginPreparationAction.java @@ -17,8 +17,6 @@ import org.springframework.webflow.execution.RequestContext; import java.io.Serializable; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.Set; @@ -55,8 +53,6 @@ protected Event doExecute(RequestContext context) { final boolean unsupportedInstitutionLogin = isUnsupportedInstitutionLogin(context); final boolean orcidRedirect = isOrcidLoginAutoRedirect(context); final String orcidLoginUrl = getOrcidLoginUrlFromFlowScope(context); - - final String encodedServiceUrl = getEncodedServiceUrlFromRequestContext(context); final boolean defaultService = isFromFlowlessErrorPage(context); final OsfUrlProperties osfUrl = Optional.of(context).map( requestContext -> (OsfUrlProperties) requestContext.getFlowScope().get(OsfCasWebflowConstants.FLOW_PARAMETER_OSF_URL) @@ -72,9 +68,9 @@ protected Event doExecute(RequestContext context) { -> (OsfCasLoginContext) requestContext.getFlowScope().get(PARAMETER_LOGIN_CONTEXT)).orElse(null); if (loginContext == null) { loginContext = new OsfCasLoginContext( - encodedServiceUrl, institutionLogin, institutionId, + StringUtils.EMPTY, unsupportedInstitutionLogin, orcidRedirect, orcidLoginUrl, @@ -82,9 +78,9 @@ protected Event doExecute(RequestContext context) { defaultServiceUrl ); } else { - loginContext.setEncodedServiceUrl(encodedServiceUrl); loginContext.setInstitutionLogin(institutionLogin); loginContext.setInstitutionId(institutionId); + loginContext.setInstitutionSupportEmail(StringUtils.EMPTY); loginContext.setUnsupportedInstitutionLogin(unsupportedInstitutionLogin); loginContext.setOrcidLoginUrl(orcidLoginUrl); loginContext.setOrcidRedirect(false); @@ -148,14 +144,6 @@ private String getOrcidLoginUrlFromFlowScope(final RequestContext context) { return null; } - private String getEncodedServiceUrlFromRequestContext(final RequestContext context) throws AssertionError { - final String serviceUrl = context.getRequestParameters().get(PARAMETER_SERVICE); - if (StringUtils.isBlank(serviceUrl)) { - return null; - } - return URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8); - } - private boolean isFromFlowlessErrorPage(final RequestContext context) { final String errorCode = context.getRequestParameters().get(PARAMETER_REDIRECT_SOURCE); return !StringUtils.isBlank(errorCode) && EXPECTED_REDIRECT_CODES.contains(errorCode); diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java index 157e1642..56e5dc29 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfInstitutionLoginPreparationAction.java @@ -80,6 +80,11 @@ protected Event doExecute(RequestContext context) { loginContext.setInstitutionId(null); context.getFlowScope().put(PARAMETER_LOGIN_CONTEXT, loginContext); institutionId = null; + } else { + final String institutionSupportEmail = OsfInstitutionUtils.getInstitutionSupportEmail(jpaOsfDao, institutionId); + if (institutionSupportEmail != null) { + loginContext.setInstitutionSupportEmail(institutionSupportEmail); + } } } diff --git a/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java index 63456742..dcbcfee6 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java +++ b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java @@ -15,9 +15,12 @@ import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException; import io.cos.cas.osf.authentication.support.DelegationProtocol; import io.cos.cas.osf.authentication.support.OsfApiPermissionDenied; +import io.cos.cas.osf.authentication.support.OsfInstitutionUtils; import io.cos.cas.osf.configuration.model.OsfApiProperties; import io.cos.cas.osf.configuration.model.OsfUrlProperties; +import io.cos.cas.osf.dao.JpaOsfDao; import io.cos.cas.osf.web.support.OsfApiInstitutionAuthenticationResult; +import io.cos.cas.osf.web.support.OsfCasSsoErrorContext; import com.nimbusds.jose.crypto.DirectEncrypter; import com.nimbusds.jose.crypto.MACSigner; @@ -141,6 +144,8 @@ @Getter public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNonInteractiveCredentialsAction { + private static final String PARAMETER_SSO_ERROR_CONTEXT = "osfCasSsoErrorContext"; + private static final String USERNAME_PARAMETER_NAME = "username"; private static final String VERIFICATION_KEY_PARAMETER_NAME = "verification_key"; @@ -179,6 +184,9 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon @NotNull private CentralAuthenticationService centralAuthenticationService; + @NotNull + private final JpaOsfDao jpaOsfDao; + @NotNull private OsfUrlProperties osfUrlProperties; @@ -195,6 +203,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction( final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver, final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy, final CentralAuthenticationService centralAuthenticationService, + final JpaOsfDao jpaOsfDao, final OsfUrlProperties osfUrlProperties, final OsfApiProperties osfApiProperties, final Map> authnDelegationClients @@ -205,6 +214,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction( adaptiveAuthenticationPolicy ); this.centralAuthenticationService = centralAuthenticationService; + this.jpaOsfDao = jpaOsfDao; this.osfUrlProperties = osfUrlProperties; this.osfApiProperties = osfApiProperties; this.authnDelegationClients = authnDelegationClients; @@ -240,7 +250,8 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex ); final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromPac4jAuthentication(context, clientName); if (osfPostgresCredential != null) { - final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential); + final OsfApiInstitutionAuthenticationResult remoteUserInfo + = notifyOsfApiOfInstnAuthnSuccess(context, osfPostgresCredential); osfPostgresCredential.setUsername(remoteUserInfo.getSsoEmail()); osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId()); WebUtils.removeCredential(context); @@ -263,7 +274,8 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex // Type 3: institution sso via Shibboleth authentication using the SAML protocol LOGGER.debug("Shibboleth session / header found in request context."); final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromShibbolethAuthentication(context, request); - final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential); + final OsfApiInstitutionAuthenticationResult remoteUserInfo + = notifyOsfApiOfInstnAuthnSuccess(context, osfPostgresCredential); final String ssoIdentity = osfPostgresCredential.getSsoIdentity(); final String eppn = osfPostgresCredential.getDelegationAttributes().get("eppn"); final String mail = osfPostgresCredential.getDelegationAttributes().get("mail"); @@ -568,6 +580,7 @@ private JSONObject extractInstnAuthnDataFromCredential(final OsfPostgresCredenti * @throws AccountException if there is an issue with authentication data or if the OSF API request has failed */ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( + final RequestContext context, final OsfPostgresCredential credential ) throws AccountException { @@ -758,6 +771,15 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( final String errorDetail = ((JsonObject) error).get("detail").getAsString(); if (OsfApiPermissionDenied.INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED.getId().equals(errorDetail)) { LOGGER.error("[OSF API] Failure - Institution Selective SSO Not Allowed: {}, filter={}", ssoUser, selectiveSsoFilter); + setSsoErrorContext( + context, + InstitutionSsoSelectiveLoginDeniedException.class.getSimpleName(), + String.format("Institution Selective SSO Not Allowed: %s", ssoUser), + ssoEmail, + ssoIdentity, + institutionId, + OsfInstitutionUtils.getInstitutionSupportEmail(this.jpaOsfDao, institutionId) + ); throw new InstitutionSsoSelectiveLoginDeniedException("OSF API denies selective SSO login"); } if (OsfApiPermissionDenied.INSTITUTION_SSO_DUPLICATE_IDENTITY.getId().equals(errorDetail)) { @@ -810,4 +832,35 @@ private String retrieveDepartment(final String departmentRaw, final boolean eduP } return ""; } + + /** + * Prepare {@link OsfCasSsoErrorContext} and put it in flow. + * + * @param context the request context + * @param handleErrorName the error name + * @param errorMessage the error message + * @param ssoEmail user's SSO email + * @param ssoIdentity user's SSO identity + * @param institutionId institution ID + * @param institutionSupportEmail institution support email + */ + private void setSsoErrorContext( + final RequestContext context, + final String handleErrorName, + final String errorMessage, + final String ssoEmail, + final String ssoIdentity, + final String institutionId, + final String institutionSupportEmail + ) { + OsfCasSsoErrorContext ssoErrorContext = new OsfCasSsoErrorContext( + handleErrorName, + errorMessage, + ssoEmail, + ssoIdentity, + institutionId, + institutionSupportEmail + ); + context.getFlowScope().put(PARAMETER_SSO_ERROR_CONTEXT, ssoErrorContext); + } } diff --git a/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java b/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java index 35bdd22b..d0679497 100644 --- a/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java +++ b/src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java @@ -26,20 +26,12 @@ public class OsfCasLoginContext implements Serializable { private static final long serialVersionUID = 7523144720609509742L; - /** - * The encoded service URL provided by the "service=" query param in the request URL. - * - * This attribute is deprecated and should be removed since 1) ThymeLeaf handles URL building elegantly in the template and 2) both of - * the flow parameters "service.originalUrl" and "originalUrl" stores the current service information. - */ - private String encodedServiceUrl; - - private String handleErrorName; - private boolean institutionLogin; private String institutionId; + private String institutionSupportEmail; + private boolean unsupportedInstitutionLogin; private boolean orcidRedirect; @@ -54,25 +46,4 @@ public class OsfCasLoginContext implements Serializable { * e.g. http(s)://[OSF Domain]/login?next=[encoded version of http(s)://[OSF Domain]/] */ private String defaultServiceUrl; - - public OsfCasLoginContext ( - final String encodedServiceUrl, - final boolean institutionLogin, - final String institutionId, - final boolean unsupportedInstitutionLogin, - final boolean orcidRedirect, - final String orcidLoginUrl, - final boolean defaultService, - final String defaultServiceUrl - ) { - this.encodedServiceUrl = encodedServiceUrl; - this.handleErrorName = null; - this.institutionLogin = institutionLogin; - this.institutionId = institutionId; - this.unsupportedInstitutionLogin = unsupportedInstitutionLogin; - this.orcidRedirect = orcidRedirect; - this.orcidLoginUrl = orcidLoginUrl; - this.defaultService = defaultService; - this.defaultServiceUrl = defaultServiceUrl; - } } diff --git a/src/main/java/io/cos/cas/osf/web/support/OsfCasSsoErrorContext.java b/src/main/java/io/cos/cas/osf/web/support/OsfCasSsoErrorContext.java new file mode 100644 index 00000000..1cddda37 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/web/support/OsfCasSsoErrorContext.java @@ -0,0 +1,40 @@ +package io.cos.cas.osf.web.support; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + * This is {@link OsfCasSsoErrorContext}. + * + * Stores detailed error information, which can be prepared and put into flow before raising an exception. Extends {@link Serializable} + * so that it can be put into and retrieved from the flow context conveniently. + * + * @author Longze Chen + * @since 23.2.0 + */ +@AllArgsConstructor +@Getter +@NoArgsConstructor +@ToString +@Setter +public class OsfCasSsoErrorContext implements Serializable { + + private static final long serialVersionUID = -1366351087792035267L; + + private String handleErrorName; + + private String errorMessage; + + private String ssoEmail; + + private String ssoIdentity; + + private String institutionId; + + private String institutionSupportEmail; +} diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 7e43c0af..4e99c955 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -708,10 +708,15 @@ screen.institutionssoattributeparsingfailed.message=\ screen.institutionssoduplicateidentity.message=\ Your request cannot be completed at this time due to an error caused by duplicate SSO identity. \ Please contact Support for help. -screen.institutionssoselectivelogindenied.message=\ +screen.institutionssoselectivelogindenied.standard.message=\ Your institutional account is unable to authenticate to OSF. Please check with your institution. \ If your institution believes this is in error, \ contact Support for help. +screen.institutionssoselectivelogindenied.support.message=\ + Your institutional account is unable to authenticate to OSF. \ + Please contact support at your institution first. \ + If your institution believes this is in error, \ + contact OSF Support for help. screen.institutionssoosfapifailed.message=\ Your request cannot be completed at this time due to an unexpected error. \ Please return to OSF and try again later. \ diff --git a/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html b/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html index 36ee35ef..dceaa48a 100644 --- a/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html +++ b/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html @@ -25,7 +25,8 @@

-

+

+