diff --git a/CHANGELOG.md b/CHANGELOG.md index afca8219..4f7285b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +23.1.0 (01-25-2023) +=================== + +* Institution Rework Project - CAS Part + 22.1.3 (12-20-2022) =================== diff --git a/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java b/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java index 9027ecd6..0d5fa644 100644 --- a/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java +++ b/src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java @@ -72,7 +72,7 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential /** * The user's institutional identity when authenticated via institutional SSO. */ - private String institutionalIdentity = ""; + private String ssoIdentity = ""; /** * The authentication delegation protocol that is used between CAS / Shib and institutions. diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAccountInactiveException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAccountInactiveException.java new file mode 100644 index 00000000..494ca065 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAccountInactiveException.java @@ -0,0 +1,30 @@ +package io.cos.cas.osf.authentication.exception; + +import lombok.NoArgsConstructor; + +import javax.security.auth.login.AccountException; + +/** + * Describes an authentication error condition where institution SSO has failed + * due to the OSF account is not active or not eligible for activation. + * + * @author Longze Chen + * @since 23.1.0 + */ +@NoArgsConstructor +public class InstitutionSsoAccountInactiveException extends AccountException { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = -430454081442388569L; + + /** + * Instantiates a new {@link InstitutionSsoAccountInactiveException}. + * + * @param msg the msg + */ + public InstitutionSsoAccountInactiveException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeMissingException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeMissingException.java new file mode 100644 index 00000000..348c9143 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeMissingException.java @@ -0,0 +1,30 @@ +package io.cos.cas.osf.authentication.exception; + +import lombok.NoArgsConstructor; + +import javax.security.auth.login.AccountException; + +/** + * Describes an authentication error condition where institution SSO has failed + * due to missing required attributes from IdP. + * + * @author Longze Chen + * @since 23.1.0 + */ +@NoArgsConstructor +public class InstitutionSsoAttributeMissingException extends AccountException { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = 1412743002614665584L; + + /** + * Instantiates a new {@link InstitutionSsoAttributeMissingException}. + * + * @param msg the msg + */ + public InstitutionSsoAttributeMissingException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeParsingException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeParsingException.java new file mode 100644 index 00000000..253e2034 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeParsingException.java @@ -0,0 +1,30 @@ +package io.cos.cas.osf.authentication.exception; + +import lombok.NoArgsConstructor; + +import javax.security.auth.login.AccountException; + +/** + * Describes an authentication error condition where institution SSO has failed + * due to attribute normalization or parsing failure. + * + * @author Longze Chen + * @since 23.1.0 + */ +@NoArgsConstructor +public class InstitutionSsoAttributeParsingException extends AccountException { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = 4319114898092268727L; + + /** + * Instantiates a new {@link InstitutionSsoAttributeParsingException}. + * + * @param msg the msg + */ + public InstitutionSsoAttributeParsingException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoDuplicateIdentityException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoDuplicateIdentityException.java new file mode 100644 index 00000000..a8dd8316 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoDuplicateIdentityException.java @@ -0,0 +1,30 @@ +package io.cos.cas.osf.authentication.exception; + +import lombok.NoArgsConstructor; + +import javax.security.auth.login.AccountException; + +/** + * Describes an authentication error condition where institution SSO has failed + * due to duplicate SSO identity. + * + * @author Longze Chen + * @since 23.1.0 + */ +@NoArgsConstructor +public class InstitutionSsoDuplicateIdentityException extends AccountException { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = 1412743002614665584L; + + /** + * Instantiates a new {@link InstitutionSsoDuplicateIdentityException}. + * + * @param msg the msg + */ + public InstitutionSsoDuplicateIdentityException(final String msg) { + super(msg); + } +} diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java index 3d6a673a..52785f15 100644 --- a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoFailedException.java @@ -5,7 +5,8 @@ import javax.security.auth.login.AccountException; /** - * Describes an authentication error condition where institution SSO has failed. + * Describes an authentication error condition where institution SSO has failed + * in a way that doesn't fit into any specific exception. * * @author Longze Chen * @since 21.0.0 diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailedException.java similarity index 60% rename from src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.java rename to src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailedException.java index 7319165a..dbc00317 100644 --- a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.java +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailedException.java @@ -5,14 +5,14 @@ import javax.security.auth.login.AccountException; /** - * Describes an authentication error condition when connection failures and/or server errors happen between - * CAS and OSF API during institution SSO. + * Describes an authentication error condition when connection failures and/or server errors happen + * between CAS and OSF API during institution SSO. * * @author Longze Chen * @since 22.1.3 */ @NoArgsConstructor -public class InstitutionSsoOsfApiFailureException extends AccountException { +public class InstitutionSsoOsfApiFailedException extends AccountException { /** * Serialization metadata. @@ -20,11 +20,11 @@ public class InstitutionSsoOsfApiFailureException extends AccountException { private static final long serialVersionUID = -620313210360224932L; /** - * Instantiates a new {@link InstitutionSsoOsfApiFailureException}. + * Instantiates a new {@link InstitutionSsoOsfApiFailedException}. * * @param msg the msg */ - public InstitutionSsoOsfApiFailureException(final String msg) { + public InstitutionSsoOsfApiFailedException(final String msg) { super(msg); } } diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSelectiveSsoFailedException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoSelectiveLoginDeniedException.java similarity index 60% rename from src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSelectiveSsoFailedException.java rename to src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoSelectiveLoginDeniedException.java index 4490b30d..fc7934b9 100644 --- a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSelectiveSsoFailedException.java +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoSelectiveLoginDeniedException.java @@ -5,13 +5,14 @@ import javax.security.auth.login.AccountException; /** - * Describes an authentication error condition where user is not allowed to access OSF via institution SSO. + * Describes an authentication error condition where user is not allowed to access OSF + * via institution SSO due to Selective SSO rules. * * @author Longze Chen * @since 22.0.1 */ @NoArgsConstructor -public class InstitutionSelectiveSsoFailedException extends AccountException { +public class InstitutionSsoSelectiveLoginDeniedException extends AccountException { /** * Serialization metadata. @@ -19,11 +20,11 @@ public class InstitutionSelectiveSsoFailedException extends AccountException { private static final long serialVersionUID = -7613915260905373074L; /** - * Instantiates a new {@link InstitutionSelectiveSsoFailedException}. + * Instantiates a new {@link InstitutionSsoSelectiveLoginDeniedException}. * * @param msg the msg */ - public InstitutionSelectiveSsoFailedException(final String msg) { + public InstitutionSsoSelectiveLoginDeniedException(final String msg) { super(msg); } } diff --git a/src/main/java/io/cos/cas/osf/authentication/support/OsfApiPermissionDenied.java b/src/main/java/io/cos/cas/osf/authentication/support/OsfApiPermissionDenied.java index e956e7ee..cf69beca 100644 --- a/src/main/java/io/cos/cas/osf/authentication/support/OsfApiPermissionDenied.java +++ b/src/main/java/io/cos/cas/osf/authentication/support/OsfApiPermissionDenied.java @@ -10,7 +10,11 @@ public enum OsfApiPermissionDenied { DEFAULT("PermissionDenied"), - INSTITUTION_SELECTIVE_SSO_FAILURE("InstitutionSsoSelectiveNotAllowed"); + INSTITUTION_SSO_DUPLICATE_IDENTITY("InstitutionSsoDuplicateIdentity"), + + INSTITUTION_SSO_ACCOUNT_INACTIVE("InstitutionSsoAccountInactive"), + + INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED("InstitutionSsoSelectiveLoginDenied"); private final String id; diff --git a/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java index caa3f49f..cdfea1e7 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java +++ b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java @@ -2,9 +2,13 @@ import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException; -import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException; -import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAccountInactiveException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAttributeMissingException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAttributeParsingException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoDuplicateIdentityException; import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoSelectiveLoginDeniedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidPasswordException; import io.cos.cas.osf.authentication.exception.InvalidUserStatusException; @@ -44,14 +48,18 @@ public Set> handledAuthenticationExceptions() { Set> errors = new LinkedHashSet<>(); errors.add(AccountNotConfirmedIdpException.class); errors.add(AccountNotConfirmedOsfException.class); - errors.add(InvalidOneTimePasswordException.class); + errors.add(InstitutionSsoAccountInactiveException.class); + errors.add(InstitutionSsoAttributeMissingException.class); + errors.add(InstitutionSsoAttributeParsingException.class); + errors.add(InstitutionSsoDuplicateIdentityException.class); errors.add(InstitutionSsoFailedException.class); + errors.add(InstitutionSsoOsfApiFailedException.class); + errors.add(InstitutionSsoSelectiveLoginDeniedException.class); + errors.add(InvalidOneTimePasswordException.class); errors.add(InvalidPasswordException.class); errors.add(InvalidUserStatusException.class); errors.add(InvalidVerificationKeyException.class); errors.add(OneTimePasswordRequiredException.class); - errors.add(InstitutionSelectiveSsoFailedException.class); - errors.add(InstitutionSsoOsfApiFailureException.class); errors.add(TermsOfServiceConsentRequiredException.class); // Add built-in exceptions after OSF-specific exceptions since order matters diff --git a/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java b/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java index 52e31af1..8144ca74 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java +++ b/src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java @@ -3,9 +3,13 @@ import io.cos.cas.osf.authentication.credential.OsfPostgresCredential; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException; import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException; -import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException; -import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAccountInactiveException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAttributeMissingException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAttributeParsingException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoDuplicateIdentityException; import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoSelectiveLoginDeniedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidUserStatusException; import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException; @@ -231,6 +235,41 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) { AccountNotConfirmedOsfException.class.getSimpleName(), OsfCasWebflowConstants.VIEW_ID_ACCOUNT_NOT_CONFIRMED_OSF ); + createTransitionForState( + handler, + InstitutionSsoAccountInactiveException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ACCOUNT_INACTIVE + ); + createTransitionForState( + handler, + InstitutionSsoAttributeMissingException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_MISSING + ); + createTransitionForState( + handler, + InstitutionSsoAttributeParsingException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_PARSING_FAILED + ); + createTransitionForState( + handler, + InstitutionSsoDuplicateIdentityException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_DUPLICATE_IDENTITY + ); + createTransitionForState( + handler, + InstitutionSsoFailedException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED + ); + createTransitionForState( + handler, + InstitutionSsoOsfApiFailedException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_OSF_API_FAILED + ); + createTransitionForState( + handler, + InstitutionSsoSelectiveLoginDeniedException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED + ); createTransitionForState( handler, InvalidUserStatusException.class.getSimpleName(), @@ -256,21 +295,6 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) { TermsOfServiceConsentRequiredException.class.getSimpleName(), OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED ); - createTransitionForState( - handler, - InstitutionSsoFailedException.class.getSimpleName(), - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED - ); - createTransitionForState( - handler, - InstitutionSelectiveSsoFailedException.class.getSimpleName(), - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED - ); - createTransitionForState( - handler, - InstitutionSsoOsfApiFailureException.class.getSimpleName(), - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE - ); // The default transition createStateDefaultTransition(handler, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM); @@ -411,6 +435,26 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) { OsfCasWebflowConstants.VIEW_ID_INVALID_VERIFICATION_KEY, OsfCasWebflowConstants.VIEW_ID_INVALID_VERIFICATION_KEY ); + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ACCOUNT_INACTIVE, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ACCOUNT_INACTIVE + ); + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_MISSING, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_MISSING + ); + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_PARSING_FAILED, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_PARSING_FAILED + ); + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_DUPLICATE_IDENTITY, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_DUPLICATE_IDENTITY + ); createViewState( flow, OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_FAILED, @@ -418,13 +462,13 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) { ); createViewState( flow, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_OSF_API_FAILED, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_OSF_API_FAILED ); createViewState( flow, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE, - OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED ); } 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 8a8e8ffc..63456742 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 @@ -7,9 +7,12 @@ import com.google.gson.JsonParser; import io.cos.cas.osf.authentication.credential.OsfPostgresCredential; -import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAttributeMissingException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoAttributeParsingException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoDuplicateIdentityException; import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; -import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoSelectiveLoginDeniedException; +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.configuration.model.OsfApiProperties; @@ -238,7 +241,7 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromPac4jAuthentication(context, clientName); if (osfPostgresCredential != null) { final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential); - osfPostgresCredential.setUsername(remoteUserInfo.getUsername()); + osfPostgresCredential.setUsername(remoteUserInfo.getSsoEmail()); osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId()); WebUtils.removeCredential(context); return osfPostgresCredential; @@ -260,33 +263,39 @@ 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 String ssoEppn = osfPostgresCredential.getDelegationAttributes().get("eppn"); - final String ssoMail = osfPostgresCredential.getDelegationAttributes().get("mail"); - final String ssoMailOther = osfPostgresCredential.getDelegationAttributes().get("mailother"); - if (!remoteUserInfo.verifyOsfUsername(ssoEppn, ssoMail, ssoMailOther)) { + final String ssoIdentity = osfPostgresCredential.getSsoIdentity(); + final String eppn = osfPostgresCredential.getDelegationAttributes().get("eppn"); + final String mail = osfPostgresCredential.getDelegationAttributes().get("mail"); + final String mailOther = osfPostgresCredential.getDelegationAttributes().get("mailother"); + if (!remoteUserInfo.verifyOsfSsoEmail(eppn, mail, mailOther)) { LOGGER.error( - "[SAML Shibboleth] Critical Error: eppn={}, mail={}, mailOther={}, entityId={}, username={}, institutionId={}", - ssoEppn, - ssoMail, - ssoMailOther, - osfPostgresCredential.getDelegationAttributes().get("shib-session-id"), - remoteUserInfo.getUsername(), - remoteUserInfo.getInstitutionId() + "[SAML Shibboleth] Critical Error: ssoIdentity={}, ssoEmail={}, institutionId={}, eppn={}, mail={}, mailOther={}", + ssoIdentity, + remoteUserInfo.getSsoEmail(), + remoteUserInfo.getInstitutionId(), + eppn, + mail, + mailOther ); throw new InstitutionSsoFailedException("Critical SAML-Shibboleth SSO Failure"); } - - osfPostgresCredential.setUsername(remoteUserInfo.getUsername()); + // Note: OsfPostgresCredential.username isn't necessarily the OSF user's username. It can be any of the user's emails. + osfPostgresCredential.setUsername(remoteUserInfo.getSsoEmail()); osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId()); - if (StringUtils.isBlank(osfPostgresCredential.getInstitutionalIdentity())) { + if (StringUtils.isBlank(ssoIdentity)) { LOGGER.warn( - "[SAML Shibboleth] Missing user's institutional identity: username={}, institutionId={}", - remoteUserInfo.getUsername(), - remoteUserInfo.getInstitutionId() + "[SAML Shibboleth] OSF Postgres Credential created w/o identity: ssoEmail={}, institutionId={}", + osfPostgresCredential.getUsername(), + osfPostgresCredential.getInstitutionId() ); } + LOGGER.info( + "[SAML Shibboleth] OSF Postgres Credential created w/ identity: ssoEmail={}, institution={}, ssoIdentity={}", + osfPostgresCredential.getUsername(), + osfPostgresCredential.getInstitutionId(), + ssoIdentity + ); return osfPostgresCredential; } LOGGER.debug("No valid shibboleth session found in request context: check username and verification key."); @@ -443,17 +452,22 @@ private OsfPostgresCredential constructCredentialsFromShibbolethAuthentication( osfPostgresCredential.setRemotePrincipal(Boolean.TRUE); removeShibbolethSessionCookie(context); - final String remoteUser = request.getHeader(REMOTE_USER); - if (StringUtils.isEmpty(remoteUser)) { - LOGGER.error("[SAML Shibboleth] Missing or empty Shibboleth header: {}", REMOTE_USER); + // The request header REMOTE_USER stores the value for SSO user's institutional identity + String remoteUser = request.getHeader(REMOTE_USER); + if (remoteUser != null) { + remoteUser = remoteUser.trim(); + } + if (StringUtils.isBlank(remoteUser)) { + LOGGER.warn("[SAML Shibboleth] Missing or empty Shibboleth header [{}] for SSO identity", REMOTE_USER); } else { - LOGGER.info("[SAML Shibboleth] User's institutional identity: '{}'", remoteUser); + osfPostgresCredential.setSsoIdentity(remoteUser); + LOGGER.info("[SAML Shibboleth] SSO identity [{}] found in header [{}]", remoteUser, REMOTE_USER); } for (final String headerName : Collections.list(request.getHeaderNames())) { if (headerName.startsWith(ATTRIBUTE_PREFIX)) { final String headerValue = request.getHeader(headerName); LOGGER.debug( - "[SAML Shibboleth] User's institutional identity '{}' - auth header '{}': '{}'", + "[SAML Shibboleth] Authn header [{}]<{}:{}>", remoteUser, headerName, headerValue @@ -464,7 +478,6 @@ private OsfPostgresCredential constructCredentialsFromShibbolethAuthentication( ); } } - osfPostgresCredential.setInstitutionalIdentity(remoteUser); return osfPostgresCredential; } @@ -563,55 +576,54 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( try { normalizedPayload = extractInstnAuthnDataFromCredential(credential); } catch (final ParserConfigurationException | TransformerException e) { - LOGGER.error("[CAS XSLT] Failed to normalize attributes in the credential: {}", e.getMessage()); - throw new InstitutionSsoFailedException("Attribute normalization failure"); + LOGGER.error("[CAS XSLT] Exception - Failed to normalize attributes in the credential: {}", e.getMessage()); + throw new InstitutionSsoAttributeParsingException("Attribute normalization failure"); } // Verify required and optional attributes final JSONObject provider = normalizedPayload.optJSONObject("provider"); if (provider == null) { - LOGGER.error("[CAS XSLT] Missing identity provider."); - throw new InstitutionSsoFailedException("Missing identity provider"); + LOGGER.error("[CAS XSLT] Error - Missing identity provider."); + throw new InstitutionSsoAttributeMissingException("Missing identity provider"); } final String institutionId = provider.optString("id").trim(); if (institutionId.isEmpty()) { - LOGGER.error("[CAS XSLT] Empty identity provider"); - throw new InstitutionSsoFailedException("Empty identity provider"); + LOGGER.error("[CAS XSLT] Error - Empty identity provider"); + throw new InstitutionSsoAttributeMissingException("Empty identity provider"); } final JSONObject user = provider.optJSONObject("user"); if (user == null) { - LOGGER.error("[CAS XSLT] Missing institutional user"); - throw new InstitutionSsoFailedException("Missing institutional user"); + LOGGER.error("[CAS XSLT] Error - Missing institutional user"); + throw new InstitutionSsoAttributeMissingException("Missing institutional user"); } - final String username = user.optString("username").trim(); + // Note: SSO Identity didn't come from the normalized attribute set but came from a dedicated Shibboleth header. It was parsed, + // trimmed and stored in the credential object. Thus, it must be explicitly inserted into the payload. + final String ssoIdentity = credential.getSsoIdentity(); + normalizedPayload.getJSONObject("provider").getJSONObject("user").put("ssoIdentity", credential.getSsoIdentity()); + // Note: For legacy reasons, the key for SSO email in the normalized attribute set is "username". SSO API endpoint now expects + // "ssoEmail". Thus, it also needs to be explicitly inserted into the payload with key "ssoEmail". + final String ssoEmail = user.optString("username").trim(); + normalizedPayload.getJSONObject("provider").getJSONObject("user").put("ssoEmail", ssoEmail); + final String ssoUser = String.format("institution=%s, ssoEmail=%s, ssoIdentity=%s", institutionId, ssoEmail, ssoIdentity); + final String fullname = user.optString("fullname").trim(); final String givenName = user.optString("givenName").trim(); final String familyName = user.optString("familyName").trim(); final String isMemberOf = user.optString("isMemberOf").trim(); final String userRoles = user.optString("userRoles").trim(); - if (username.isEmpty()) { - LOGGER.error("[CAS XSLT] Missing email (username) for user at institution '{}'", institutionId); - throw new InstitutionSsoFailedException("Missing email (username)"); + if (ssoEmail.isEmpty()) { + LOGGER.error("[CAS XSLT] Error - Missing SSO Email for user: {}", ssoUser); + throw new InstitutionSsoAttributeMissingException("Missing SSO Email)"); } if (fullname.isEmpty() && (givenName.isEmpty() || familyName.isEmpty())) { - LOGGER.error("[CAS XSLT] Missing names: username={}, institution={}", username, institutionId); - throw new InstitutionSsoFailedException("Missing user's names"); + LOGGER.error("[CAS XSLT] Error - Missing names: {}", ssoUser); + throw new InstitutionSsoAttributeMissingException("Missing user's names"); } if (!isMemberOf.isEmpty()) { - LOGGER.info( - "[CAS XSLT] Shared SSO \"isMemberOf\" detected: username={}, institution={}, isMemberOf={}", - username, - institutionId, - isMemberOf - ); + LOGGER.info("[CAS XSLT] Shared SSO \"isMemberOf\" detected: {}, isMemberOf={}", ssoUser, isMemberOf); } else if (!userRoles.isEmpty()) { - LOGGER.info( - "[CAS XSLT] Shared SSO \"userRoles\" detected: username={}, institution={}, userRoles={}", - username, - institutionId, - userRoles - ); + LOGGER.info("[CAS XSLT] Shared SSO \"userRoles\" detected: {}, userRoles={}", ssoUser, userRoles); } else { - LOGGER.debug("[CAS XSLT] Shared SSO not eligible: username={}, institution={}", username, institutionId); + LOGGER.debug("[CAS XSLT] Shared SSO not eligible: {}", ssoUser); } // Parse the department attribute final String departmentRaw = user.optString("departmentRaw").trim(); @@ -621,17 +633,16 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( final boolean eduPerson = user.optBoolean("eduPerson"); String department = ""; if (!departmentRaw.isEmpty()) { - department = this.retrieveDepartment(departmentRaw, eduPerson); + department = this.retrieveDepartment(departmentRaw, eduPerson, ssoUser); LOGGER.info( - "[CAS XSLT] Department detected and parsed: username={}, institution={}, eduPerson={}, departmentRaw={}, department={}", - username, - institutionId, + "[CAS XSLT] Department detected and parsed: {}, eduPerson={}, departmentRaw={}, department={}", + ssoUser, eduPerson, departmentRaw, department ); } else { - LOGGER.debug("[CAS XSLT] Department is not provided: username={} institution={}", username, institutionId); + LOGGER.debug("[CAS XSLT] Department not provided: {}", ssoUser); } // Insert the `department` attribute into the payload, which does not overwrite `departmentRaw`. normalizedPayload.getJSONObject("provider").getJSONObject("user").put("department", department); @@ -639,31 +650,22 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( final boolean isSelectiveSso = user.optBoolean("isSelectiveSso"); String selectiveSsoFilter = ""; if (!isSelectiveSso) { - LOGGER.debug("[CAS XSLT] Selective SSO is not enabled: institution={}", institutionId); + LOGGER.debug("[CAS XSLT] Selective SSO is not enabled: {}", ssoUser); } else { selectiveSsoFilter = user.optString("selectiveSsoFilter").trim(); - LOGGER.debug("[CAS XSLT] Selective SSO is enabled for institution={} with filter={}", institutionId, selectiveSsoFilter); + LOGGER.debug("[CAS XSLT] Selective SSO is enabled: {}, selectiveSsoFilter={}", ssoUser, selectiveSsoFilter); } // Insert the `selectiveSsoFilter` attribute into the payload normalizedPayload.getJSONObject("provider").getJSONObject("user").put("selectiveSsoFilter", selectiveSsoFilter); final String osfApiInstnAuthnPayload = normalizedPayload.toString(); - LOGGER.info( - "[CAS XSLT] All attributes checked: username={}, institution={}", - username, - institutionId - ); - LOGGER.debug( - "[CAS XSLT] All attributes checked: username={}, institution={}, normalizedPayload={}", - username, - institutionId, - osfApiInstnAuthnPayload - ); + LOGGER.info("[CAS XSLT] All attributes checked: {}", ssoUser); + LOGGER.debug("[CAS XSLT] All attributes checked: {}, normalizedPayload={}", ssoUser, osfApiInstnAuthnPayload); // Build the payload to be sent to OSF API institution authentication endpoint final String jweString; try { final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() - .subject(username) + .subject(ssoEmail) .claim("data", osfApiInstnAuthnPayload) .expirationTime(new Date(new Date().getTime() + SIXTY_SECONDS)) .build(); @@ -678,18 +680,14 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( jweObject.encrypt(new DirectEncrypter(osfApiProperties.getInstnAuthnJweSecret().getBytes())); jweString = jweObject.serialize(); } catch (final JOSEException e) { - LOGGER.error( - "[OSF API] Notify Remote Principal Authenticated Failed: Payload Error - {}", - e.getMessage() - ); - throw new InstitutionSsoFailedException("OSF CAS failed to build JWT / JWE payload for OSF API"); + LOGGER.error("[OSF API] Exception - Failed to construct API Payload: {}, error={}", ssoUser, e.getMessage()); + throw new InstitutionSsoOsfApiFailedException("OSF CAS failed to build JWT / JWE payload for OSF API"); } // Send the POST request to OSF API to verify an existing institution user or to create a new one int statusCode = -1; int retry = 0; - final String ssoUser = String.format("institution=%s, username=%s", institutionId, username); HttpResponse httpResponse = null; - InstitutionSsoOsfApiFailureException casError = null; + InstitutionSsoOsfApiFailedException casError = null; while (retry < OSF_API_RETRY_LIMIT) { retry += 1; // Reset exception from previous attempt @@ -703,52 +701,27 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( .execute() .returnResponse(); statusCode = httpResponse.getStatusLine().getStatusCode(); - LOGGER.info( - "[OSF API] Notify Remote Principal Authenticated Response Received: {}, attempt={}, status={}", - ssoUser, - retry, - statusCode - ); + LOGGER.debug("[OSF API] Response Received: {}, attempt={}, status={}", ssoUser, retry, statusCode); // CAS expects OSF API to return HTTP 204 OK with no content if authentication succeeds if (statusCode == HttpStatus.SC_NO_CONTENT) { - LOGGER.info( - "[OSF API] Notify Remote Principal Authenticated Passed: {}, attempt={}, status={}", - ssoUser, - retry, - statusCode - ); - return new OsfApiInstitutionAuthenticationResult(username, institutionId); + LOGGER.info("[OSF API] Success - API request succeeded: {}, attempt={}, status={}", ssoUser, retry, statusCode); + return new OsfApiInstitutionAuthenticationResult(institutionId, ssoEmail, ssoIdentity); } if (OSF_API_RETRY_STATUS.contains(statusCode)) { - LOGGER.error( - "[OSF API] Notify Remote Principal Authenticated Failed - Server Error: {}, attempt={}, status={}", - ssoUser, - retry, - statusCode - ); - casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); + LOGGER.error("[OSF API] Failure - Server Error: {}, attempt={}, status={}", ssoUser, retry, statusCode); + casError = new InstitutionSsoOsfApiFailedException("Communication Error between OSF CAS and OSF API"); } else { break; } } catch (final IOException e) { - LOGGER.error( - "[OSF API] Notify Remote Principal Authenticated Failed - Communication Error: {}, attempt={}, error={}", - ssoUser, - retry, - e.getMessage() - ); - casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); + LOGGER.error("[OSF API] Exception - IO Exception: {}, attempt={}, error={}", ssoUser, retry, e.getMessage()); + casError = new InstitutionSsoOsfApiFailedException("Communication Error between OSF CAS and OSF API"); } try { TimeUnit.SECONDS.sleep(OSF_API_RETRY_DELAY_IN_SECONDS * retry); } catch (InterruptedException e) { - LOGGER.error( - "[OSF API] Notify Remote Principal Authenticated Failed - Retry Interrupted: {}, attempt={}, error={}", - ssoUser, - retry, - e.getMessage() - ); - casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); + LOGGER.error("[OSF API] Exception - Retry Interrupted: {}, attempt={}, error={}", ssoUser, retry, e.getMessage()); + casError = new InstitutionSsoOsfApiFailedException("Communication Error between OSF CAS and OSF API"); break; } } @@ -757,8 +730,8 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( } // Handler unexpected exceptions (i.e. any status other than 403) if (statusCode != HttpStatus.SC_FORBIDDEN) { - LOGGER.error("[OSF API] Notify Remote Principal Authenticated Failed: Unexpected Failure - statusCode={}", statusCode); - throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); + LOGGER.error("[OSF API] Failure - Unexpected HTTP response code: {}, statusCode={}", ssoUser, statusCode); + throw new InstitutionSsoOsfApiFailedException("OSF API failed to process CAS request"); } // CAS expects OSF API to return HTTP 403 FORBIDDEN with error details if authentication fails. String responseRaw; @@ -766,45 +739,43 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( HttpEntity entity = httpResponse.getEntity(); responseRaw = EntityUtils.toString(entity); } catch (final IOException | ParseException e) { - LOGGER.error("[OSF API] Notify Remote Principal Authenticated Failed: Entity String Parse Error - {}", e.getMessage()); + LOGGER.error("[OSF API] Exception - Invalid Response Entity: {}, error={}", ssoUser, e.getMessage()); throw new InstitutionSsoFailedException("CAS fails to parse OSF API error response"); } - // Handle failures due to denied selective SSO try { + // Attempt to identify and handle customized HTTP 403 FORBIDDEN failures final JsonObject responseJson = JsonParser.parseString(responseRaw).getAsJsonObject(); final JsonArray errorList = responseJson.getAsJsonArray("errors"); for (final JsonElement error : errorList) { if (!error.isJsonObject()) { - LOGGER.warn("[OSF API] Unexpected API Response Format: error is not a JSON object"); + LOGGER.warn("[OSF API] Warning - Invalid JSON Response: error is not a JSON object, check next"); continue; } if (!((JsonObject) error).has("detail")) { - LOGGER.warn("[OSF API] Unexpected API Response Format: missing key \"detail\" in the error object"); + LOGGER.warn("[OSF API] Warning - Invalid Response: missing key \"detail\" in the error object, check next"); continue; } final String errorDetail = ((JsonObject) error).get("detail").getAsString(); - if (OsfApiPermissionDenied.INSTITUTION_SELECTIVE_SSO_FAILURE.getId().equals(errorDetail)) { - LOGGER.error( - "[OSF API] Institution Selective SSO Not Allowed: institution={}, email={}, filter={}", - institutionId, - username, - selectiveSsoFilter - ); - throw new InstitutionSelectiveSsoFailedException("OSF API denies selective SSO login"); + if (OsfApiPermissionDenied.INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED.getId().equals(errorDetail)) { + LOGGER.error("[OSF API] Failure - Institution Selective SSO Not Allowed: {}, filter={}", ssoUser, selectiveSsoFilter); + throw new InstitutionSsoSelectiveLoginDeniedException("OSF API denies selective SSO login"); + } + if (OsfApiPermissionDenied.INSTITUTION_SSO_DUPLICATE_IDENTITY.getId().equals(errorDetail)) { + LOGGER.error("[OSF API] Failure - Duplicate SSO Identity: {}", ssoUser); + throw new InstitutionSsoDuplicateIdentityException("OSF API can't handle duplicate SSO identity"); + } + if (OsfApiPermissionDenied.INSTITUTION_SSO_ACCOUNT_INACTIVE.getId().equals(errorDetail)) { + LOGGER.error("[OSF API] Failure - Inactive Account: {}", ssoUser); + throw new InstitutionSsoDuplicateIdentityException("OSF API denies inactive account"); } } + // Handle unidentified HTTP 403 FORBIDDEN failures + LOGGER.error("[OSF API] Failure - HTTP 403 FORBIDDEN: {}, statusCode={}", ssoUser, statusCode); + throw new InstitutionSsoOsfApiFailedException("OSF API failed to process CAS request"); } catch (final JsonParseException | IllegalStateException e) { - LOGGER.error("[OSF API] Notify Remote Principal Authenticated Failed: JSON Object Parse Error - {}", e.getMessage()); - throw new InstitutionSsoFailedException("Fail to parse OSF API error response"); + LOGGER.error("[OSF API] Exception - Invalid Response: {}, error={}", ssoUser, e.getMessage()); + throw new InstitutionSsoOsfApiFailedException("CAS failed to parse OSF API error response"); } - // Handle other 403 response with general error details - LOGGER.error( - "[OSF API] Notify Remote Principal Authenticated Failed: statusCode={}, institution={}, username={}", - statusCode, - institutionId, - username - ); - throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); } /** @@ -812,9 +783,10 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( * * @param departmentRaw the raw department string * @param eduPerson whether the department attribute uses eduPerson schema + * @param ssoUser a string that includes institution ID, SSO email and SSO identity of the current SSO user * @return the department value */ - private String retrieveDepartment(final String departmentRaw, final boolean eduPerson) { + private String retrieveDepartment(final String departmentRaw, final boolean eduPerson, final String ssoUser) { // Return the raw value as it is if institutions do not use eduPerson schema for the department attribute if (!eduPerson) { @@ -832,12 +804,8 @@ private String retrieveDepartment(final String departmentRaw, final boolean eduP } } } catch (final InvalidNameException | IndexOutOfBoundsException e) { - LOGGER.error( - "[CAS XSLT] Invalid syntax for LDAP Distinguished Names: departmentRaw={}, error={}", - departmentRaw, - e.getMessage() - ); // Return an empty string if the syntax is wrong + LOGGER.error("[CAS XSLT] Exception - Invalid LDAP DN: {}, departmentRaw={}, error={}", ssoUser, departmentRaw, e.getMessage()); return ""; } return ""; diff --git a/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java b/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java index 0aa33611..43c4ad0d 100644 --- a/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java +++ b/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java @@ -60,11 +60,23 @@ public interface OsfCasWebflowConstants { String VIEW_ID_INVALID_VERIFICATION_KEY = "casInvalidVerificationKeyView"; + // Exception Views for Institution SSO + + String VIEW_ID_INSTITUTION_SSO_ACCOUNT_INACTIVE = "casInstitutionSsoAccountInactiveView"; + + String VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_MISSING = "casInstitutionSsoAttributeMissingView"; + + String VIEW_ID_INSTITUTION_SSO_ATTRIBUTE_PARSING_FAILED = "casInstitutionSsoAttributeParsingFailedView"; + + String VIEW_ID_INSTITUTION_SSO_DUPLICATE_IDENTITY = "casInstitutionSsoDuplicateIdentityView"; + String VIEW_ID_INSTITUTION_SSO_FAILED = "casInstitutionSsoFailedView"; - String VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED = "casInstitutionSelectiveSsoFailedView"; + String VIEW_ID_INSTITUTION_SSO_OSF_API_FAILED = "casInstitutionSsoOsfApiFailedView"; + + String VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED = "casInstitutionSsoSelectiveLoginDeniedView"; - String VIEW_ID_INSTITUTION_OSF_API_FAILURE = "casInstitutionOsfApiFailureView"; + // Exception Views for OAuth 2.0 Authorization Flow String VIEW_ID_OAUTH_20_ERROR_VIEW = "casOAuth20ErrorView"; } diff --git a/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java b/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java index 39adb4a4..e8ef3bf8 100644 --- a/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java +++ b/src/main/java/io/cos/cas/osf/web/support/OsfApiInstitutionAuthenticationResult.java @@ -27,23 +27,38 @@ public class OsfApiInstitutionAuthenticationResult implements Serializable { private static final long serialVersionUID = 3971349776123204760L; - private String username; - + /** + * The object ID of an OSF Institution. + */ private String institutionId; /** - * Verify that the username comes from one of the three attributes in Shibboleth SSO headers. + * The user's institutional email. + */ + private String ssoEmail; + + /** + * The user's institutional identity. + */ + private String ssoIdentity; + + /** + * Verify that the SSO email comes from one of the three attributes in Shibboleth SSO headers. + * + * Note: From OSF API's perspective, the email provided by SSO is stored in {@link #ssoEmail} which doesn't have to be + * the {@code username} f a candidate OSF user. From CAS's perspective, this {@link #ssoEmail} comes from three + * SSO attributes provided by Shibboleth's authn request: {@code eppn}, {@code mail} and {@code mailOther}. * - * @param ssoEppn eppn - * @param ssoMail mail - * @param ssoMailOther customized attribute for email - * @return true if username equals to any of the three else false + * @param eppn the eppn attribute + * @param mail the mail attribute + * @param mailOther the customized mail attribute + * @return {@code true} if {@link #ssoEmail} equals to any of the three email attributes; otherwise return {@code false} */ - public Boolean verifyOsfUsername(final String ssoEppn, final String ssoMail, final String ssoMailOther) { - if (StringUtils.isBlank(username)) { - LOGGER.error("[CAS XSLT] Username={} is blank", username); + public Boolean verifyOsfSsoEmail(final String eppn, final String mail, final String mailOther) { + if (StringUtils.isBlank(ssoEmail)) { + LOGGER.error("[CAS XSLT] SSO Email cannot be blank!"); return false; } - return username.equalsIgnoreCase(ssoEppn) || username.equalsIgnoreCase(ssoMail) || username.equalsIgnoreCase(ssoMailOther); + return ssoEmail.equalsIgnoreCase(eppn) || ssoEmail.equalsIgnoreCase(mail) || ssoEmail.equalsIgnoreCase(mailOther); } } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index bf41ada3..7e43c0af 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -686,16 +686,37 @@ screen.onetimepasswordrequired.message=Two-factor authentication has been enable style="white-space: nowrap" href="mailto:support@osf.io">OSF Support. screen.institutionssofailed.title=Institution SSO Error screen.institutionssofailed.heading=Institution login failed -screen.institutionssofailed.message=Your request cannot be completed at this time. Please \ - return to OSF and try again later.

If the issue persists, \ - check with your institution to verify your account is entitled to authenticate to OSF.

If you believe this \ - is in error, please contact Support for help. -screen.institutionselectivessofailed.message=Your institutional account is unable to authenticate to OSF. \ - Please check with your institution. If your institution believes this is in error, please contact \ - Support for help. -screen.institutionosfapifailure.message=Your request cannot be completed at this time due to an unexpected error. Please \ - return to OSF and try again later.

If the issue persists, \ - please contact Support for help. +screen.institutionssofailed.message=\ + Your request cannot be completed at this time. \ + Please return to OSF and try again later. \ + If the issue persists, check with your institution to verify your account is entitled to authenticate to OSF. \ + If you believe this is in error, \ + contact Support for help. +screen.institutionssoaccountinactive.message=\ + Institution login is not available for an inactive OSF account. \ + Please contact Support for help. +screen.institutionssoattributemissing.message=\ + Your request cannot be completed at this time. \ + The system failed to receive required information from your institution. \ + Please return to OSF and try again later. \ + If the issue persists, contact Support for help. +screen.institutionssoattributeparsingfailed.message=\ + Your request cannot be completed at this time. \ + The system failed to validate the information provided by your institution. \ + Please return to OSF and try again later. \ + If the issue persists, contact Support for help. +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=\ + 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.institutionssoosfapifailed.message=\ + Your request cannot be completed at this time due to an unexpected error. \ + Please return to OSF and try again later. \ + If the issue persists, contact Support for help. + # # OAuth 2.0 Views and Error Views # diff --git a/src/main/resources/templates/casInstitutionOsfApiFailureView.html b/src/main/resources/templates/casInstitutionOsfApiFailureView.html deleted file mode 100644 index f78a478f..00000000 --- a/src/main/resources/templates/casInstitutionOsfApiFailureView.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - -
- -
- -
- - - -
- - - diff --git a/src/main/resources/templates/casInstitutionSelectiveSsoFailedView.html b/src/main/resources/templates/casInstitutionSsoAccountInactiveView.html similarity index 96% rename from src/main/resources/templates/casInstitutionSelectiveSsoFailedView.html rename to src/main/resources/templates/casInstitutionSsoAccountInactiveView.html index ad3070c3..0f1b48c6 100644 --- a/src/main/resources/templates/casInstitutionSelectiveSsoFailedView.html +++ b/src/main/resources/templates/casInstitutionSsoAccountInactiveView.html @@ -25,7 +25,7 @@

-

+

diff --git a/src/main/resources/templates/casInstitutionSsoAttributeMissingView.html b/src/main/resources/templates/casInstitutionSsoAttributeMissingView.html new file mode 100644 index 00000000..d23ff05b --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoAttributeMissingView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + diff --git a/src/main/resources/templates/casInstitutionSsoAttributeParsingFailedView.html b/src/main/resources/templates/casInstitutionSsoAttributeParsingFailedView.html new file mode 100644 index 00000000..8146be64 --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoAttributeParsingFailedView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + diff --git a/src/main/resources/templates/casInstitutionSsoDuplicateIdentityView.html b/src/main/resources/templates/casInstitutionSsoDuplicateIdentityView.html new file mode 100644 index 00000000..642243b2 --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoDuplicateIdentityView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + diff --git a/src/main/resources/templates/casInstitutionSsoFailedView.html b/src/main/resources/templates/casInstitutionSsoFailedView.html index 5dd1cd0d..ce441b23 100644 --- a/src/main/resources/templates/casInstitutionSsoFailedView.html +++ b/src/main/resources/templates/casInstitutionSsoFailedView.html @@ -27,14 +27,14 @@

-
- - +
+ +

-
- +
+
diff --git a/src/main/resources/templates/casInstitutionSsoOsfApiFailedView.html b/src/main/resources/templates/casInstitutionSsoOsfApiFailedView.html new file mode 100644 index 00000000..1a687c36 --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoOsfApiFailedView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + diff --git a/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html b/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html new file mode 100644 index 00000000..36ee35ef --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + +