diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/InstitutionLoginFailedOsfApiLoAException.java b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/InstitutionLoginFailedOsfApiLoAException.java new file mode 100644 index 00000000..f5be5dc5 --- /dev/null +++ b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/InstitutionLoginFailedOsfApiLoAException.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020. Center for Open Science + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cos.cas.authentication.exceptions; + +/** + * Describes an error condition where institution login fails when communicating with OSF API. + * + * @author Longze Chen + * @since 20.1.0 + */ +public class InstitutionLoginFailedOsfApiLoAException extends InstitutionLoginFailedException { + + private static final long serialVersionUID = 1737367176204402913L; + + /** Instantiates a new exception (default). */ + public InstitutionLoginFailedOsfApiLoAException() { + super(); + } + + /** + * Instantiates a new exception with a given message. + * + * @param message the message + */ + public InstitutionLoginFailedOsfApiLoAException(final String message) { + super(message); + } +} diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java index c47bf3fb..94d0beed 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java @@ -1,6 +1,6 @@ /* * Copyright (c) 2015. Center for Open Science - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -35,6 +35,7 @@ import io.cos.cas.authentication.exceptions.InstitutionLoginFailedAttributesMissingException; import io.cos.cas.authentication.exceptions.InstitutionLoginFailedAttributesParsingException; import io.cos.cas.authentication.exceptions.InstitutionLoginFailedOsfApiException; +import io.cos.cas.authentication.exceptions.InstitutionLoginFailedOsfApiLoAException; // @R2022-48 loa import io.cos.cas.authentication.OpenScienceFrameworkCredential; import org.apache.http.client.fluent.Request; @@ -58,6 +59,7 @@ import org.jasig.cas.ticket.TicketGrantingTicket; import org.jasig.cas.web.support.WebUtils; import org.json.JSONObject; +import org.json.JSONException; import org.json.XML; import org.pac4j.oauth.client.OrcidClient; @@ -140,16 +142,19 @@ public static class PrincipalAuthenticationResult { private String username; private String institutionId; + private String context; /** * Creates a new instance with the given parameters. * * @param username The username * @param institutionId The institution id + * @param context The context */ - public PrincipalAuthenticationResult(final String username, final String institutionId) { + public PrincipalAuthenticationResult(final String username, final String institutionId, final String context) { this.username = username; this.institutionId = institutionId; + this.context = context; } public String getUsername() { @@ -159,6 +164,10 @@ public String getUsername() { public String getInstitutionId() { return institutionId; } + + public String getContext() { + return context; + } } private static final String CONST_CREDENTIAL = "credential"; @@ -322,6 +331,7 @@ protected OpenScienceFrameworkCredential constructCredential( ) throws AccountException, FailedLoginException { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); + final HttpServletResponse response = WebUtils.getHttpServletResponse(context); // WARN: Do not use `WebUtils.getCredential(RequestContext context)`, it will make the credential `null`. // TODO: Check both `FlowScope` and `RequestScope`. Write a `.getCredential(RequestContext context)` which @@ -385,9 +395,38 @@ protected OpenScienceFrameworkCredential constructCredential( } } + logger.info("[SAML Shibboleth] credential : '{}'", credential); + // Parse the attributes and notify OSF API of the remote principal authentication final PrincipalAuthenticationResult remoteUserInfo = notifyRemotePrincipalAuthenticated(credential); - + final String remoteUserContext = remoteUserInfo.getContext(); + final JSONObject json; + logger.info("[SAML Shibboleth] context : '{}'", remoteUserContext); + if (StringUtils.hasText(remoteUserContext)) { + try { + json = new JSONObject(remoteUserContext); + } catch (final JSONException e) { + logger.error( + "[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", + e.getMessage() + ); + throw new InstitutionLoginFailedOsfApiException("Communication Error between OSF CAS and OSF API"); + } + final String mfaUrl = json.optString("mfa_url"); + if (StringUtils.hasText(mfaUrl)) { + try { + logger.info("[OSF API] Redirect MFA URL: '{}'", mfaUrl); + response.sendRedirect(mfaUrl); + return null; + } catch (final IOException e) { + logger.error( + "[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", + e.getMessage() + ); + throw new InstitutionLoginFailedOsfApiException("Communication Error between OSF CAS and OSF API"); + } + } + } // Build and return the OSF-specific credential credential.setUsername(remoteUserInfo.getUsername()); credential.setInstitutionId(remoteUserInfo.getInstitutionId()); @@ -505,7 +544,34 @@ protected OpenScienceFrameworkCredential constructCredential( // Parse the attributes and notify OSF API of the remote principal authentication final PrincipalAuthenticationResult remoteUserInfo = notifyRemotePrincipalAuthenticated(credential); - + final String remoteUserContext = remoteUserInfo.getContext(); + final JSONObject json; + logger.info("[CAS PAC4J] context : '{}'", remoteUserContext); + if (StringUtils.hasText(remoteUserContext)) { + try { + json = new JSONObject(remoteUserContext); + } catch (final JSONException e) { + logger.error( + "[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", + e.getMessage() + ); + throw new InstitutionLoginFailedOsfApiException("Communication Error between OSF CAS and OSF API"); + } + final String mfaUrl = json.optString("mfa_url"); + if (StringUtils.hasText(mfaUrl)) { + try { + logger.info("[OSF API] Redirect MFA URL: '{}'", mfaUrl); + response.sendRedirect(mfaUrl); + return null; + } catch (final IOException e) { + logger.error( + "[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", + e.getMessage() + ); + throw new InstitutionLoginFailedOsfApiException("Communication Error between OSF CAS and OSF API"); + } + } + } credential.setUsername(remoteUserInfo.getUsername()); credential.setInstitutionId(remoteUserInfo.getInstitutionId()); @@ -567,6 +633,8 @@ protected PrincipalAuthenticationResult notifyRemotePrincipalAuthenticated( logger.error("[CAS XSLT] Missing institutional user"); throw new InstitutionLoginFailedAttributesMissingException("Missing institutional user"); } + final String givenNameTmp = user.optString("givenName"); + logger.info("[CAS XSLT] All attributes checked: givenNameTmp={}", givenNameTmp); final String username = user.optString("username").trim(); final String fullname = user.optString("fullname").trim(); final String givenName = user.optString("givenName").trim(); @@ -579,9 +647,10 @@ protected PrincipalAuthenticationResult notifyRemotePrincipalAuthenticated( logger.error("[CAS XSLT] Missing names: username={}, institution={}", username, institutionId); throw new InstitutionLoginFailedAttributesMissingException("Missing user's names"); } - + logger.info("[CAS XSLT] All attributes checked: givenName={}", givenName); // Call Login Availability API final String entitlement = user.optString("entitlement").trim(); + logger.info("[CAS XSLT] All attributes checked: entitlement={}", entitlement); if (!StringUtils.isEmpty(entitlement)) { // send post method to RDM API final JSONObject bodyObj = new JSONObject(); @@ -589,7 +658,11 @@ protected PrincipalAuthenticationResult notifyRemotePrincipalAuthenticated( bodyObj.put("institution_id", institutionId); bodyObj.put("entitlements", getEntitlements(normalizeEntitlement)); user.put("entitlement", normalizeEntitlement); // normalize entitlement in payload - + logger.info( + "[CAS XSLT] All attributes checked: institution_id={}, normalizeEntitlement={}", + institutionId, + normalizeEntitlement + ); HttpResponse httpResponse; try { httpResponse = callLoginAvailabilityAPI(bodyObj); @@ -658,30 +731,36 @@ protected PrincipalAuthenticationResult notifyRemotePrincipalAuthenticated( .execute() .returnResponse(); final int statusCode = httpResponse.getStatusLine().getStatusCode(); + final String context = new BasicResponseHandler().handleResponse(httpResponse); logger.info( - "[OSF API] Notify Remote Principal Authenticated Response: username={} statusCode={}", + "[OSF API] Notify Remote Principal Authenticated Response: username={} statusCode={} context={}", username, - statusCode + statusCode, + context ); // The OSF API institution authentication endpoint always returns the HTTP 204 No Content if successful. - if (statusCode != HttpStatus.SC_NO_CONTENT) { - final String responseString = new BasicResponseHandler().handleResponse(httpResponse); + //if (statusCode != HttpStatus.SC_NO_CONTENT) { + if (statusCode != HttpStatus.SC_OK && statusCode != HttpStatus.SC_NO_CONTENT) { logger.error( - "[OSF API] Notify Remote Principal Authenticated Failed: statusCode={}, body={}", + "[OSF API] Notify Remote Principal Authenticated Failed: statusCode={}, context={}", statusCode, - responseString + context ); throw new InstitutionLoginFailedOsfApiException("OSF API failed to process CAS request"); } - // Return user's username and the institution ID to build the OSF credential - return new PrincipalAuthenticationResult(username, institutionId); + return new PrincipalAuthenticationResult(username, institutionId, context); } catch (final IOException e) { + final String errmsg = e.getMessage(); logger.error( "[OSF API] Notify Remote Principal Authenticated Failed: Communication Error - {}", e.getMessage() ); - throw new InstitutionLoginFailedOsfApiException("Communication Error between OSF CAS and OSF API"); + if ("Bad Request".equals(errmsg)) { + throw new InstitutionLoginFailedOsfApiLoAException("Communication Error between OSF CAS and OSF API"); + } else { + throw new InstitutionLoginFailedOsfApiException("Communication Error between OSF CAS and OSF API"); + } } } diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java b/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java index 73d1e044..5d1981ba 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java @@ -29,6 +29,7 @@ import io.cos.cas.authentication.exceptions.InstitutionLoginFailedAttributesMissingException; import io.cos.cas.authentication.exceptions.InstitutionLoginFailedAttributesParsingException; import io.cos.cas.authentication.exceptions.InstitutionLoginFailedOsfApiException; +import io.cos.cas.authentication.exceptions.InstitutionLoginFailedOsfApiLoAException; // @R2022-48 loa import io.cos.cas.authentication.exceptions.InvalidUserStatusException; import io.cos.cas.authentication.exceptions.InvalidVerificationKeyException; import io.cos.cas.authentication.exceptions.OneTimePasswordFailedLoginException; @@ -90,6 +91,7 @@ public class OpenScienceFrameworkAuthenticationExceptionHandler extends Authenti DEFAULT_ERROR_LIST.add(InstitutionLoginFailedAttributesMissingException.class); DEFAULT_ERROR_LIST.add(InstitutionLoginFailedAttributesParsingException.class); DEFAULT_ERROR_LIST.add(InstitutionLoginFailedOsfApiException.class); + DEFAULT_ERROR_LIST.add(InstitutionLoginFailedOsfApiLoAException.class); // @R2022-48 loa DEFAULT_ERROR_LIST.add(InvalidVerificationKeyException.class); DEFAULT_ERROR_LIST.add(InvalidUserStatusException.class); DEFAULT_ERROR_LIST.add(OneTimePasswordFailedLoginException.class); diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkTerminateSessionAction.java b/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkTerminateSessionAction.java index 40d5fbe5..fdfc6605 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkTerminateSessionAction.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkTerminateSessionAction.java @@ -91,9 +91,11 @@ public Event terminate(final RequestContext context) { String institutionId = null; Boolean remotePrincipal = Boolean.FALSE; + final HttpServletRequest request = WebUtils.getHttpServletRequest(context); + final String serviceUrl = request.getParameter("service"); + logger.info("[serviceUrl] Param: '{}'", serviceUrl); // for logout, we need to get the cookie's value if (tgtId == null) { - final HttpServletRequest request = WebUtils.getHttpServletRequest(context); tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request); } // for institution logout, get the institutionId stored in TGT @@ -122,9 +124,14 @@ public Event terminate(final RequestContext context) { this.ticketGrantingTicketCookieGenerator.removeCookie(response); this.warnCookieGenerator.removeCookie(response); + final String institutionLogoutUrl; // if logged in through institutions, redirect to institution logout endpoint if (remotePrincipal && institutionId != null) { - final String institutionLogoutUrl = institutionHandler.findInstitutionLogoutUrlById(institutionId); + if (serviceUrl != null) { + institutionLogoutUrl = serviceUrl; + } else { + institutionLogoutUrl = institutionHandler.findInstitutionLogoutUrlById(institutionId); + } if (institutionLogoutUrl == null) { logger.warn("Institution {} does not have a dedicated logout url, use default logout redirection instead", institutionId); } else { @@ -132,6 +139,9 @@ public Event terminate(final RequestContext context) { // return `finish` event to prevent `logoutRedirectUrl` being overwritten return new Event(this, "finish"); } + } else if (serviceUrl != null) { + context.getFlowScope().put("logoutRedirectUrl", serviceUrl); + return new Event(this, "finish"); } return this.eventFactorySupport.success(this); diff --git a/cas-server-support-osf/src/test/java/io/cos/cas/AbstractTestUtils.java b/cas-server-support-osf/src/test/java/io/cos/cas/AbstractTestUtils.java index 1daa3c67..944ba9db 100644 --- a/cas-server-support-osf/src/test/java/io/cos/cas/AbstractTestUtils.java +++ b/cas-server-support-osf/src/test/java/io/cos/cas/AbstractTestUtils.java @@ -45,6 +45,8 @@ public abstract class AbstractTestUtils { public static final String[] CONST_SINGLE_ENTITLEMENTS_OUTPUT = {"value1-1", "value1-2", "value1-3"}; + public static final String CONST_JSON_STRING = "{\"key1-1\":\"value1-1\"}"; + private static final String REMOTE_USER = "REMOTE_USER"; private static final String ATTRIBUTE_PREFIX = "AUTH-"; diff --git a/cas-server-support-osf/src/test/java/io/cos/cas/mock/MockNotifyRemotePrincipalAuthenticated.java b/cas-server-support-osf/src/test/java/io/cos/cas/mock/MockNotifyRemotePrincipalAuthenticated.java index a878d4fa..d70330fb 100644 --- a/cas-server-support-osf/src/test/java/io/cos/cas/mock/MockNotifyRemotePrincipalAuthenticated.java +++ b/cas-server-support-osf/src/test/java/io/cos/cas/mock/MockNotifyRemotePrincipalAuthenticated.java @@ -23,6 +23,9 @@ public MockNotifyRemotePrincipalAuthenticated(final CentralAuthenticationService @Override protected PrincipalAuthenticationResult notifyRemotePrincipalAuthenticated( final OpenScienceFrameworkCredential credential) throws AccountException { - return new PrincipalAuthenticationResult(AbstractTestUtils.CONST_MAIL, AbstractTestUtils.CONST_INSTITUTION_ID); + return new PrincipalAuthenticationResult( + AbstractTestUtils.CONST_MAIL, + AbstractTestUtils.CONST_INSTITUTION_ID, + AbstractTestUtils.CONST_JSON_STRING); } } diff --git a/cas-server-webapp/src/main/resources/messages.properties b/cas-server-webapp/src/main/resources/messages.properties index 8c3a4194..f3407fd1 100644 --- a/cas-server-webapp/src/main/resources/messages.properties +++ b/cas-server-webapp/src/main/resources/messages.properties @@ -164,6 +164,13 @@ screen.institutionloginfailed.message=Your request cannot be completed at this t is in error, please contact Support for help and \ include the error code below. +# Institution Login Failure(LoA) Page +screen.institutionloginfailedloa.heading=Institution login failed +screen.institutionloginfailedloa.message=Does not meet the required AAL and IAL.

\ + If you believe this is in error,\ + please contact Support for help and \ + include the error code below. + # OAuth screen.oauth.confirm.header=Authorize application screen.oauth.confirm.message=

{0}

has asked for the following permission(s) to access your GakuNin RDM account. diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casInstitutionLoginFailedLoAView.jsp b/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casInstitutionLoginFailedLoAView.jsp new file mode 100644 index 00000000..b9bc6f2c --- /dev/null +++ b/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casInstitutionLoginFailedLoAView.jsp @@ -0,0 +1,42 @@ +<%-- + + Copyright (c) 2016. Center for Open Science + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--%> + +<%-- Institution login exception page --%> + + + +
+

+

+

errorCode=${casViewErrorCode}

+
+ + + + + + + + + + diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml b/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml index 11c45267..ae648b2a 100644 --- a/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml +++ b/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml @@ -209,6 +209,7 @@ + @@ -312,6 +313,12 @@ + + + + + + diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/webflow/logout/logout-webflow.xml b/cas-server-webapp/src/main/webapp/WEB-INF/webflow/logout/logout-webflow.xml index 2108fc00..5e7f22fa 100644 --- a/cas-server-webapp/src/main/webapp/WEB-INF/webflow/logout/logout-webflow.xml +++ b/cas-server-webapp/src/main/webapp/WEB-INF/webflow/logout/logout-webflow.xml @@ -25,6 +25,10 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"> + + + + @@ -48,9 +52,14 @@ - + + + + + +