Skip to content

Commit

Permalink
Merge branch 'hotfix/22.1.3'
Browse files Browse the repository at this point in the history
  • Loading branch information
cslzchen committed Dec 20, 2022
2 parents 30d3380 + dda5004 commit 64641f5
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 21 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
===================

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +51,7 @@ public Set<Class<? extends Throwable>> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,6 +93,7 @@
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* This is {@link OsfPrincipalFromNonInteractiveCredentialsAction}.
Expand Down Expand Up @@ -160,6 +162,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<Integer> 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;

Expand Down Expand Up @@ -672,28 +685,75 @@ 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;
InstitutionSsoOsfApiFailureException casError = null;
while (retry < OSF_API_RETRY_LIMIT) {
retry += 1;
// Reset exception from previous attempt
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()
.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 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()
);
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()
);
casError = new InstitutionSsoOsfApiFailureException("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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
3 changes: 3 additions & 0 deletions src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
<a style="white-space: nowrap" href="mailto:[email protected]">Support</a> for help.
screen.institutionosfapifailure.message=Your request cannot be completed at this time due to an unexpected error. Please \
<a style="white-space: nowrap" href="{0}">return to OSF</a> and try again later.</br></br>If the issue persists, \
please contact <a style="white-space: nowrap" href="mailto:[email protected]">Support</a> for help.
#
# OAuth 2.0 Views and Error Views
#
Expand Down
49 changes: 49 additions & 0 deletions src/main/resources/templates/casInstitutionOsfApiFailureView.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layoutosf}">

<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />

<title th:text="#{screen.institutionssofailed.title}"></title>
<link href="../../static/css/cas.css" rel="stylesheet" th:remove="tag" />
</head>

<body class="mdc-typography">
<div layout:fragment="content" class="d-flex justify-content-center">

<div class="d-flex justify-content-center flex-md-row flex-column mdc-card mdc-card-content w-lg-30">
<section class="login-error-card">
<section>
<div th:replace="fragments/osfbannerui :: osfBannerUI">
<a href="fragments/osfbannerui.html"></a>
</div>
</section>
<section class="text-without-mdi text-center text-bold text-large margin-large-vertical title-danger">
<span th:utext="#{screen.authnerror.tips}"></span>
</section>
<hr class="my-4" />
<section class="card-message">
<h1 th:utext="#{screen.institutionssofailed.heading}"></h1>
<p th:utext="#{screen.institutionosfapifailure.message}"></p>
</section>
<section class="form-button">
<a class="mdc-button mdc-button--raised button-osf-blue" th:href="@{/logout(service=${osfUrl.logout})}">
<span class="mdc-button__label" th:utext="#{screen.authnerror.button.backtoosf}"></span>
</a>
</section>
<hr class="my-4" />
<section class="text-with-mdi" th:with="loginUrl=@{${@casServerLoginUrl}(casRedirectSource=cas)}">
<span><a th:href="@{/logout(service=${loginUrl})}" th:utext="#{screen.error.page.loginagain}"></a></span>
</section>
</section>
</div>

<script type="text/javascript">
disableSignUpButton();
</script>

</div>
</body>

</html>

0 comments on commit 64641f5

Please sign in to comment.