Skip to content

Commit

Permalink
Merge branch 'release/23.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cslzchen committed May 25, 2023
2 parents 4639e47 + b375444 commit 5ce8e80
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 49 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

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

23.2.0 (06-25-2023)
===================

* Institution support email for selective SSO

23.1.0 (01-25-2023)
===================

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public static boolean validateInstitutionForLogin(final JpaOsfDao jpaOsfDao, fin
return institution != null && institution.getDelegationProtocol() != null;
}

public static String getInstitutionSupportEmail(final JpaOsfDao jpaOsfDao, final String id) {
final OsfInstitution institution = jpaOsfDao.findOneInstitutionById(id);
return institution != null ? institution.getSupportEmail() : null;
}

public static Map<String, String> getInstitutionLoginUrlMap(
final JpaOsfDao jpaOsfDao,
final String target,
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/io/cos/cas/osf/model/OsfInstitution.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public class OsfInstitution extends AbstractOsfModel {
@Column(name = "deactivated")
private Date dateDeactivated;

@Column(name = "support_email", nullable = false)
private String supportEmail;

public DelegationProtocol getDelegationProtocol() {
try {
return DelegationProtocol.getType(delegationProtocol);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public Action osfNonInteractiveAuthenticationCheckAction() {
serviceTicketRequestWebflowEventResolver.getObject(),
adaptiveAuthenticationPolicy.getObject(),
centralAuthenticationService.getObject(),
jpaOsfDao.getObject(),
casProperties.getAuthn().getOsfUrl(),
casProperties.getAuthn().getOsfApi(),
authnDelegationClients
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
import org.springframework.webflow.execution.RequestContext;

import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.Set;

Expand Down Expand Up @@ -55,8 +53,6 @@ protected Event doExecute(RequestContext context) {
final boolean unsupportedInstitutionLogin = isUnsupportedInstitutionLogin(context);
final boolean orcidRedirect = isOrcidLoginAutoRedirect(context);
final String orcidLoginUrl = getOrcidLoginUrlFromFlowScope(context);

final String encodedServiceUrl = getEncodedServiceUrlFromRequestContext(context);
final boolean defaultService = isFromFlowlessErrorPage(context);
final OsfUrlProperties osfUrl = Optional.of(context).map(
requestContext -> (OsfUrlProperties) requestContext.getFlowScope().get(OsfCasWebflowConstants.FLOW_PARAMETER_OSF_URL)
Expand All @@ -72,19 +68,19 @@ protected Event doExecute(RequestContext context) {
-> (OsfCasLoginContext) requestContext.getFlowScope().get(PARAMETER_LOGIN_CONTEXT)).orElse(null);
if (loginContext == null) {
loginContext = new OsfCasLoginContext(
encodedServiceUrl,
institutionLogin,
institutionId,
StringUtils.EMPTY,
unsupportedInstitutionLogin,
orcidRedirect,
orcidLoginUrl,
defaultService,
defaultServiceUrl
);
} else {
loginContext.setEncodedServiceUrl(encodedServiceUrl);
loginContext.setInstitutionLogin(institutionLogin);
loginContext.setInstitutionId(institutionId);
loginContext.setInstitutionSupportEmail(StringUtils.EMPTY);
loginContext.setUnsupportedInstitutionLogin(unsupportedInstitutionLogin);
loginContext.setOrcidLoginUrl(orcidLoginUrl);
loginContext.setOrcidRedirect(false);
Expand Down Expand Up @@ -148,14 +144,6 @@ private String getOrcidLoginUrlFromFlowScope(final RequestContext context) {
return null;
}

private String getEncodedServiceUrlFromRequestContext(final RequestContext context) throws AssertionError {
final String serviceUrl = context.getRequestParameters().get(PARAMETER_SERVICE);
if (StringUtils.isBlank(serviceUrl)) {
return null;
}
return URLEncoder.encode(serviceUrl, StandardCharsets.UTF_8);
}

private boolean isFromFlowlessErrorPage(final RequestContext context) {
final String errorCode = context.getRequestParameters().get(PARAMETER_REDIRECT_SOURCE);
return !StringUtils.isBlank(errorCode) && EXPECTED_REDIRECT_CODES.contains(errorCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ protected Event doExecute(RequestContext context) {
loginContext.setInstitutionId(null);
context.getFlowScope().put(PARAMETER_LOGIN_CONTEXT, loginContext);
institutionId = null;
} else {
final String institutionSupportEmail = OsfInstitutionUtils.getInstitutionSupportEmail(jpaOsfDao, institutionId);
if (institutionSupportEmail != null) {
loginContext.setInstitutionSupportEmail(institutionSupportEmail);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
import io.cos.cas.osf.authentication.exception.InstitutionSsoOsfApiFailedException;
import io.cos.cas.osf.authentication.support.DelegationProtocol;
import io.cos.cas.osf.authentication.support.OsfApiPermissionDenied;
import io.cos.cas.osf.authentication.support.OsfInstitutionUtils;
import io.cos.cas.osf.configuration.model.OsfApiProperties;
import io.cos.cas.osf.configuration.model.OsfUrlProperties;
import io.cos.cas.osf.dao.JpaOsfDao;
import io.cos.cas.osf.web.support.OsfApiInstitutionAuthenticationResult;
import io.cos.cas.osf.web.support.OsfCasSsoErrorContext;

import com.nimbusds.jose.crypto.DirectEncrypter;
import com.nimbusds.jose.crypto.MACSigner;
Expand Down Expand Up @@ -141,6 +144,8 @@
@Getter
public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNonInteractiveCredentialsAction {

private static final String PARAMETER_SSO_ERROR_CONTEXT = "osfCasSsoErrorContext";

private static final String USERNAME_PARAMETER_NAME = "username";

private static final String VERIFICATION_KEY_PARAMETER_NAME = "verification_key";
Expand Down Expand Up @@ -179,6 +184,9 @@ public class OsfPrincipalFromNonInteractiveCredentialsAction extends AbstractNon
@NotNull
private CentralAuthenticationService centralAuthenticationService;

@NotNull
private final JpaOsfDao jpaOsfDao;

@NotNull
private OsfUrlProperties osfUrlProperties;

Expand All @@ -195,6 +203,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction(
final CasWebflowEventResolver serviceTicketRequestWebflowEventResolver,
final AdaptiveAuthenticationPolicy adaptiveAuthenticationPolicy,
final CentralAuthenticationService centralAuthenticationService,
final JpaOsfDao jpaOsfDao,
final OsfUrlProperties osfUrlProperties,
final OsfApiProperties osfApiProperties,
final Map<String, List<String>> authnDelegationClients
Expand All @@ -205,6 +214,7 @@ public OsfPrincipalFromNonInteractiveCredentialsAction(
adaptiveAuthenticationPolicy
);
this.centralAuthenticationService = centralAuthenticationService;
this.jpaOsfDao = jpaOsfDao;
this.osfUrlProperties = osfUrlProperties;
this.osfApiProperties = osfApiProperties;
this.authnDelegationClients = authnDelegationClients;
Expand Down Expand Up @@ -240,7 +250,8 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex
);
final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromPac4jAuthentication(context, clientName);
if (osfPostgresCredential != null) {
final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential);
final OsfApiInstitutionAuthenticationResult remoteUserInfo
= notifyOsfApiOfInstnAuthnSuccess(context, osfPostgresCredential);
osfPostgresCredential.setUsername(remoteUserInfo.getSsoEmail());
osfPostgresCredential.setInstitutionId(remoteUserInfo.getInstitutionId());
WebUtils.removeCredential(context);
Expand All @@ -263,7 +274,8 @@ protected Credential constructCredentialsFromRequest(final RequestContext contex
// Type 3: institution sso via Shibboleth authentication using the SAML protocol
LOGGER.debug("Shibboleth session / header found in request context.");
final OsfPostgresCredential osfPostgresCredential = constructCredentialsFromShibbolethAuthentication(context, request);
final OsfApiInstitutionAuthenticationResult remoteUserInfo = notifyOsfApiOfInstnAuthnSuccess(osfPostgresCredential);
final OsfApiInstitutionAuthenticationResult remoteUserInfo
= notifyOsfApiOfInstnAuthnSuccess(context, osfPostgresCredential);
final String ssoIdentity = osfPostgresCredential.getSsoIdentity();
final String eppn = osfPostgresCredential.getDelegationAttributes().get("eppn");
final String mail = osfPostgresCredential.getDelegationAttributes().get("mail");
Expand Down Expand Up @@ -568,6 +580,7 @@ private JSONObject extractInstnAuthnDataFromCredential(final OsfPostgresCredenti
* @throws AccountException if there is an issue with authentication data or if the OSF API request has failed
*/
private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess(
final RequestContext context,
final OsfPostgresCredential credential
) throws AccountException {

Expand Down Expand Up @@ -758,6 +771,15 @@ private OsfApiInstitutionAuthenticationResult notifyOsfApiOfInstnAuthnSuccess(
final String errorDetail = ((JsonObject) error).get("detail").getAsString();
if (OsfApiPermissionDenied.INSTITUTION_SSO_SELECTIVE_LOGIN_DENIED.getId().equals(errorDetail)) {
LOGGER.error("[OSF API] Failure - Institution Selective SSO Not Allowed: {}, filter={}", ssoUser, selectiveSsoFilter);
setSsoErrorContext(
context,
InstitutionSsoSelectiveLoginDeniedException.class.getSimpleName(),
String.format("Institution Selective SSO Not Allowed: %s", ssoUser),
ssoEmail,
ssoIdentity,
institutionId,
OsfInstitutionUtils.getInstitutionSupportEmail(this.jpaOsfDao, institutionId)
);
throw new InstitutionSsoSelectiveLoginDeniedException("OSF API denies selective SSO login");
}
if (OsfApiPermissionDenied.INSTITUTION_SSO_DUPLICATE_IDENTITY.getId().equals(errorDetail)) {
Expand Down Expand Up @@ -810,4 +832,35 @@ private String retrieveDepartment(final String departmentRaw, final boolean eduP
}
return "";
}

/**
* Prepare {@link OsfCasSsoErrorContext} and put it in flow.
*
* @param context the request context
* @param handleErrorName the error name
* @param errorMessage the error message
* @param ssoEmail user's SSO email
* @param ssoIdentity user's SSO identity
* @param institutionId institution ID
* @param institutionSupportEmail institution support email
*/
private void setSsoErrorContext(
final RequestContext context,
final String handleErrorName,
final String errorMessage,
final String ssoEmail,
final String ssoIdentity,
final String institutionId,
final String institutionSupportEmail
) {
OsfCasSsoErrorContext ssoErrorContext = new OsfCasSsoErrorContext(
handleErrorName,
errorMessage,
ssoEmail,
ssoIdentity,
institutionId,
institutionSupportEmail
);
context.getFlowScope().put(PARAMETER_SSO_ERROR_CONTEXT, ssoErrorContext);
}
}
33 changes: 2 additions & 31 deletions src/main/java/io/cos/cas/osf/web/support/OsfCasLoginContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,12 @@ public class OsfCasLoginContext implements Serializable {

private static final long serialVersionUID = 7523144720609509742L;

/**
* The encoded service URL provided by the "service=" query param in the request URL.
*
* This attribute is deprecated and should be removed since 1) ThymeLeaf handles URL building elegantly in the template and 2) both of
* the flow parameters "service.originalUrl" and "originalUrl" stores the current service information.
*/
private String encodedServiceUrl;

private String handleErrorName;

private boolean institutionLogin;

private String institutionId;

private String institutionSupportEmail;

private boolean unsupportedInstitutionLogin;

private boolean orcidRedirect;
Expand All @@ -54,25 +46,4 @@ public class OsfCasLoginContext implements Serializable {
* e.g. http(s)://[OSF Domain]/login?next=[encoded version of http(s)://[OSF Domain]/]
*/
private String defaultServiceUrl;

public OsfCasLoginContext (
final String encodedServiceUrl,
final boolean institutionLogin,
final String institutionId,
final boolean unsupportedInstitutionLogin,
final boolean orcidRedirect,
final String orcidLoginUrl,
final boolean defaultService,
final String defaultServiceUrl
) {
this.encodedServiceUrl = encodedServiceUrl;
this.handleErrorName = null;
this.institutionLogin = institutionLogin;
this.institutionId = institutionId;
this.unsupportedInstitutionLogin = unsupportedInstitutionLogin;
this.orcidRedirect = orcidRedirect;
this.orcidLoginUrl = orcidLoginUrl;
this.defaultService = defaultService;
this.defaultServiceUrl = defaultServiceUrl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.cos.cas.osf.web.support;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;

/**
* This is {@link OsfCasSsoErrorContext}.
*
* Stores detailed error information, which can be prepared and put into flow before raising an exception. Extends {@link Serializable}
* so that it can be put into and retrieved from the flow context conveniently.
*
* @author Longze Chen
* @since 23.2.0
*/
@AllArgsConstructor
@Getter
@NoArgsConstructor
@ToString
@Setter
public class OsfCasSsoErrorContext implements Serializable {

private static final long serialVersionUID = -1366351087792035267L;

private String handleErrorName;

private String errorMessage;

private String ssoEmail;

private String ssoIdentity;

private String institutionId;

private String institutionSupportEmail;
}
7 changes: 6 additions & 1 deletion src/main/resources/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -708,10 +708,15 @@ screen.institutionssoattributeparsingfailed.message=\
screen.institutionssoduplicateidentity.message=\
Your request cannot be completed at this time due to an error caused by duplicate SSO identity. \
Please contact <a style="white-space: nowrap" href="mailto:[email protected]">Support</a> for help.
screen.institutionssoselectivelogindenied.message=\
screen.institutionssoselectivelogindenied.standard.message=\
Your institutional account is unable to authenticate to OSF. Please check with your institution. \
If your institution believes this is in error, \
contact <a style="white-space: nowrap" href="mailto:[email protected]">Support</a> for help.
screen.institutionssoselectivelogindenied.support.message=\
Your institutional account is unable to authenticate to OSF. \
Please contact <a style="white-space: nowrap" href="mailto:{0}">support at your institution</a> first. \
If your institution believes this is in error, \
contact <a style="white-space: nowrap" href="mailto:[email protected]">OSF Support</a> for help.
screen.institutionssoosfapifailed.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. \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
<hr class="my-4" />
<section class="card-message">
<h1 th:utext="#{screen.institutionssofailed.heading}"></h1>
<p th:utext="#{screen.institutionssoselectivelogindenied.message}"></p>
<p th:if="${#strings.isEmpty(osfCasSsoErrorContext.institutionSupportEmail)}" th:utext="#{screen.institutionssoselectivelogindenied.standard.message}"></p>
<p th:unless="${#strings.isEmpty(osfCasSsoErrorContext.institutionSupportEmail)}" th:utext="#{screen.institutionssoselectivelogindenied.support.message(${osfCasSsoErrorContext.institutionSupportEmail})}"></p>
</section>
<section class="form-button">
<a class="mdc-button mdc-button--raised button-osf-blue" th:href="@{/logout(service=${osfUrl.logout})}">
Expand Down

0 comments on commit 5ce8e80

Please sign in to comment.