From 58710f00888813b8234498a85a8e4d667addd8ae Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 1 Sep 2025 15:59:06 -0400 Subject: [PATCH 1/3] Handle institution SSO multiple emails not supported error --- ...soMultipleEmailsNotSupportedException.java | 31 ++++++++++++ .../support/OsfApiPermissionDenied.java | 4 +- .../OsfCasCoreWebflowConfiguration.java | 2 + .../OsfCasLoginWebflowConfigurer.java | 11 +++++ ...alFromNonInteractiveCredentialsAction.java | 5 ++ .../flow/support/OsfCasWebflowConstants.java | 2 + src/main/resources/messages.properties | 5 ++ ...tionSsoMultipleEmailsNotSupportedView.html | 49 +++++++++++++++++++ 8 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java create mode 100644 src/main/resources/templates/casInstitutionSsoMultipleEmailsNotSupportedView.html diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java new file mode 100644 index 00000000..6d831de9 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java @@ -0,0 +1,31 @@ +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 OSF API not supporting multiple SSO emails. + * + * @author Longze Chen + * @since 25.0.0 + */ +@NoArgsConstructor +public class InstitutionSsoMultipleEmailsNotSupportedException extends AccountException { + + /** + * Serialization metadata. + */ + + private static final long serialVersionUID = -7703550523317297865L; + + /** + * Instantiates a new {@link InstitutionSsoMultipleEmailsNotSupportedException}. + * + * @param msg the msg + */ + public InstitutionSsoMultipleEmailsNotSupportedException(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 cf69beca..88b7d25a 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,9 +10,11 @@ public enum OsfApiPermissionDenied { DEFAULT("PermissionDenied"), + INSTITUTION_SSO_ACCOUNT_INACTIVE("InstitutionSsoAccountInactive"), + INSTITUTION_SSO_DUPLICATE_IDENTITY("InstitutionSsoDuplicateIdentity"), - INSTITUTION_SSO_ACCOUNT_INACTIVE("InstitutionSsoAccountInactive"), + INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED("InstitutionSsoMultipleEmailsNotSupported"), INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED("InstitutionSsoSelectiveLoginDenied"); 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 cdfea1e7..7014d3d0 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 @@ -7,6 +7,7 @@ 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.InstitutionSsoMultipleEmailsNotSupportedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoSelectiveLoginDeniedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; @@ -53,6 +54,7 @@ public Set> handledAuthenticationExceptions() { errors.add(InstitutionSsoAttributeParsingException.class); errors.add(InstitutionSsoDuplicateIdentityException.class); errors.add(InstitutionSsoFailedException.class); + errors.add(InstitutionSsoMultipleEmailsNotSupportedException.class); errors.add(InstitutionSsoOsfApiFailedException.class); errors.add(InstitutionSsoSelectiveLoginDeniedException.class); errors.add(InvalidOneTimePasswordException.class); 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 8144ca74..d232f6db 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 @@ -8,6 +8,7 @@ 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.InstitutionSsoMultipleEmailsNotSupportedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoSelectiveLoginDeniedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; @@ -270,6 +271,11 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) { InstitutionSsoSelectiveLoginDeniedException.class.getSimpleName(), OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED ); + createTransitionForState( + handler, + InstitutionSsoMultipleEmailsNotSupportedException.class.getSimpleName(), + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED + ); createTransitionForState( handler, InvalidUserStatusException.class.getSimpleName(), @@ -470,6 +476,11 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) { OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED, OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED ); + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED + ); } /** 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 38b2742d..92eeca73 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 @@ -12,6 +12,7 @@ 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.InstitutionSsoMultipleEmailsNotSupportedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoSelectiveLoginDeniedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException; import io.cos.cas.osf.authentication.support.DelegationProtocol; @@ -791,6 +792,10 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( LOGGER.error("[OSF API] Failure - Inactive Account: {}", ssoUser); throw new InstitutionSsoAccountInactiveException("OSF API denies inactive account"); } + if (OsfApiPermissionDenied.INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED.getId().equals(errorDetail)) { + LOGGER.error("[OSF API] Failure - Multiple SSO Emails Error: {}", ssoUser); + throw new InstitutionSsoMultipleEmailsNotSupportedException("OSF API can't process multiple SSO emails"); + } } // Handle unidentified HTTP 403 FORBIDDEN failures LOGGER.error("[OSF API] Failure - HTTP 403 FORBIDDEN: {}, statusCode={}", ssoUser, statusCode); 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 43c4ad0d..6f938981 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 @@ -76,6 +76,8 @@ public interface OsfCasWebflowConstants { String VIEW_ID_INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED = "casInstitutionSsoSelectiveLoginDeniedView"; + String VIEW_ID_INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED = "casInstitutionSsoMultipleEmailsNotSupportedView"; + // Exception Views for OAuth 2.0 Authorization Flow String VIEW_ID_OAUTH_20_ERROR_VIEW = "casOAuth20ErrorView"; diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 4e99c955..b051bb89 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -721,6 +721,11 @@ 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. +screen.institutionssomultipleemailsnotsupported.message=\ + Your request cannot be completed at this time. \ + The system received multiple SSO emails from your institution. \ + 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/casInstitutionSsoMultipleEmailsNotSupportedView.html b/src/main/resources/templates/casInstitutionSsoMultipleEmailsNotSupportedView.html new file mode 100644 index 00000000..829a6cd5 --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoMultipleEmailsNotSupportedView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + From e691d33f17e786fb514dcc27988e6fdc75c0c76b Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 1 Sep 2025 16:43:11 -0400 Subject: [PATCH 2/3] Fix linting --- .../InstitutionSsoMultipleEmailsNotSupportedException.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java index 6d831de9..d3283b04 100644 --- a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoMultipleEmailsNotSupportedException.java @@ -17,7 +17,6 @@ public class InstitutionSsoMultipleEmailsNotSupportedException extends AccountEx /** * Serialization metadata. */ - private static final long serialVersionUID = -7703550523317297865L; /** From 2cca406f1ffc47921691b0d7186911727ebc811e Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 5 Sep 2025 15:46:12 -0400 Subject: [PATCH 3/3] Dedupe ssoEmail when creating credential after OSF API success --- ...alFromNonInteractiveCredentialsAction.java | 35 ++++++++++++++++++- ...OsfApiInstitutionAuthenticationResult.java | 26 +++++++++++--- 2 files changed, 55 insertions(+), 6 deletions(-) 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 92eeca73..7b883cf1 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 @@ -98,9 +98,12 @@ import java.io.IOException; import java.io.StringWriter; import java.util.Collections; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -166,6 +169,8 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon private static final String ATTRIBUTE_PREFIX = "auth-"; + private static final String MULTIPLE_ATTRIBUTE_DELIMITER = ";"; + private static final String SHIBBOLETH_SESSION_HEADER = ATTRIBUTE_PREFIX + "shib-session-id"; private static final String SHIBBOLETH_COOKIE_PREFIX = "_shibsession_"; @@ -720,7 +725,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( // 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] Success - API request succeeded: {}, attempt={}, status={}", ssoUser, retry, statusCode); - return new OsfApiInstitutionAuthenticationResult(institutionId, ssoEmail, ssoIdentity); + return new OsfApiInstitutionAuthenticationResult(institutionId, deduplicateSsoEmail(ssoEmail, ssoUser), ssoIdentity); } if (OSF_API_RETRY_STATUS.contains(statusCode)) { LOGGER.error("[OSF API] Failure - Server Error: {}, attempt={}, status={}", ssoUser, retry, statusCode); @@ -869,4 +874,32 @@ private void setSsoErrorContext( ); context.getFlowScope().put(PARAMETER_SSO_ERROR_CONTEXT, ssoErrorContext); } + + /** + * Attempt to deduplicate the {@code ssoEmail} attribute. This method is only called after OSF API has already + * successfully deduplicated the attribute and thus should throw {@link InstitutionSsoFailedException} if failed. + * + * @param ssoEmail the SSO email to deduplicate + * @param ssoUser the SSO user information + * @return deduplicated SSO email on success + * @throws InstitutionSsoFailedException if deduplication failed + */ + private String deduplicateSsoEmail( + final String ssoEmail, + final String ssoUser + ) throws InstitutionSsoFailedException { + if (StringUtils.isBlank(ssoEmail)) { + LOGGER.error("[OSF CAS] Critical Error: SSO email should not be blank after OSF API success: [{}]", ssoUser); + throw new InstitutionSsoFailedException("SSO email should not be blank"); + } + if (!ssoEmail.contains(MULTIPLE_ATTRIBUTE_DELIMITER)) { + return ssoEmail; + } + Set ssoEmailSet = new HashSet<>(Arrays.asList(ssoEmail.split(MULTIPLE_ATTRIBUTE_DELIMITER))); + if (ssoEmailSet.size() != 1) { + LOGGER.error("[OSF CAS] Critical Error: SSO email should not fail deduplication after OSF API success: [{}]", ssoUser); + throw new InstitutionSsoFailedException("SSO email should not fail deduplication"); + } + return ssoEmailSet.iterator().next(); + } } 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 e8ef3bf8..7d19f605 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 @@ -25,6 +25,9 @@ @Slf4j public class OsfApiInstitutionAuthenticationResult implements Serializable { + /** + * Serialization metadata. + */ private static final long serialVersionUID = 3971349776123204760L; /** @@ -44,10 +47,11 @@ public class OsfApiInstitutionAuthenticationResult implements Serializable { /** * 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}. + * 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}. + * Since {@link #ssoEmail} may have already been deduplicated after successful OSF API request, this method does + * substring check instead of equality check. * * @param eppn the eppn attribute * @param mail the mail attribute @@ -59,6 +63,18 @@ public Boolean verifyOsfSsoEmail(final String eppn, final String mail, final Str LOGGER.error("[CAS XSLT] SSO Email cannot be blank!"); return false; } - return ssoEmail.equalsIgnoreCase(eppn) || ssoEmail.equalsIgnoreCase(mail) || ssoEmail.equalsIgnoreCase(mailOther); + boolean isPartOfEppn = Boolean.FALSE; + boolean isPartOfMail = Boolean.FALSE; + boolean isPartOfMailOther = Boolean.FALSE; + if (!StringUtils.isBlank(eppn)) { + isPartOfEppn = eppn.toLowerCase().contains(ssoEmail.toLowerCase()); + } + if (!StringUtils.isBlank(mail)) { + isPartOfMail = mail.toLowerCase().contains(ssoEmail.toLowerCase()); + } + if (!StringUtils.isBlank(mailOther)) { + isPartOfMailOther = mailOther.toLowerCase().contains(ssoEmail.toLowerCase()); + } + return isPartOfEppn || isPartOfMail || isPartOfMailOther; } }