Skip to content
Open
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,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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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() {
Expand All @@ -159,6 +164,10 @@ public String getUsername() {
public String getInstitutionId() {
return institutionId;
}

public String getContext() {
return context;
}
}

private static final String CONST_CREDENTIAL = "credential";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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();
Expand All @@ -579,17 +647,22 @@ 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();
final String normalizeEntitlement = entitlement.replace("\\;", ";");
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);
Expand Down Expand Up @@ -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");
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,16 +124,24 @@ 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 {
context.getFlowScope().put("logoutRedirectUrl", institutionLogoutUrl);
// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions cas-server-webapp/src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,13 @@ screen.institutionloginfailed.message=Your request cannot be completed at this t
is in error, please contact <a style="white-space: nowrap" href="mailto:rdm_support@nii.ac.jp">Support</a> 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.</br></br>\
If you believe this is in error,\
please contact <a style="white-space: nowrap" href="mailto:rdm_support@nii.ac.jp">Support</a> for help and \
include the error code below.

# OAuth
screen.oauth.confirm.header=Authorize application
screen.oauth.confirm.message=<h2>{0}</h2> has asked for the following permission(s) to access your GakuNin RDM account.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 --%>

<jsp:directive.include file="includes/top.jsp"/>

<div id="msg" class="errors">
<h2><spring:message code="screen.institutionloginfailedloa.heading"/></h2>
<p><spring:message code="screen.institutionloginfailedloa.message"/></p>
<p>errorCode=${casViewErrorCode}</p>
</div>

<spring:message code="screen.osf.login.message.error" var="errorDescription"/>
<script>
description = document.getElementById("description");
if (description != null) {
description.innerHTML = "<br><br>${errorDescription}";
}
</script>

<c:set var="linkSignIn" value="false"/>
<c:set var="linkSignOut" value="false"/>
<c:set var="linkCreateAccount" value="false"/>
<c:set var="linkBackToOsf" value="true"/>

<jsp:directive.include file="includes/bottom.jsp"/>
Loading
Loading