Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.cos.cas.osf.authentication.exception;

import lombok.NoArgsConstructor;

import javax.security.auth.login.AccountException;

/**
* Describes an authentication error condition where ORCiD SSO has failed due to missing id or token(s).
*
* @author Longze Chen
* @since 26.2.0
*/
@NoArgsConstructor
public class OrcidSsoFailedException extends AccountException {

/** Serial version UID. */
private static final long serialVersionUID = 7499754046067009352L;

/**
* Instantiates a new {@link OrcidSsoFailedException}.
*
* @param msg the msg
*/
public OrcidSsoFailedException(final String msg) {
super(msg);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.cos.cas.osf.authentication.handler.support;

import io.cos.cas.osf.authentication.credential.OsfOrcidSsoCredential;
import io.cos.cas.osf.authentication.exception.OrcidSsoFailedException;

import lombok.extern.slf4j.Slf4j;
import lombok.Getter;
Expand Down Expand Up @@ -71,8 +72,8 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfOrcidSsoInte
) throws GeneralSecurityException {

if (credential == null) {
LOGGER.error("[ORCiD SSO] Null/Empty ORCiD Credential.");
throw new GeneralSecurityException("Null/Empty ORCiD Credential.");
LOGGER.error("[ORCiD SSO] ERROR: Null/Empty ORCiD Credential.");
throw new OrcidSsoFailedException("Null/Empty ORCiD Credential.");
}

final String credentialId = credential.getId();
Expand All @@ -81,11 +82,12 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfOrcidSsoInte
final String orcidRefreshToken = credential.getOrcidRefreshToken();

if (StringUtils.isBlank(orcidId)) {
LOGGER.error("[ORCiD SSO] Null/Empty ORCiD ID.");
throw new GeneralSecurityException("Null/Empty ORCiD ID.");
LOGGER.error("[ORCiD SSO] ERROR: Null/Empty ORCiD ID.");
throw new OrcidSsoFailedException("Null/Empty ORCiD ID.");
} else if (StringUtils.isBlank(orcidAccessToken)) {
LOGGER.error("[ORCiD SSO] Null/Empty ORCiD Access Token, orcidId=[{}]", orcidId);
throw new GeneralSecurityException("Null/Empty ORCiD Access Token.");
// TODO: should we only log error but let authentication pass?
LOGGER.error("[ORCiD SSO] ERROR: Null/Empty ORCiD Access Token, orcidId=[{}]", orcidId);
throw new OrcidSsoFailedException("Null/Empty ORCiD Access Token.");
}

LOGGER.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import io.cos.cas.osf.authentication.exception.InvalidUserStatusException;
import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException;
import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException;
import io.cos.cas.osf.authentication.exception.OrcidSsoFailedException;
import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException;

import org.apereo.cas.configuration.CasConfigurationProperties;
Expand Down Expand Up @@ -62,6 +63,7 @@ public Set<Class<? extends Throwable>> handledAuthenticationExceptions() {
errors.add(InvalidUserStatusException.class);
errors.add(InvalidVerificationKeyException.class);
errors.add(OneTimePasswordRequiredException.class);
errors.add(OrcidSsoFailedException.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 @@ -15,6 +15,7 @@
import io.cos.cas.osf.authentication.exception.InvalidUserStatusException;
import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException;
import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException;
import io.cos.cas.osf.authentication.exception.OrcidSsoFailedException;
import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException;
import io.cos.cas.osf.web.flow.support.OsfCasWebflowConstants;

Expand Down Expand Up @@ -301,6 +302,11 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) {
TermsOfServiceConsentRequiredException.class.getSimpleName(),
OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED
);
createTransitionForState(
handler,
OrcidSsoFailedException.class.getSimpleName(),
OsfCasWebflowConstants.VIEW_ID_ORCID_SSO_FAILED
);

// The default transition
createStateDefaultTransition(handler, CasWebflowConstants.STATE_ID_INIT_LOGIN_FORM);
Expand Down Expand Up @@ -481,6 +487,11 @@ private void createOsfCasAuthenticationExceptionViewStates(final Flow flow) {
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED,
OsfCasWebflowConstants.VIEW_ID_INSTITUTION_SSO_MULTIPLE_EMAILS_NOT_SUPPORTED
);
createViewState(
flow,
OsfCasWebflowConstants.VIEW_ID_ORCID_SSO_FAILED,
OsfCasWebflowConstants.VIEW_ID_ORCID_SSO_FAILED
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public interface OsfCasWebflowConstants {

String VIEW_ID_INVALID_VERIFICATION_KEY = "casInvalidVerificationKeyView";

// Exception Views for ORCiD SSO

String VIEW_ID_ORCID_SSO_FAILED = "casOrcidSsoFailedView";

// Exception Views for Institution SSO

String VIEW_ID_INSTITUTION_SSO_ACCOUNT_INACTIVE = "casInstitutionSsoAccountInactiveView";
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,12 @@ screen.onetimepasswordrequired.heading=OSF Two-factor Authentication
screen.onetimepasswordrequired.message=Two-factor authentication has been enabled for this OSF account. Please enter \
the one-time password generated by the authentication app. If you believe this should not happen, please contact <a \
style="white-space: nowrap" href="mailto:support@osf.io">OSF Support</a>.
screen.orcidssofailed.title=ORCiD SSO Error
screen.orcidssofailed.heading=ORCiD login failed
screen.orcidssofailed.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. \
If the issue persists, contact <a style="white-space: nowrap" href="mailto:support@osf.io">Support</a> for help.
screen.institutionssofailed.title=Institution SSO Error
screen.institutionssofailed.heading=Institution login failed
screen.institutionssofailed.message=\
Expand Down
40 changes: 40 additions & 0 deletions src/main/resources/templates/casOrcidSsoFailedView.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!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.orcidssofailed.title}"></title>
<link href="../../static/css/cas-redesigned.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 class="card-message">
<h1 th:utext="#{screen.orcidssofailed.heading}"></h1>
<p th:utext="#{screen.orcidssofailed.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>
Loading