From a3694450eff5225b92aac6001f65fb885739883e Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 21 Jun 2022 15:34:59 -0400 Subject: [PATCH 01/10] Release eppn to OSF API for eligible institutions --- ...incipalFromNonInteractiveCredentialsAction.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 8a8e8ff..e99f0c2 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 @@ -647,6 +647,20 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( // Insert the `selectiveSsoFilter` attribute into the payload normalizedPayload.getJSONObject("provider").getJSONObject("user").put("selectiveSsoFilter", selectiveSsoFilter); + // Check if `eppn` is provided for user's institutional identity. + // This is a temporary solution for institutions that switched its email attribute from `eppn` to `mail`. + final String eduPersonPrincipalName = user.optString("eppn").trim(); + if (!eduPersonPrincipalName.isEmpty()) { + LOGGER.info( + "[CAS XSLT] eduPersonPrincipalName detected for user identity: eduPersonPrincipalName={}, username={}, institution={}", + eduPersonPrincipalName, + username, + institutionId + ); + } + // Insert the `eduPersonPrincipalName` attribute into the payload + normalizedPayload.getJSONObject("provider").getJSONObject("user").put("eppn", eduPersonPrincipalName); + final String osfApiInstnAuthnPayload = normalizedPayload.toString(); LOGGER.info( "[CAS XSLT] All attributes checked: username={}, institution={}", From e39541f1188de5430e06b02832ed8b8d96dddc90 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Wed, 29 Jun 2022 13:29:40 -0400 Subject: [PATCH 02/10] Disambiguate ssoEmail and username; release ssoEmail to OSF --- ...alFromNonInteractiveCredentialsAction.java | 51 ++++++++++--------- ...OsfApiInstitutionAuthenticationResult.java | 2 +- 2 files changed, 27 insertions(+), 26 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 e99f0c2..0cb3558 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 @@ -238,7 +238,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; @@ -277,13 +277,12 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex ); throw new InstitutionSsoFailedException("Critical SAML-Shibboleth SSO Failure"); } - - osfPostgresCredential.setUsername(remoteUserInfo.getUsername()); + osfPostgresCredential.setUsername(remoteUserInfo.getSsoEmail()); osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId()); if (StringUtils.isBlank(osfPostgresCredential.getInstitutionalIdentity())) { LOGGER.warn( - "[SAML Shibboleth] Missing user's institutional identity: username={}, institutionId={}", - remoteUserInfo.getUsername(), + "[SAML Shibboleth] Missing user's institutional identity: ssoEmail={}, institutionId={}", + remoteUserInfo.getSsoEmail(), remoteUserInfo.getInstitutionId() ); } @@ -582,24 +581,24 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( LOGGER.error("[CAS XSLT] Missing institutional user"); throw new InstitutionSsoFailedException("Missing institutional user"); } - final String username = user.optString("username").trim(); + final String ssoEmail = user.optString("username").trim(); 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()) { + if (ssoEmail.isEmpty()) { LOGGER.error("[CAS XSLT] Missing email (username) for user at institution '{}'", institutionId); throw new InstitutionSsoFailedException("Missing email (username)"); } if (fullname.isEmpty() && (givenName.isEmpty() || familyName.isEmpty())) { - LOGGER.error("[CAS XSLT] Missing names: username={}, institution={}", username, institutionId); + LOGGER.error("[CAS XSLT] Missing names: ssoEmail={}, institution={}", ssoEmail, institutionId); throw new InstitutionSsoFailedException("Missing user's names"); } if (!isMemberOf.isEmpty()) { LOGGER.info( - "[CAS XSLT] Shared SSO \"isMemberOf\" detected: username={}, institution={}, isMemberOf={}", - username, + "[CAS XSLT] Shared SSO \"isMemberOf\" detected: ssoEmail={}, institution={}, isMemberOf={}", + ssoEmail, institutionId, isMemberOf ); @@ -611,7 +610,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( userRoles ); } else { - LOGGER.debug("[CAS XSLT] Shared SSO not eligible: username={}, institution={}", username, institutionId); + LOGGER.debug("[CAS XSLT] Shared SSO not eligible: ssoEmail={}, institution={}", ssoEmail, institutionId); } // Parse the department attribute final String departmentRaw = user.optString("departmentRaw").trim(); @@ -623,15 +622,15 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( if (!departmentRaw.isEmpty()) { department = this.retrieveDepartment(departmentRaw, eduPerson); LOGGER.info( - "[CAS XSLT] Department detected and parsed: username={}, institution={}, eduPerson={}, departmentRaw={}, department={}", - username, + "[CAS XSLT] Department detected and parsed: ssoEmail={}, institution={}, eduPerson={}, departmentRaw={}, department={}", + ssoEmail, institutionId, eduPerson, departmentRaw, department ); } else { - LOGGER.debug("[CAS XSLT] Department is not provided: username={} institution={}", username, institutionId); + LOGGER.debug("[CAS XSLT] Department is not provided: ssoEmail={} institution={}", ssoEmail, institutionId); } // Insert the `department` attribute into the payload, which does not overwrite `departmentRaw`. normalizedPayload.getJSONObject("provider").getJSONObject("user").put("department", department); @@ -652,24 +651,26 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( final String eduPersonPrincipalName = user.optString("eppn").trim(); if (!eduPersonPrincipalName.isEmpty()) { LOGGER.info( - "[CAS XSLT] eduPersonPrincipalName detected for user identity: eduPersonPrincipalName={}, username={}, institution={}", + "[CAS XSLT] eduPersonPrincipalName detected for user identity: eduPersonPrincipalName={}, ssoEmail={}, institution={}", eduPersonPrincipalName, - username, + ssoEmail, institutionId ); } // Insert the `eduPersonPrincipalName` attribute into the payload normalizedPayload.getJSONObject("provider").getJSONObject("user").put("eppn", eduPersonPrincipalName); + // Insert `username` as `ssoEmail` into the payload + normalizedPayload.getJSONObject("provider").getJSONObject("user").put("ssoEmail", ssoEmail); final String osfApiInstnAuthnPayload = normalizedPayload.toString(); LOGGER.info( - "[CAS XSLT] All attributes checked: username={}, institution={}", - username, + "[CAS XSLT] All attributes checked: ssoEmail={}, institution={}", + ssoEmail, institutionId ); LOGGER.debug( - "[CAS XSLT] All attributes checked: username={}, institution={}, normalizedPayload={}", - username, + "[CAS XSLT] All attributes checked: ssoEmail={}, institution={}, normalizedPayload={}", + ssoEmail, institutionId, osfApiInstnAuthnPayload ); @@ -677,7 +678,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( 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(); @@ -701,7 +702,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( // 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); + final String ssoUser = String.format("institution=%s, ssoEmail=%s", institutionId, ssoEmail); HttpResponse httpResponse = null; InstitutionSsoOsfApiFailureException casError = null; while (retry < OSF_API_RETRY_LIMIT) { @@ -801,7 +802,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( LOGGER.error( "[OSF API] Institution Selective SSO Not Allowed: institution={}, email={}, filter={}", institutionId, - username, + ssoEmail, selectiveSsoFilter ); throw new InstitutionSelectiveSsoFailedException("OSF API denies selective SSO login"); @@ -813,10 +814,10 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( } // Handle other 403 response with general error details LOGGER.error( - "[OSF API] Notify Remote Principal Authenticated Failed: statusCode={}, institution={}, username={}", + "[OSF API] Notify Remote Principal Authenticated Failed: statusCode={}, institution={}, ssoEmail={}", statusCode, institutionId, - username + ssoEmail ); throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); } 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 39adb4a..7d7f1b7 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,7 +27,7 @@ public class OsfApiInstitutionAuthenticationResult implements Serializable { private static final long serialVersionUID = 3971349776123204760L; - private String username; + private String ssoEmail; private String institutionId; From cd8b96ad9b6d419cec605d8e46dc4de4ccab17b5 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 17 Jan 2023 22:36:24 -0500 Subject: [PATCH 03/10] Refactor a few key components for the non-interactive authn action * Shib session and attributes parsing * Initial credential construction from shib * API payload creation * Authentication result object * Final credential update from API result * Logs and errors * More JavaDoc --- .../credential/OsfPostgresCredential.java | 2 +- ...alFromNonInteractiveCredentialsAction.java | 62 ++++++++++++------- ...OsfApiInstitutionAuthenticationResult.java | 35 ++++++++--- 3 files changed, 66 insertions(+), 33 deletions(-) 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 9027ecd..0d5fa64 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/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java index 0cb3558..7ebb0c5 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 @@ -260,32 +260,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"); } + // 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: ssoEmail={}, institutionId={}", - remoteUserInfo.getSsoEmail(), - 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."); @@ -442,17 +449,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 @@ -463,7 +475,6 @@ private OsfPostgresCredential constructCredentialsFromShibbolethAuthentication( ); } } - osfPostgresCredential.setInstitutionalIdentity(remoteUser); return osfPostgresCredential; } @@ -581,7 +592,14 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( LOGGER.error("[CAS XSLT] Missing institutional user"); throw new InstitutionSsoFailedException("Missing institutional user"); } + // 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 fullname = user.optString("fullname").trim(); final String givenName = user.optString("givenName").trim(); final String familyName = user.optString("familyName").trim(); 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 7d7f1b7..e8ef3bf 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; + /** + * The object ID of an OSF Institution. + */ + private String institutionId; + + /** + * The user's institutional email. + */ private String ssoEmail; - private String institutionId; + /** + * The user's institutional identity. + */ + private String ssoIdentity; /** - * Verify that the username comes from one of the three attributes in Shibboleth SSO headers. + * 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); } } From 57c8a7d4e3461b068b27398623abd2e7f6d67904 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 17 Jan 2023 23:46:12 -0500 Subject: [PATCH 04/10] Rewrite SSO logs and exception messages; remove temporary eppn hack * Normalized format string in all logs * All logs include SSO info (institution, email and identity) * All logs start with their "[Component Name]" * All logs are trimmed (but remains distinguishable and readable) * Error logs now have 3 different types: Error, Exception, Failure In addition, removed the temporary solution that uses `eppn` as both email and identity. This hack was added to solve one special case where an institution changed their email domain. --- ...alFromNonInteractiveCredentialsAction.java | 156 +++++------------- 1 file changed, 40 insertions(+), 116 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 7ebb0c5..3f23253 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 @@ -573,23 +573,23 @@ 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()); + LOGGER.error("[CAS XSLT] Exception - Failed to normalize attributes in the credential: {}", e.getMessage()); throw new InstitutionSsoFailedException("Attribute normalization failure"); } // Verify required and optional attributes final JSONObject provider = normalizedPayload.optJSONObject("provider"); if (provider == null) { - LOGGER.error("[CAS XSLT] Missing identity provider."); + LOGGER.error("[CAS XSLT] Error - Missing identity provider."); throw new InstitutionSsoFailedException("Missing identity provider"); } final String institutionId = provider.optString("id").trim(); if (institutionId.isEmpty()) { - LOGGER.error("[CAS XSLT] Empty identity provider"); + LOGGER.error("[CAS XSLT] Error - Empty identity provider"); throw new InstitutionSsoFailedException("Empty identity provider"); } final JSONObject user = provider.optJSONObject("user"); if (user == null) { - LOGGER.error("[CAS XSLT] Missing institutional user"); + LOGGER.error("[CAS XSLT] Error - Missing institutional user"); throw new InstitutionSsoFailedException("Missing institutional user"); } // Note: SSO Identity didn't come from the normalized attribute set but came from a dedicated Shibboleth header. It was parsed, @@ -600,35 +600,27 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( // "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 (ssoEmail.isEmpty()) { - LOGGER.error("[CAS XSLT] Missing email (username) for user at institution '{}'", institutionId); + LOGGER.error("[CAS XSLT] Error - Missing SSO Email for user: {}", ssoUser); throw new InstitutionSsoFailedException("Missing email (username)"); } if (fullname.isEmpty() && (givenName.isEmpty() || familyName.isEmpty())) { - LOGGER.error("[CAS XSLT] Missing names: ssoEmail={}, institution={}", ssoEmail, institutionId); + LOGGER.error("[CAS XSLT] Error - Missing names: {}", ssoUser); throw new InstitutionSsoFailedException("Missing user's names"); } if (!isMemberOf.isEmpty()) { - LOGGER.info( - "[CAS XSLT] Shared SSO \"isMemberOf\" detected: ssoEmail={}, institution={}, isMemberOf={}", - ssoEmail, - 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: ssoEmail={}, institution={}", ssoEmail, institutionId); + LOGGER.debug("[CAS XSLT] Shared SSO not eligible: {}", ssoUser); } // Parse the department attribute final String departmentRaw = user.optString("departmentRaw").trim(); @@ -638,17 +630,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: ssoEmail={}, institution={}, eduPerson={}, departmentRaw={}, department={}", - ssoEmail, - institutionId, + "[CAS XSLT] Department detected and parsed: {}, eduPerson={}, departmentRaw={}, department={}", + ssoUser, eduPerson, departmentRaw, department ); } else { - LOGGER.debug("[CAS XSLT] Department is not provided: ssoEmail={} institution={}", ssoEmail, 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); @@ -656,42 +647,17 @@ 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); - // Check if `eppn` is provided for user's institutional identity. - // This is a temporary solution for institutions that switched its email attribute from `eppn` to `mail`. - final String eduPersonPrincipalName = user.optString("eppn").trim(); - if (!eduPersonPrincipalName.isEmpty()) { - LOGGER.info( - "[CAS XSLT] eduPersonPrincipalName detected for user identity: eduPersonPrincipalName={}, ssoEmail={}, institution={}", - eduPersonPrincipalName, - ssoEmail, - institutionId - ); - } - // Insert the `eduPersonPrincipalName` attribute into the payload - normalizedPayload.getJSONObject("provider").getJSONObject("user").put("eppn", eduPersonPrincipalName); - // Insert `username` as `ssoEmail` into the payload - normalizedPayload.getJSONObject("provider").getJSONObject("user").put("ssoEmail", ssoEmail); - final String osfApiInstnAuthnPayload = normalizedPayload.toString(); - LOGGER.info( - "[CAS XSLT] All attributes checked: ssoEmail={}, institution={}", - ssoEmail, - institutionId - ); - LOGGER.debug( - "[CAS XSLT] All attributes checked: ssoEmail={}, institution={}, normalizedPayload={}", - ssoEmail, - 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 { @@ -711,16 +677,12 @@ 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() - ); + LOGGER.error("[OSF API] Exception - Failed to construct API Payload: {}, error={}", ssoUser, e.getMessage()); throw new InstitutionSsoFailedException("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, ssoEmail=%s", institutionId, ssoEmail); HttpResponse httpResponse = null; InstitutionSsoOsfApiFailureException casError = null; while (retry < OSF_API_RETRY_LIMIT) { @@ -736,51 +698,26 @@ 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 - ); + LOGGER.error("[OSF API] Failure - Server Error: {}, attempt={}, status={}", ssoUser, retry, statusCode); casError = new InstitutionSsoOsfApiFailureException("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() - ); + LOGGER.error("[OSF API] Exception - IO Exception: {}, attempt={}, error={}", ssoUser, retry, e.getMessage()); casError = new InstitutionSsoOsfApiFailureException("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() - ); + LOGGER.error("[OSF API] Exception - Retry Interrupted: {}, attempt={}, error={}", ssoUser, retry, e.getMessage()); casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); break; } @@ -790,7 +727,7 @@ 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); + LOGGER.error("[OSF API] Failure - Unexpected HTTP response code: {}, statusCode={}", ssoUser, statusCode); throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); } // CAS expects OSF API to return HTTP 403 FORBIDDEN with error details if authentication fails. @@ -799,45 +736,35 @@ 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, - ssoEmail, - selectiveSsoFilter - ); + LOGGER.error("[OSF API] Failure - Institution Selective SSO Not Allowed: {}, filter={}", ssoUser, selectiveSsoFilter); throw new InstitutionSelectiveSsoFailedException("OSF API denies selective SSO login"); } } + // Handle unidentified HTTP 403 FORBIDDEN failures + LOGGER.error("[OSF API] Failure - HTTP 403 FORBIDDEN: {}, statusCode={}", ssoUser, statusCode); + throw new InstitutionSsoFailedException("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 InstitutionSsoFailedException("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={}, ssoEmail={}", - statusCode, - institutionId, - ssoEmail - ); - throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); } /** @@ -845,9 +772,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) { @@ -865,12 +793,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 ""; From 2b37a41706477aae9ed9005388a9d3c7d6f29353 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Thu, 19 Jan 2023 21:52:31 -0500 Subject: [PATCH 05/10] Add new and update existing institution SSO Exceptions --- ...nstitutionSsoAccountInactiveException.java | 30 +++++++ ...stitutionSsoAttributeMissingException.java | 30 +++++++ ...stitutionSsoAttributeParsingException.java | 30 +++++++ ...titutionSsoDuplicateIdentityException.java | 30 +++++++ .../InstitutionSsoFailedException.java | 3 +- ... InstitutionSsoOsfApiFailedException.java} | 10 +-- ...tionSsoSelectiveLoginDeniedException.java} | 9 +- .../OsfCasCoreWebflowConfiguration.java | 18 ++-- .../OsfCasLoginWebflowConfigurer.java | 86 ++++++++++++++----- .../flow/support/OsfCasWebflowConstants.java | 16 +++- 10 files changed, 224 insertions(+), 38 deletions(-) create mode 100644 src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAccountInactiveException.java create mode 100644 src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeMissingException.java create mode 100644 src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoAttributeParsingException.java create mode 100644 src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoDuplicateIdentityException.java rename src/main/java/io/cos/cas/osf/authentication/exception/{InstitutionSsoOsfApiFailureException.java => InstitutionSsoOsfApiFailedException.java} (60%) rename src/main/java/io/cos/cas/osf/authentication/exception/{InstitutionSelectiveSsoFailedException.java => InstitutionSsoSelectiveLoginDeniedException.java} (60%) 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 0000000..494ca06 --- /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 0000000..348c914 --- /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 0000000..253e203 --- /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 0000000..a8dd831 --- /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 3d6a673..52785f1 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 7319165..dbc0031 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 4490b30..fc7934b 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/web/flow/config/OsfCasCoreWebflowConfiguration.java b/src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java index caa3f49..cdfea1e 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 52e31af..8144ca7 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/support/OsfCasWebflowConstants.java b/src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java index 0aa3361..43c4ad0 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"; } From cebe0fae65b8dc60e488eb89c9711a167e254085 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Thu, 19 Jan 2023 22:34:36 -0500 Subject: [PATCH 06/10] Fix existing institutio sso error views --- src/main/resources/messages.properties | 4 +- .../casInstitutionOsfApiFailureView.html | 49 ------------------- ...=> casInstitutionSsoOsfApiFailedView.html} | 2 +- ...nstitutionSsoSelectiveLoginDeniedView.html | 49 +++++++++++++++++++ 4 files changed, 52 insertions(+), 52 deletions(-) delete mode 100644 src/main/resources/templates/casInstitutionOsfApiFailureView.html rename src/main/resources/templates/{casInstitutionSelectiveSsoFailedView.html => casInstitutionSsoOsfApiFailedView.html} (96%) create mode 100644 src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index bf41ada..96cc86d 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -690,10 +690,10 @@ screen.institutionssofailed.message=Your request cannot be completed at this tim 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. \ +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, please contact \ Support for help. -screen.institutionosfapifailure.message=Your request cannot be completed at this time due to an unexpected error. Please \ +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, \ please contact Support for help. # diff --git a/src/main/resources/templates/casInstitutionOsfApiFailureView.html b/src/main/resources/templates/casInstitutionOsfApiFailureView.html deleted file mode 100644 index f78a478..0000000 --- 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/casInstitutionSsoOsfApiFailedView.html similarity index 96% rename from src/main/resources/templates/casInstitutionSelectiveSsoFailedView.html rename to src/main/resources/templates/casInstitutionSsoOsfApiFailedView.html index ad3070c..1a687c3 100644 --- a/src/main/resources/templates/casInstitutionSelectiveSsoFailedView.html +++ b/src/main/resources/templates/casInstitutionSsoOsfApiFailedView.html @@ -25,7 +25,7 @@

-

+

diff --git a/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html b/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html new file mode 100644 index 0000000..36ee35e --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoSelectiveLoginDeniedView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + From efb0e3c3628d5ccf91ba757d8d3d0359becf273f Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Thu, 19 Jan 2023 22:36:22 -0500 Subject: [PATCH 07/10] Refactor institution SSO exceptior/error/failure handling * Attribute exceptions: missing or invalid attributes, parsing or normalizationfailures, etc. * API exceptions: payload encryption/encoding, network/communication, expected and unexpected response, etc. * Special exceptions: selective SSO denied, invalid account status, duplicate identity, etc. * Generic exception: the rest --- .../support/OsfApiPermissionDenied.java | 6 ++- ...alFromNonInteractiveCredentialsAction.java | 47 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) 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 e956e7e..cf69bec 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/login/OsfPrincipalFromNonInteractiveCredentialsAction.java b/src/main/java/io/cos/cas/osf/web/flow/login/OsfPrincipalFromNonInteractiveCredentialsAction.java index 3f23253..6345674 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; @@ -574,23 +577,23 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( normalizedPayload = extractInstnAuthnDataFromCredential(credential); } catch (final ParserConfigurationException | TransformerException e) { LOGGER.error("[CAS XSLT] Exception - Failed to normalize attributes in the credential: {}", e.getMessage()); - throw new InstitutionSsoFailedException("Attribute normalization failure"); + throw new InstitutionSsoAttributeParsingException("Attribute normalization failure"); } // Verify required and optional attributes final JSONObject provider = normalizedPayload.optJSONObject("provider"); if (provider == null) { LOGGER.error("[CAS XSLT] Error - Missing identity provider."); - throw new InstitutionSsoFailedException("Missing identity provider"); + throw new InstitutionSsoAttributeMissingException("Missing identity provider"); } final String institutionId = provider.optString("id").trim(); if (institutionId.isEmpty()) { LOGGER.error("[CAS XSLT] Error - Empty identity provider"); - throw new InstitutionSsoFailedException("Empty identity provider"); + throw new InstitutionSsoAttributeMissingException("Empty identity provider"); } final JSONObject user = provider.optJSONObject("user"); if (user == null) { LOGGER.error("[CAS XSLT] Error - Missing institutional user"); - throw new InstitutionSsoFailedException("Missing institutional user"); + throw new InstitutionSsoAttributeMissingException("Missing institutional user"); } // 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. @@ -609,11 +612,11 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( final String userRoles = user.optString("userRoles").trim(); if (ssoEmail.isEmpty()) { LOGGER.error("[CAS XSLT] Error - Missing SSO Email for user: {}", ssoUser); - throw new InstitutionSsoFailedException("Missing email (username)"); + throw new InstitutionSsoAttributeMissingException("Missing SSO Email)"); } if (fullname.isEmpty() && (givenName.isEmpty() || familyName.isEmpty())) { LOGGER.error("[CAS XSLT] Error - Missing names: {}", ssoUser); - throw new InstitutionSsoFailedException("Missing user's names"); + throw new InstitutionSsoAttributeMissingException("Missing user's names"); } if (!isMemberOf.isEmpty()) { LOGGER.info("[CAS XSLT] Shared SSO \"isMemberOf\" detected: {}, isMemberOf={}", ssoUser, isMemberOf); @@ -678,13 +681,13 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( jweString = jweObject.serialize(); } catch (final JOSEException e) { LOGGER.error("[OSF API] Exception - Failed to construct API Payload: {}, error={}", ssoUser, e.getMessage()); - throw new InstitutionSsoFailedException("OSF CAS failed to build JWT / JWE payload for OSF API"); + 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; HttpResponse httpResponse = null; - InstitutionSsoOsfApiFailureException casError = null; + InstitutionSsoOsfApiFailedException casError = null; while (retry < OSF_API_RETRY_LIMIT) { retry += 1; // Reset exception from previous attempt @@ -706,19 +709,19 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( } if (OSF_API_RETRY_STATUS.contains(statusCode)) { LOGGER.error("[OSF API] Failure - Server Error: {}, attempt={}, status={}", ssoUser, retry, statusCode); - casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); + casError = new InstitutionSsoOsfApiFailedException("Communication Error between OSF CAS and OSF API"); } else { break; } } catch (final IOException e) { LOGGER.error("[OSF API] Exception - IO Exception: {}, attempt={}, error={}", ssoUser, retry, e.getMessage()); - casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); + 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] Exception - Retry Interrupted: {}, attempt={}, error={}", ssoUser, retry, e.getMessage()); - casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); + casError = new InstitutionSsoOsfApiFailedException("Communication Error between OSF CAS and OSF API"); break; } } @@ -728,7 +731,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( // Handler unexpected exceptions (i.e. any status other than 403) if (statusCode != HttpStatus.SC_FORBIDDEN) { LOGGER.error("[OSF API] Failure - Unexpected HTTP response code: {}, statusCode={}", ssoUser, statusCode); - throw new InstitutionSsoFailedException("OSF API failed to process CAS request"); + 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; @@ -753,17 +756,25 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( continue; } final String errorDetail = ((JsonObject) error).get("detail").getAsString(); - if (OsfApiPermissionDenied.INSTITUTION_SELECTIVE_SSO_FAILURE.getId().equals(errorDetail)) { + 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 InstitutionSelectiveSsoFailedException("OSF API denies selective SSO login"); + 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 InstitutionSsoFailedException("OSF API failed to process CAS request"); + throw new InstitutionSsoOsfApiFailedException("OSF API failed to process CAS request"); } catch (final JsonParseException | IllegalStateException e) { LOGGER.error("[OSF API] Exception - Invalid Response: {}, error={}", ssoUser, e.getMessage()); - throw new InstitutionSsoFailedException("CAS failed to parse OSF API error response"); + throw new InstitutionSsoOsfApiFailedException("CAS failed to parse OSF API error response"); } } From ef1e48e8fdb8593bd1517631130306f46ece88ed Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 20 Jan 2023 00:43:50 -0500 Subject: [PATCH 08/10] Add new pages and rework user-facing error messages --- src/main/resources/messages.properties | 41 ++++++++++++---- .../casInstitutionSsoAccountInactiveView.html | 49 +++++++++++++++++++ ...casInstitutionSsoAttributeMissingView.html | 49 +++++++++++++++++++ ...titutionSsoAttributeParsingFailedView.html | 49 +++++++++++++++++++ ...asInstitutionSsoDuplicateIdentityView.html | 49 +++++++++++++++++++ 5 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 src/main/resources/templates/casInstitutionSsoAccountInactiveView.html create mode 100644 src/main/resources/templates/casInstitutionSsoAttributeMissingView.html create mode 100644 src/main/resources/templates/casInstitutionSsoAttributeParsingFailedView.html create mode 100644 src/main/resources/templates/casInstitutionSsoDuplicateIdentityView.html diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 96cc86d..7e43c0a 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.institutionssoselectivelogindenied.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.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, \ - 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/casInstitutionSsoAccountInactiveView.html b/src/main/resources/templates/casInstitutionSsoAccountInactiveView.html new file mode 100644 index 0000000..0f1b48c --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoAccountInactiveView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + diff --git a/src/main/resources/templates/casInstitutionSsoAttributeMissingView.html b/src/main/resources/templates/casInstitutionSsoAttributeMissingView.html new file mode 100644 index 0000000..d23ff05 --- /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 0000000..8146be6 --- /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 0000000..642243b --- /dev/null +++ b/src/main/resources/templates/casInstitutionSsoDuplicateIdentityView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + From 2c7be1b69f6320319de542d8cb7dc4f5f468e5c1 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Fri, 20 Jan 2023 11:17:31 -0500 Subject: [PATCH 09/10] Swap exit login / back to OSF and login again --- .../templates/casInstitutionSsoFailedView.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/templates/casInstitutionSsoFailedView.html b/src/main/resources/templates/casInstitutionSsoFailedView.html index 5dd1cd0..ce441b2 100644 --- a/src/main/resources/templates/casInstitutionSsoFailedView.html +++ b/src/main/resources/templates/casInstitutionSsoFailedView.html @@ -27,14 +27,14 @@

-
- - +
+ +

-
- +
+
From a6e0366de36185c1e48c396a8c601f1a02040fa6 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Wed, 25 Jan 2023 16:38:51 -0500 Subject: [PATCH 10/10] Update change log for osf-cas release 23.1.0 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index afca821..4f7285b 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) ===================