Skip to content

Commit a457201

Browse files
committed
Merge tag '22.1.3' into develop
Add manual retries for OSF API requests during institution SSO
2 parents ef787f9 + 64641f5 commit a457201

8 files changed

Lines changed: 183 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.
44

5+
22.1.3 (12-20-2022)
6+
===================
7+
8+
* Add retries for OSF API requests during institution SSO
9+
510
22.1.2 (11-21-2022)
611
===================
712

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.cos.cas.osf.authentication.exception;
2+
3+
import lombok.NoArgsConstructor;
4+
5+
import javax.security.auth.login.AccountException;
6+
7+
/**
8+
* Describes an authentication error condition when connection failures and/or server errors happen between
9+
* CAS and OSF API during institution SSO.
10+
*
11+
* @author Longze Chen
12+
* @since 22.1.3
13+
*/
14+
@NoArgsConstructor
15+
public class InstitutionSsoOsfApiFailureException extends AccountException {
16+
17+
/**
18+
* Serialization metadata.
19+
*/
20+
private static final long serialVersionUID = -620313210360224932L;
21+
22+
/**
23+
* Instantiates a new {@link InstitutionSsoOsfApiFailureException}.
24+
*
25+
* @param msg the msg
26+
*/
27+
public InstitutionSsoOsfApiFailureException(final String msg) {
28+
super(msg);
29+
}
30+
}

src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException;
44
import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException;
55
import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException;
6+
import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException;
67
import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException;
78
import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException;
89
import io.cos.cas.osf.authentication.exception.InvalidPasswordException;
@@ -50,6 +51,7 @@ public Set<Class<? extends Throwable>> handledAuthenticationExceptions() {
5051
errors.add(InvalidVerificationKeyException.class);
5152
errors.add(OneTimePasswordRequiredException.class);
5253
errors.add(InstitutionSelectiveSsoFailedException.class);
54+
errors.add(InstitutionSsoOsfApiFailureException.class);
5355
errors.add(TermsOfServiceConsentRequiredException.class);
5456

5557
// Add built-in exceptions after OSF-specific exceptions since order matters

src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException;
55
import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException;
66
import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException;
7+
import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException;
78
import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException;
89
import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException;
910
import io.cos.cas.osf.authentication.exception.InvalidUserStatusException;
@@ -265,6 +266,11 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) {
265266
InstitutionSelectiveSsoFailedException.class.getSimpleName(),
266267
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED
267268
);
269+
createTransitionForState(
270+
handler,
271+
InstitutionSsoOsfApiFailureException.class.getSimpleName(),
272+
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE
273+
);
268274

269275
// The default transition
270276
createStateDefaultTransition(handler, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM);
@@ -415,6 +421,11 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) {
415421
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED,
416422
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED
417423
);
424+
createViewState(
425+
flow,
426+
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE,
427+
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_OSF_API_FAILURE
428+
);
418429
}
419430

420431
/**

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

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.cos.cas.osf.authentication.credential.OsfPostgresCredential;
1010
import io.cos.cas.osf.authentication.exception.InstitutionSelectiveSsoFailedException;
1111
import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException;
12+
import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailureException;
1213
import io.cos.cas.osf.authentication.support.DelegationProtocol;
1314
import io.cos.cas.osf.authentication.support.OsfApiPermissionDenied;
1415
import io.cos.cas.osf.configuration.model.OsfApiProperties;
@@ -92,6 +93,7 @@
9293
import java.util.Date;
9394
import java.util.List;
9495
import java.util.Map;
96+
import java.util.concurrent.TimeUnit;
9597

9698
/**
9799
* This is {@link OsfPrincipalFromNonInteractiveCredentialsAction}.
@@ -160,6 +162,17 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon
160162

161163
private static final String LDAP_DN_OU_PREFIX = "ou=";
162164

165+
private static final int OSF_API_RETRY_LIMIT = 3;
166+
167+
private static final List<Integer> OSF_API_RETRY_STATUS = List.of(
168+
HttpStatus.SC_INTERNAL_SERVER_ERROR,
169+
HttpStatus.SC_BAD_GATEWAY,
170+
HttpStatus.SC_SERVICE_UNAVAILABLE,
171+
HttpStatus.SC_GATEWAY_TIMEOUT
172+
);
173+
174+
private static final int OSF_API_RETRY_DELAY_IN_SECONDS = 1;
175+
163176
@NotNull
164177
private CentralAuthenticationService centralAuthenticationService;
165178

@@ -672,28 +685,75 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess(
672685
throw new InstitutionSsoFailedException("OSF CAS failed to build JWT / JWE payload for OSF API");
673686
}
674687
// Send the POST request to OSF API to verify an existing institution user or to create a new one
675-
int statusCode;
676-
HttpResponse httpResponse;
677-
try {
678-
httpResponse = Request.Post(osfApiProperties.getInstnAuthnEndpoint())
679-
.addHeader(new BasicHeader("Content-Type", "text/plain"))
680-
.bodyString(jweString, ContentType.APPLICATION_JSON)
681-
.execute()
682-
.returnResponse();
683-
statusCode = httpResponse.getStatusLine().getStatusCode();
684-
LOGGER.info(
685-
"[OSF API] Notify Remote Principal Authenticated Response: username={}, statusCode={}",
686-
username,
687-
statusCode
688-
);
689-
} catch (final IOException e) {
690-
LOGGER.error("[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", e.getMessage());
691-
throw new InstitutionSsoFailedException("Communication Error between OSF CAS and OSF API");
688+
int statusCode = -1;
689+
int retry = 0;
690+
final String ssoUser = String.format("institution=%s, username=%s", institutionId, username);
691+
HttpResponse httpResponse = null;
692+
InstitutionSsoOsfApiFailureException casError = null;
693+
while (retry < OSF_API_RETRY_LIMIT) {
694+
retry += 1;
695+
// Reset exception from previous attempt
696+
casError = null;
697+
try {
698+
httpResponse = Request.Post(osfApiProperties.getInstnAuthnEndpoint())
699+
.connectTimeout(SIXTY_SECONDS)
700+
.socketTimeout(SIXTY_SECONDS)
701+
.addHeader(new BasicHeader("Content-Type", "text/plain"))
702+
.bodyString(jweString, ContentType.APPLICATION_JSON)
703+
.execute()
704+
.returnResponse();
705+
statusCode = httpResponse.getStatusLine().getStatusCode();
706+
LOGGER.info(
707+
"[OSF API] Notify Remote Principal Authenticated Response Received: {}, attempt={}, status={}",
708+
ssoUser,
709+
retry,
710+
statusCode
711+
);
712+
// CAS expects OSF API to return HTTP 204 OK with no content if authentication succeeds
713+
if (statusCode == HttpStatus.SC_NO_CONTENT) {
714+
LOGGER.info(
715+
"[OSF API] Notify Remote Principal Authenticated Passed: {}, attempt={}, status={}",
716+
ssoUser,
717+
retry,
718+
statusCode
719+
);
720+
return new OsfApiInstitutionAuthenticationResult(username, institutionId);
721+
}
722+
if (OSF_API_RETRY_STATUS.contains(statusCode)) {
723+
LOGGER.error(
724+
"[OSF API] Notify Remote Principal Authenticated Failed - Server Error: {}, attempt={}, status={}",
725+
ssoUser,
726+
retry,
727+
statusCode
728+
);
729+
casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API");
730+
} else {
731+
break;
732+
}
733+
} catch (final IOException e) {
734+
LOGGER.error(
735+
"[OSF API] Notify Remote Principal Authenticated Failed - Communication Error: {}, attempt={}, error={}",
736+
ssoUser,
737+
retry,
738+
e.getMessage()
739+
);
740+
casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API");
741+
}
742+
try {
743+
TimeUnit.SECONDS.sleep(OSF_API_RETRY_DELAY_IN_SECONDS * retry);
744+
} catch (InterruptedException e) {
745+
LOGGER.error(
746+
"[OSF API] Notify Remote Principal Authenticated Failed - Retry Interrupted: {}, attempt={}, error={}",
747+
ssoUser,
748+
retry,
749+
e.getMessage()
750+
);
751+
casError = new InstitutionSsoOsfApiFailureException("Communication Error between OSF CAS and OSF API");
752+
break;
753+
}
692754
}
693-
// CAS expects OSF API to return HTTP 204 OK with no content if authentication succeeds
694-
if (statusCode == HttpStatus.SC_NO_CONTENT) {
695-
LOGGER.info("[OSF API] Notify Remote Principal Authenticated Passed: institution={}, username={}", institutionId, username);
696-
return new OsfApiInstitutionAuthenticationResult(username, institutionId);
755+
if (casError != null) {
756+
throw casError;
697757
}
698758
// Handler unexpected exceptions (i.e. any status other than 403)
699759
if (statusCode != HttpStatus.SC_FORBIDDEN) {

src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,7 @@ public interface OsfCasWebflowConstants {
6464

6565
String VIEW_ID_INSTITUTION_SELECTIVE_SSO_FAILED = "casInstitutionSelectiveSsoFailedView";
6666

67+
String VIEW_ID_INSTITUTION_OSF_API_FAILURE = "casInstitutionOsfApiFailureView";
68+
6769
String VIEW_ID_OAUTH_20_ERROR_VIEW = "casOAuth20ErrorView";
6870
}

src/main/resources/messages.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,9 @@ screen.institutionssofailed.message=Your request cannot be completed at this tim
693693
screen.institutionselectivessofailed.message=Your institutional account is unable to authenticate to OSF. \
694694
Please check with your institution. If your institution believes this is in error, please contact \
695695
<a style="white-space: nowrap" href="mailto:support@osf.io">Support</a> for help.
696+
screen.institutionosfapifailure.message=Your request cannot be completed at this time due to an unexpected error. Please \
697+
<a style="white-space: nowrap" href="{0}">return to OSF</a> and try again later.</br></br>If the issue persists, \
698+
please contact <a style="white-space: nowrap" href="mailto:support@osf.io">Support</a> for help.
696699
#
697700
# OAuth 2.0 Views and Error Views
698701
#
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<!DOCTYPE html>
2+
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layoutosf}">
3+
4+
<head>
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
7+
8+
<title th:text="#{screen.institutionssofailed.title}"></title>
9+
<link href="../../static/css/cas.css" rel="stylesheet" th:remove="tag" />
10+
</head>
11+
12+
<body class="mdc-typography">
13+
<div layout:fragment="content" class="d-flex justify-content-center">
14+
15+
<div class="d-flex justify-content-center flex-md-row flex-column mdc-card mdc-card-content w-lg-30">
16+
<section class="login-error-card">
17+
<section>
18+
<div th:replace="fragments/osfbannerui :: osfBannerUI">
19+
<a href="fragments/osfbannerui.html"></a>
20+
</div>
21+
</section>
22+
<section class="text-without-mdi text-center text-bold text-large margin-large-vertical title-danger">
23+
<span th:utext="#{screen.authnerror.tips}"></span>
24+
</section>
25+
<hr class="my-4" />
26+
<section class="card-message">
27+
<h1 th:utext="#{screen.institutionssofailed.heading}"></h1>
28+
<p th:utext="#{screen.institutionosfapifailure.message}"></p>
29+
</section>
30+
<section class="form-button">
31+
<a class="mdc-button mdc-button--raised button-osf-blue" th:href="@{/logout(service=${osfUrl.logout})}">
32+
<span class="mdc-button__label" th:utext="#{screen.authnerror.button.backtoosf}"></span>
33+
</a>
34+
</section>
35+
<hr class="my-4" />
36+
<section class="text-with-mdi" th:with="loginUrl=@{${@casServerLoginUrl}(casRedirectSource=cas)}">
37+
<span><a th:href="@{/logout(service=${loginUrl})}" th:utext="#{screen.error.page.loginagain}"></a></span>
38+
</section>
39+
</section>
40+
</div>
41+
42+
<script type="text/javascript">
43+
disableSignUpButton();
44+
</script>
45+
46+
</div>
47+
</body>
48+
49+
</html>

0 commit comments

Comments
 (0)