Skip to content
Merged
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 @@ -372,22 +382,61 @@ protected OpenScienceFrameworkCredential constructCredential(
for (final String headerName : Collections.list(request.getHeaderNames())) {
if (headerName.startsWith(ATTRIBUTE_PREFIX)) {
final String headerValue = request.getHeader(headerName);
String decodedValue;
if (headerValue == null) {
decodedValue = headerValue;
} else {
try {
decodedValue = new String(headerValue.getBytes("ISO-8859-1"), "UTF-8");
} catch (final java.io.UnsupportedEncodingException e) {
decodedValue = headerValue; // UTF-8 is always supported, this never happens
}
}
logger.debug(
"[SAML Shibboleth] User's institutional identity '{}' - auth header '{}': '{}'",
remoteUser,
headerName,
headerValue
decodedValue
);
credential.getDelegationAttributes().put(
headerName.substring(ATTRIBUTE_PREFIX.length()),
headerValue
decodedValue
);
}
}

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 +554,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 +643,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 +657,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 +741,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
Loading
Loading