Skip to content
Draft
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,95 @@
package io.cos.cas.osf.authentication.credential;

import com.fasterxml.jackson.annotation.JsonIgnore;

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

import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.credential.AbstractCredential;

import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.validation.ValidationContext;

/**
* This is {@link OsfOrcidSsoCredential}.
*
* @author Longze Chen
* @since 26.2.0
*/
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class OsfOrcidSsoCredential extends AbstractCredential {

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

/** The prefix which is added to {@link #orcidId} in {@link #getId()}. */
public static final String CREDENTIAL_ID_PREFIX = "OrcidProfile#";

/** Attribute name for ORCiD ID, which is released to OSF. */
public static final String AUTHENTICATION_ATTRIBUTE_ORCID_ID = "orcidId";

/** Attribute name for Access Token, which is released to OSF. */
public static final String AUTHENTICATION_ATTRIBUTE_ORCID_ACCESS_TOKEN = "orcidAccessToken";

/** Attribute name for Refresh Token, which is released to OSF. */
public static final String AUTHENTICATION_ATTRIBUTE_ORCID_REFRESH_TOKEN = "orcidRefreshToken";

/** ORCiD ID. */
private String orcidId;

/** ORCiD Access Token. */
private String orcidAccessToken;

/** ORCiD Refresh Token. */
private String orcidRefreshToken;

/**
* Get the unique identifier for this credential.
*
* @return the credential ID, formed as {@code CREDENTIAL_ID_PREFIX + orcidId}
*/
@Override
public String getId() {
return CREDENTIAL_ID_PREFIX + this.getOrcidId();
}

/**
* Check if credential is valid. {@link #orcidId} and {@link #orcidAccessToken} must not be null or empty.
*
* @return {@code true} if both {@code orcidId} and {@code orcidAccessToken} are non-null and non-blank,
* {@code false} otherwise
*/
@Override
@JsonIgnore
public boolean isValid() {
return StringUtils.isNoneBlank(this.orcidId, this.orcidAccessToken);
}

/**
* Validate this credential, adding an error message to the given context if it is not valid.
*
* @param context the validation context to which any error messages are added
*/
@Override
public void validate(final ValidationContext context) {
if (!isValid()) {
val messages = context.getMessageContext();
messages.addMessage(new MessageBuilder()
.error()
.source("token")
.defaultText("Unable to accept credential with an empty or unspecified ORCiD ID and/or tokens")
.build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential

private static DelegationProtocol DEFAULT_DELEGATION_PROTOCOL = DelegationProtocol.NONE;

public static String AUTHENTICATION_ATTRIBUTE_ORCID_ACCESS_TOKEN = "orcidAccessToken";

/**
* The one-time and ephemeral OSF verification key.
*/
Expand Down Expand Up @@ -84,6 +86,10 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential
*/
private Map<String, String> delegationAttributes = new LinkedHashMap<>();

private String orcidId;

private String orcidAccessToken;

@Override
public String getId() {
return this.getUsername();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package io.cos.cas.osf.authentication.handler.support;

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

import lombok.extern.slf4j.Slf4j;
import lombok.Getter;
import lombok.Setter;

import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.MessageDescriptor;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.principal.Principal;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;

import org.apache.commons.lang3.StringUtils;

import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* This is {@link OsfOrcidSsoAuthenticationHandler}.
*
* @author Longze Chen
* @since 26.2.0
*/
@Getter
@Setter
@Slf4j
public class OsfOrcidSsoAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {

/** Constructor for all required args. */
public OsfOrcidSsoAuthenticationHandler(
final String name,
final ServicesManager servicesManager,
final PrincipalFactory principalFactory,
final Integer order
) {
super(name, servicesManager, principalFactory, order);
}

/** Authenticate with no-op credential transform. */
@Override
protected final AuthenticationHandlerExecutionResult doAuthentication(
Credential credential
) throws GeneralSecurityException {
OsfOrcidSsoCredential osfOrcidSsoCredential = (OsfOrcidSsoCredential) credential;
LOGGER.debug("[ORCiD SSO] Attempting authentication internally for transformed credential [{}]", osfOrcidSsoCredential);
return authenticateOsfOrcidSsoInternal(osfOrcidSsoCredential);
}

/** {@link OsfOrcidSsoAuthenticationHandler} only supports {@link OsfOrcidSsoCredential} */
@Override
public boolean supports(final Class<? extends Credential> clazz) {
return OsfOrcidSsoCredential.class.isAssignableFrom(clazz);
}

/** {@link OsfOrcidSsoAuthenticationHandler} only supports {@link OsfOrcidSsoCredential} */
@Override
public boolean supports(final Credential credential) {
return credential instanceof OsfOrcidSsoCredential;
}

/** Create {@link AuthenticationHandlerExecutionResult} from {@link OsfOrcidSsoCredential}. */
protected final AuthenticationHandlerExecutionResult authenticateOsfOrcidSsoInternal(
final OsfOrcidSsoCredential credential
) throws GeneralSecurityException {

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

final String credentialId = credential.getId();
final String orcidId = credential.getOrcidId();
final String orcidAccessToken = credential.getOrcidAccessToken();
final String orcidRefreshToken = credential.getOrcidRefreshToken();

if (StringUtils.isBlank(orcidId)) {
LOGGER.error("[ORCiD SSO] Null/Empty ORCiD ID.");
throw new GeneralSecurityException("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.");
}

LOGGER.info(
"[ORCiD SSO] Credential metadata: id=[{}], orcidId=[{}], hasAccessToken=[{}], hasRefreshToken=[{}]",
credentialId,
orcidId,
StringUtils.isNotBlank(orcidAccessToken),
StringUtils.isNotBlank(orcidRefreshToken)
);

final Map<String, List<Object>> attributes = new LinkedHashMap<>();
final Principal principal = this.principalFactory.createPrincipal(credentialId, attributes);
final List<MessageDescriptor> warnings = new ArrayList<>();
return createHandlerResult(credential, principal, warnings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ protected final AuthenticationHandlerExecutionResult doAuthentication(
) throws GeneralSecurityException {
OsfPostgresCredential osfPostgresCredential = (OsfPostgresCredential) credential;
transformUsername(osfPostgresCredential);
if (osfPostgresCredential.isRemotePrincipal()) {
if (osfPostgresCredential.isRemotePrincipal() || osfPostgresCredential.getOrcidId() != null) {
osfPostgresCredential.setPassword(null);
osfPostgresCredential.setVerificationKey(null);
} else {
Expand Down Expand Up @@ -114,6 +114,8 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte
final boolean isTermsOfServiceChecked = credential.isTermsOfServiceChecked();
final boolean isRemotePrincipal = credential.isRemotePrincipal();
final DelegationProtocol delegationProtocol = credential.getDelegationProtocol();
final String orcidId = credential.getOrcidId();
final String orcidAccessToken = credential.getOrcidAccessToken();

LOGGER.debug(
"Credential metadata: username=[{}], password=[{}], verificationKey=[{}], oneTimePassword=[{}], " +
Expand All @@ -128,6 +130,22 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte
delegationProtocol
);

if (StringUtils.isNoneBlank(orcidId)) {
final String principalId = "OrcidProfile#" + orcidId;
final Map<String, List<Object>> attributes = new LinkedHashMap<>();
attributes.put("orcidAccessToken", Collections.singletonList(orcidAccessToken));
LOGGER.debug(">>>> Authenticating credential for ORCiD SSO");
LOGGER.debug(">>>> ---- username from credential = {}", username);
credential.setUsername(principalId);
LOGGER.debug(">>>> ---- username from credential updated = {}", credential.getUsername());
LOGGER.debug(">>>> ---- orcidId = {}", orcidId);
LOGGER.debug(">>>> ---- orcidAccessToken = {}", orcidAccessToken);
LOGGER.debug(">>>> ---- principalId = {}", principalId);
final Principal principal = this.principalFactory.createPrincipal(principalId, attributes);
final List<MessageDescriptor> warnings = new ArrayList<>();
return createHandlerResult(credential, principal, warnings);
}

final OsfUser osfUser = jpaOsfDao.findOneUserByEmail(username);
if (osfUser == null) {
if (StringUtils.isNoneBlank(plainTextPassword) || StringUtils.isNoneBlank(oneTimePassword)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.cos.cas.osf.authentication.metadata;

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

import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.apereo.cas.authentication.AuthenticationBuilder;
import org.apereo.cas.authentication.AuthenticationMetaDataPopulator;
import org.apereo.cas.authentication.AuthenticationTransaction;
import org.apereo.cas.authentication.Credential;

/**
* This is {@link OsfOrcidSsoAuthenticationMetaDataPopulator}.
*
* @author Longze Chen
* @since 26.1.0
*/
@Getter
@ToString(callSuper = true)
@Slf4j
public class OsfOrcidSsoAuthenticationMetaDataPopulator implements AuthenticationMetaDataPopulator {

/** Add attribute to authentication metadata. */
@Override
public void populateAttributes(final AuthenticationBuilder builder, final AuthenticationTransaction transaction) {
transaction.getPrimaryCredential().ifPresent(r -> {
final OsfOrcidSsoCredential credential = (OsfOrcidSsoCredential) r;
LOGGER.info(
"[ORCiD SSO] Credential is of type [{}], thus adding attributes [{}, {}, and optionally {} if not null/blank)]",
OsfOrcidSsoCredential.class.getSimpleName(),
OsfOrcidSsoCredential.AUTHENTICATION_ATTRIBUTE_ORCID_ID,
OsfOrcidSsoCredential.AUTHENTICATION_ATTRIBUTE_ORCID_ACCESS_TOKEN,
OsfOrcidSsoCredential.AUTHENTICATION_ATTRIBUTE_ORCID_REFRESH_TOKEN
);
builder.addAttribute(
OsfOrcidSsoCredential.AUTHENTICATION_ATTRIBUTE_ORCID_ID,
credential.getOrcidId()
);
builder.addAttribute(
OsfOrcidSsoCredential.AUTHENTICATION_ATTRIBUTE_ORCID_ACCESS_TOKEN,
credential.getOrcidAccessToken()
);
final String refreshToken = credential.getOrcidRefreshToken();
if (StringUtils.isNotBlank(refreshToken)) {
builder.addAttribute(
OsfOrcidSsoCredential.AUTHENTICATION_ATTRIBUTE_ORCID_REFRESH_TOKEN,
refreshToken
);
}
});
}

/** {@link OsfOrcidSsoAuthenticationMetaDataPopulator} only supports {@link OsfOrcidSsoCredential} */
@Override
public boolean supports(final Credential credential) {
return credential instanceof OsfOrcidSsoCredential;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,14 @@ public void populateAttributes(final AuthenticationBuilder builder, final Authen
transaction.getPrimaryCredential().ifPresent(r -> {
final OsfPostgresCredential credential = (OsfPostgresCredential) r;
LOGGER.debug(
"Credential is of type [{}], thus adding attributes [{}, {}, {}, {}, {}]",
"Credential is of type [{}], thus adding attributes [{}, {}, {}, {}, {} {}]",
OsfPostgresCredential.class.getSimpleName(),
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME,
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMOTE_PRINCIPAL,
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_DELEGATION_PROTOCOL,
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_INSTITUTION_ID,
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_TOS_CONSENT
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_TOS_CONSENT,
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_ORCID_ACCESS_TOKEN
);
builder.addAttribute(
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME,
Expand All @@ -55,6 +56,10 @@ public void populateAttributes(final AuthenticationBuilder builder, final Authen
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_INSTITUTION_ID,
credential.getInstitutionId()
);
builder.addAttribute(
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_ORCID_ACCESS_TOKEN,
credential.getOrcidAccessToken()
);
});
}

Expand Down
Loading
Loading