From 0054ff018a5a9147ee68445c84f7163984f9ae61 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 20 Dec 2022 04:38:15 -0500 Subject: [PATCH 1/4] Add manual retries for OSF API requests during institution SSO --- ...alFromNonInteractiveCredentialsAction.java | 99 +++++++++++++++---- 1 file changed, 78 insertions(+), 21 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 3e7fcc8..a5d397f 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 @@ -92,6 +92,7 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * This is {@link OsfPrincipalFromNonInteractiveCredentialsAction}. @@ -160,6 +161,17 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon private static final String LDAP_DN_OU_PREFIX = "ou="; + private static final int OSF_API_RETRY_LIMIT = 3; + + private static final List OSF_API_RETRY_STATUS = List.of( + HttpStatus.SC_INTERNAL_SERVER_ERROR, + HttpStatus.SC_BAD_GATEWAY, + HttpStatus.SC_SERVICE_UNAVAILABLE, + HttpStatus.SC_GATEWAY_TIMEOUT + ); + + private static final int OSF_API_RETRY_DELAY_IN_SECONDS = 1; + @NotNull private CentralAuthenticationService centralAuthenticationService; @@ -672,28 +684,73 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( 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; - HttpResponse httpResponse; - try { - httpResponse = Request.Post(osfApiProperties.getInstnAuthnEndpoint()) - .addHeader(new BasicHeader("Content-Type", "text/plain")) - .bodyString(jweString, ContentType.APPLICATION_JSON) - .execute() - .returnResponse(); - statusCode = httpResponse.getStatusLine().getStatusCode(); - LOGGER.info( - "[OSF API] Notify Remote Principal Authenticated Response: username={}, statusCode={}", - username, - statusCode - ); - } catch (final IOException e) { - LOGGER.error("[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", e.getMessage()); - throw new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + int statusCode = -1; + int retry = 0; + final String ssoUser = String.format("institution=%s, username=%s", institutionId, username); + HttpResponse httpResponse = null; + InstitutionSsoFailedException casError = null; + while (retry < OSF_API_RETRY_LIMIT) { + retry += 1; + // Reset exception from previous attempt + casError = null; + try { + httpResponse = Request.Post(osfApiProperties.getInstnAuthnEndpoint()) + .addHeader(new BasicHeader("Content-Type", "text/plain")) + .bodyString(jweString, ContentType.APPLICATION_JSON) + .execute() + .returnResponse(); + statusCode = httpResponse.getStatusLine().getStatusCode(); + LOGGER.info( + "[OSF API] Notify Remote Principal Authenticated 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); + } + if (OSF_API_RETRY_STATUS.contains(statusCode)) { + LOGGER.error( + "[OSF API] Notify Remote Principal Authenticated Failed - Server Error: {}, attempt={}, status={}", + ssoUser, + retry, + statusCode + ); + casError = new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + } else { + break; + } + } catch (final IOException e) { + LOGGER.error( + "[OSF API] Notify Remote Principal Authenticated Failed - Communication Error: {}, attempt={}, error={}", + ssoUser, + retry, + e.getMessage() + ); + casError = new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + } + try { + TimeUnit.SECONDS.sleep(OSF_API_RETRY_DELAY_IN_SECONDS * retry); + } catch (InterruptedException e) { + LOGGER.error( + "[OSF API] Notify Remote Principal Authenticated Failed - Retry Interrupted: {}, attempt={}, error={}", + ssoUser, + retry, + e.getMessage() + ); + casError = new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + break; + } } - // 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: institution={}, username={}", institutionId, username); - return new OsfApiInstitutionAuthenticationResult(username, institutionId); + if (casError != null) { + throw casError; } // Handler unexpected exceptions (i.e. any status other than 403) if (statusCode != HttpStatus.SC_FORBIDDEN) { From 150033d4e946c74795463bdafe1730fc3d80145e Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 20 Dec 2022 04:45:20 -0500 Subject: [PATCH 2/4] Create a new exception flow for API connection/communication errors --- .../InstitutionSsoOsfApiFailureException.java | 30 ++++++++++++ .../OsfCasCoreWebflowConfiguration.java | 2 + .../OsfCasLoginWebflowConfigurer.java | 11 +++++ ...alFromNonInteractiveCredentialsAction.java | 9 ++-- .../flow/support/OsfCasWebflowConstants.java | 2 + src/main/resources/messages.properties | 3 ++ .../casInstitutionOsfApiFailureView.html | 49 +++++++++++++++++++ 7 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.java create mode 100644 src/main/resources/templates/casInstitutionOsfApiFailureView.html diff --git a/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.java b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.java new file mode 100644 index 0000000..7319165 --- /dev/null +++ b/src/main/java/io/cos/cas/osf/authentication/exception/InstitutionSsoOsfApiFailureException.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 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 { + + /** + * Serialization metadata. + */ + private static final long serialVersionUID = -620313210360224932L; + + /** + * Instantiates a new {@link InstitutionSsoOsfApiFailureException}. + * + * @param msg the msg + */ + public InstitutionSsoOsfApiFailureException(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 1ed3c92..caa3f49 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 @@ -3,6 +3,7 @@ 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.InstitutionSsoFailedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidPasswordException; @@ -50,6 +51,7 @@ public Set> handledAuthenticationExceptions() { 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 c0cf224..52e31af 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 @@ -4,6 +4,7 @@ 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.InstitutionSsoFailedException; import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException; import io.cos.cas.osf.authentication.exception.InvalidUserStatusException; @@ -265,6 +266,11 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) { 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); @@ -415,6 +421,11 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) { OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED, OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED ); + createViewState( + flow, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE, + OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE + ); } /** 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 a5d397f..8d17dc1 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 @@ -9,6 +9,7 @@ import io.cos.cas.osf.authentication.credential.OsfPostgresCredential; import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException; import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException; +import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException; import io.cos.cas.osf.authentication.support.DelegationProtocol; import io.cos.cas.osf.authentication.support.OsfApiPermissionDenied; import io.cos.cas.osf.configuration.model.OsfApiProperties; @@ -688,7 +689,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( int retry = 0; final String ssoUser = String.format("institution=%s, username=%s", institutionId, username); HttpResponse httpResponse = null; - InstitutionSsoFailedException casError = null; + InstitutionSsoOsfApiFailureException casError = null; while (retry < OSF_API_RETRY_LIMIT) { retry += 1; // Reset exception from previous attempt @@ -723,7 +724,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( retry, statusCode ); - casError = new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); } else { break; } @@ -734,7 +735,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( retry, e.getMessage() ); - casError = new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); } try { TimeUnit.SECONDS.sleep(OSF_API_RETRY_DELAY_IN_SECONDS * retry); @@ -745,7 +746,7 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( retry, e.getMessage() ); - casError = new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API"); + casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API"); break; } } 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 cccfde2..0aa3361 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 @@ -64,5 +64,7 @@ public interface OsfCasWebflowConstants { String VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED = "casInstitutionSelectiveSsoFailedView"; + String VIEW_ID_INSTITUTION_OSF_API_FAILURE = "casInstitutionOsfApiFailureView"; + String VIEW_ID_OAUTH_20_ERROR_VIEW = "casOAuth20ErrorView"; } diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 8690c2c..bf41ada 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -693,6 +693,9 @@ screen.institutionssofailed.message=Your request cannot be completed at this tim screen.institutionselectivessofailed.message=Your institutional account is unable to authenticate to OSF. \ Please check with your institution. If your institution believes this is in error, please contact \ Support for help. +screen.institutionosfapifailure.message=Your request cannot be completed at this time due to an unexpected error. Please \ + return to OSF and try again later.

If the issue persists, \ + please contact Support for help. # # OAuth 2.0 Views and Error Views # diff --git a/src/main/resources/templates/casInstitutionOsfApiFailureView.html b/src/main/resources/templates/casInstitutionOsfApiFailureView.html new file mode 100644 index 0000000..f78a478 --- /dev/null +++ b/src/main/resources/templates/casInstitutionOsfApiFailureView.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + From 9e14ae6bd3c59a3f70f0e655349a7f916b39b690 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 20 Dec 2022 05:23:03 -0500 Subject: [PATCH 3/4] Add connection and socket timeout for OSF API requests --- .../login/OsfPrincipalFromNonInteractiveCredentialsAction.java | 2 ++ 1 file changed, 2 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 8d17dc1..8a8e8ff 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 @@ -696,6 +696,8 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess( casError = null; try { httpResponse = Request.Post(osfApiProperties.getInstnAuthnEndpoint()) + .connectTimeout(SIXTY_SECONDS) + .socketTimeout(SIXTY_SECONDS) .addHeader(new BasicHeader("Content-Type", "text/plain")) .bodyString(jweString, ContentType.APPLICATION_JSON) .execute() From dda5004a77427d629cad3be9d3069286133f8bf2 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Tue, 20 Dec 2022 14:21:51 -0500 Subject: [PATCH 4/4] Update change log for cas hotfix 22.1.3 --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89185c..afca821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO. +22.1.3 (12-20-2022) +=================== + +* Add retries for OSF API requests during institution SSO + 22.1.2 (11-21-2022) ===================