Skip to content

Commit 2cca406

Browse files
committed
Dedupe ssoEmail when creating credential after OSF API success
1 parent e691d33 commit 2cca406

2 files changed

Lines changed: 55 additions & 6 deletions

File tree

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,12 @@
9898
import java.io.IOException;
9999
import java.io.StringWriter;
100100
import java.util.Collections;
101+
import java.util.Arrays;
101102
import java.util.Date;
103+
import java.util.HashSet;
102104
import java.util.List;
103105
import java.util.Map;
106+
import java.util.Set;
104107
import java.util.concurrent.TimeUnit;
105108

106109
/**
@@ -166,6 +169,8 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon
166169

167170
private static final String ATTRIBUTE_PREFIX = "auth-";
168171

172+
private static final String MULTIPLE_ATTRIBUTE_DELIMITER = ";";
173+
169174
private static final String SHIBBOLETH_SESSION_HEADER = ATTRIBUTE_PREFIX + "shib-session-id";
170175

171176
private static final String SHIBBOLETH_COOKIE_PREFIX = "_shibsession_";
@@ -720,7 +725,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess(
720725
// CAS expects OSF API to return HTTP 204 OK with no content if authentication succeeds
721726
if (statusCode == HttpStatus.SC_NO_CONTENT) {
722727
LOGGER.info("[OSF API] Success - API request succeeded: {}, attempt={}, status={}", ssoUser, retry, statusCode);
723-
return new OsfApiInstitutionAuthenticationResult(institutionId, ssoEmail, ssoIdentity);
728+
return new OsfApiInstitutionAuthenticationResult(institutionId, deduplicateSsoEmail(ssoEmail, ssoUser), ssoIdentity);
724729
}
725730
if (OSF_API_RETRY_STATUS.contains(statusCode)) {
726731
LOGGER.error("[OSF API] Failure - Server Error: {}, attempt={}, status={}", ssoUser, retry, statusCode);
@@ -869,4 +874,32 @@ private void setSsoErrorContext(
869874
);
870875
context.getFlowScope().put(PARAMETER_SSO_ERROR_CONTEXT, ssoErrorContext);
871876
}
877+
878+
/**
879+
* Attempt to deduplicate the {@code ssoEmail} attribute. This method is only called after OSF API has already
880+
* successfully deduplicated the attribute and thus should throw {@link InstitutionSsoFailedException} if failed.
881+
*
882+
* @param ssoEmail the SSO email to deduplicate
883+
* @param ssoUser the SSO user information
884+
* @return deduplicated SSO email on success
885+
* @throws InstitutionSsoFailedException if deduplication failed
886+
*/
887+
private String deduplicateSsoEmail(
888+
final String ssoEmail,
889+
final String ssoUser
890+
) throws InstitutionSsoFailedException {
891+
if (StringUtils.isBlank(ssoEmail)) {
892+
LOGGER.error("[OSF CAS] Critical Error: SSO email should not be blank after OSF API success: [{}]", ssoUser);
893+
throw new InstitutionSsoFailedException("SSO email should not be blank");
894+
}
895+
if (!ssoEmail.contains(MULTIPLE_ATTRIBUTE_DELIMITER)) {
896+
return ssoEmail;
897+
}
898+
Set<String> ssoEmailSet = new HashSet<>(Arrays.asList(ssoEmail.split(MULTIPLE_ATTRIBUTE_DELIMITER)));
899+
if (ssoEmailSet.size() != 1) {
900+
LOGGER.error("[OSF CAS] Critical Error: SSO email should not fail deduplication after OSF API success: [{}]", ssoUser);
901+
throw new InstitutionSsoFailedException("SSO email should not fail deduplication");
902+
}
903+
return ssoEmailSet.iterator().next();
904+
}
872905
}

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

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
@Slf4j
2626
public class OsfApiInstitutionAuthenticationResult implements Serializable {
2727

28+
/**
29+
* Serialization metadata.
30+
*/
2831
private static final long serialVersionUID = 3971349776123204760L;
2932

3033
/**
@@ -44,10 +47,11 @@ public class OsfApiInstitutionAuthenticationResult implements Serializable {
4447

4548
/**
4649
* Verify that the SSO email comes from one of the three attributes in Shibboleth SSO headers.
47-
*
48-
* Note: From OSF API's perspective, the email provided by SSO is stored in {@link #ssoEmail} which doesn't have to be
49-
* the {@code username} f a candidate OSF user. From CAS's perspective, this {@link #ssoEmail} comes from three
50-
* SSO attributes provided by Shibboleth's authn request: {@code eppn}, {@code mail} and {@code mailOther}.
50+
* From OSF API's perspective, the email provided by SSO is stored in {@link #ssoEmail} which doesn't have to be
51+
* the {@code username} f a candidate OSF user. From CAS's perspective, this {@link #ssoEmail} comes from three
52+
* SSO attributes provided by Shibboleth's authn request: {@code eppn}, {@code mail} and {@code mailOther}.
53+
* Since {@link #ssoEmail} may have already been deduplicated after successful OSF API request, this method does
54+
* substring check instead of equality check.
5155
*
5256
* @param eppn the eppn attribute
5357
* @param mail the mail attribute
@@ -59,6 +63,18 @@ public Boolean verifyOsfSsoEmail(final String eppn, final String mail, final Str
5963
LOGGER.error("[CAS XSLT] SSO Email cannot be blank!");
6064
return false;
6165
}
62-
return ssoEmail.equalsIgnoreCase(eppn) || ssoEmail.equalsIgnoreCase(mail) || ssoEmail.equalsIgnoreCase(mailOther);
66+
boolean isPartOfEppn = Boolean.FALSE;
67+
boolean isPartOfMail = Boolean.FALSE;
68+
boolean isPartOfMailOther = Boolean.FALSE;
69+
if (!StringUtils.isBlank(eppn)) {
70+
isPartOfEppn = eppn.toLowerCase().contains(ssoEmail.toLowerCase());
71+
}
72+
if (!StringUtils.isBlank(mail)) {
73+
isPartOfMail = mail.toLowerCase().contains(ssoEmail.toLowerCase());
74+
}
75+
if (!StringUtils.isBlank(mailOther)) {
76+
isPartOfMailOther = mailOther.toLowerCase().contains(ssoEmail.toLowerCase());
77+
}
78+
return isPartOfEppn || isPartOfMail || isPartOfMailOther;
6379
}
6480
}

0 commit comments

Comments
 (0)