Skip to content

Commit

Permalink
Merge branch 'feature/update-user-status-check' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
cslzchen committed Feb 3, 2020
2 parents 499061c + 5c2b1e7 commit a77b0d0
Show file tree
Hide file tree
Showing 14 changed files with 380 additions and 166 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,39 @@
*/
package io.cos.cas.adaptors.postgres.handlers;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import io.cos.cas.adaptors.postgres.daos.OpenScienceFrameworkDaoImpl;
import io.cos.cas.adaptors.postgres.models.OpenScienceFrameworkGuid;
import io.cos.cas.adaptors.postgres.models.OpenScienceFrameworkTimeBasedOneTimePassword;
import io.cos.cas.adaptors.postgres.models.OpenScienceFrameworkUser;
import io.cos.cas.adaptors.postgres.daos.OpenScienceFrameworkDaoImpl;
import io.cos.cas.authentication.exceptions.AccountNotConfirmedIdPLoginException;
import io.cos.cas.authentication.exceptions.AccountNotConfirmedOsfLoginException;
import io.cos.cas.authentication.InvalidVerificationKeyException;
import io.cos.cas.authentication.LoginNotAllowedException;
import io.cos.cas.authentication.OneTimePasswordFailedLoginException;
import io.cos.cas.authentication.OneTimePasswordRequiredException;
import io.cos.cas.authentication.OpenScienceFrameworkCredential;

import io.cos.cas.authentication.ShouldNotHappenException;
import io.cos.cas.authentication.oath.TotpUtils;

import org.jasig.cas.authentication.AccountDisabledException;
import org.jasig.cas.authentication.Credential;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.PreventedException;
import org.jasig.cas.authentication.handler.NoOpPrincipalNameTransformer;
import org.jasig.cas.authentication.handler.PrincipalNameTransformer;
import org.jasig.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.jasig.cas.authentication.HandlerResult;
import org.jasig.cas.authentication.PreventedException;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.bcrypt.BCrypt;

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;

import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import javax.validation.constraints.NotNull;
Expand All @@ -54,7 +58,7 @@
*
* @author Michael Haselton
* @author Longze Chen
* @since 4.1.0
* @since 19.0.0
*/
public class OpenScienceFrameworkAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler
implements InitializingBean {
Expand All @@ -65,7 +69,8 @@ public class OpenScienceFrameworkAuthenticationHandler extends AbstractPreAndPos

// user status
private static final String USER_ACTIVE = "ACTIVE";
private static final String USER_NOT_CONFIRMED = "NOT_CONFIRMED";
private static final String USER_NOT_CONFIRMED_OSF = "NOT_CONFIRMED_OSF";
private static final String USER_NOT_CONFIRMED_IDP = "NOT_CONFIRMED_IDP";
private static final String USER_NOT_CLAIMED = "NOT_CLAIMED";
private static final String USER_MERGED = "MERGED";
private static final String USER_DISABLED = "DISABLED";
Expand Down Expand Up @@ -183,9 +188,11 @@ protected final HandlerResult authenticateInternal(final OpenScienceFrameworkCre
}

// Check user's status, and only ACTIVE user can sign in
if (USER_NOT_CONFIRMED.equals(userStatus)) {
throw new LoginNotAllowedException(username + " is registered but not confirmed");
} else if (USER_DISABLED.equals(userStatus)) {
if (USER_NOT_CONFIRMED_OSF.equals(userStatus)) {
throw new AccountNotConfirmedOsfLoginException(username + " is registered but not confirmed");
} else if (USER_NOT_CONFIRMED_IDP.equals(userStatus)) {
throw new AccountNotConfirmedIdPLoginException(username + " is registered via external IdP but not confirmed ");
} else if (USER_DISABLED.equals(userStatus)) {
throw new AccountDisabledException(username + " is disabled");
} else if (USER_NOT_CLAIMED.equals(userStatus)) {
throw new ShouldNotHappenException(username + " is not claimed");
Expand Down Expand Up @@ -215,42 +222,58 @@ public boolean supports(final Credential credential) {
}

/**
* Verify user status.
* Check and verify user status.
*
* USER_ACTIVE: The user is active.
*
* USER_NOT_CONFIRMED_OSF: The user is created via default username / password sign-up but not confirmed.
*
* USER_ACTIVE: Active user found, proceed.
* USER_NOT_CONFIRMED_IDP: The user is created via via external IdP (e.g. ORCiD) login but not confirmed.
*
* USER_NOT_CONFIRMED: Inform users that the account is created but not confirmed. In addition, provide them
* with a link to resend confirmation email.
* USER_NOT_CLAIMED: The user is created as an unclaimed contributor but not claimed.
*
* USER_DISABLED: Inform users that the account is disable and that they should contact OSF support.
* USER_DISABLED: The user has been deactivated.
*
* USER_MERGED,
* USER_NOT_CLAIMED,
* USER_STATUS_UNKNOWN: These three are internal or invalid user status that are not supposed to happen with
* normal authentication and authorization flow.
* USER_MERGED: The user has been merged into another user.
*
* @param user the OSF user
* @return the user status
* USER_STATUS_UNKNOWN: Unknown or invalid status. This usually indicates that there is something wrong with
* the OSF-CAS auth logic and / or the OSF user model.
*
* @param user an {@link OpenScienceFrameworkUser} instance
* @return a {@link String} that represents the user status
*/
private String verifyUserStatus(final OpenScienceFrameworkUser user) {

// An active user must be registered, not disabled, not merged and has a not null password.
// Only active users can pass the verification.
if (user.isActive()) {
logger.info("User Status Check: {}", USER_ACTIVE);
return USER_ACTIVE;
} else {
// If the user instance is neither registered nor not confirmed, it can be either an unclaimed contributor
// or a newly created user pending confirmation. The difference is whether it has a usable password.
// or a newly created user pending confirmation.
if (!user.isRegistered() && !user.isConfirmed()) {
if (isUnusablePassword(user.getPassword())) {
// If the user instance has an unusable password, it must be an unclaimed contributor.
// If the user instance has an unusable password but also has a pending external identity "CREATE"
// confirmation, it must be an unconfirmed user created via external IdP login.
try {
if (isCreatedByExternalIdp(user.getExternalIdentity())) {
logger.info("User Status Check: {}", USER_NOT_CONFIRMED_IDP);
return USER_NOT_CONFIRMED_IDP;
}
} catch (final ShouldNotHappenException e) {
logger.error("User Status Check: {}", USER_STATUS_UNKNOWN);
return USER_STATUS_UNKNOWN;
}
// If the user instance has an unusable password without any pending external identity "CREATE"
// confirmation, it must be an unclaimed contributor.
logger.info("User Status Check: {}", USER_NOT_CLAIMED);
return USER_NOT_CLAIMED;
} else if (checkPasswordPrefix(user.getPassword())) {
// If the user instance has a password with a valid prefix, it must be a unconfirmed user who
// has registered for a new account.
logger.info("User Status Check: {}", USER_NOT_CONFIRMED);
return USER_NOT_CONFIRMED;
logger.info("User Status Check: {}", USER_NOT_CONFIRMED_OSF);
return USER_NOT_CONFIRMED_OSF;
}
}
// If the user instance has been merged by another user, it stays registered and confirmed. The username is
Expand Down Expand Up @@ -307,6 +330,33 @@ private boolean verifyPassword(final String plainTextPassword, final String user
}
}

/**
* Check if the user instance is created by an external identity provider and is pending confirmation.
*
* @param externalIdentity a {@link JsonObject} that stores all external identities of a user instance
* @return {@code true} if so and {@code false} otherwise
* @throws ShouldNotHappenException if {@code externalIdentity} fails JSON parsing.
*/
private boolean isCreatedByExternalIdp(final JsonObject externalIdentity) throws ShouldNotHappenException {

for (final Map.Entry<String, JsonElement> provider : externalIdentity.entrySet()) {
try {
for (final Map.Entry<String, JsonElement> identity : provider.getValue().getAsJsonObject().entrySet()) {
if (!identity.getValue().isJsonPrimitive()) {
throw new ShouldNotHappenException();
}
if ("CREATE".equals(identity.getValue().getAsString())) {
logger.info("New and unconfirmed OSF user: {} : {}", identity.getKey(), identity.getValue().toString());
return true;
}
}
} catch (final IllegalStateException e) {
throw new ShouldNotHappenException();
}
}
return false;
}

/**
* Check if the password hash is "django-unusable".
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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.adaptors.postgres.hibernate;

import org.hibernate.dialect.PostgreSQL9Dialect;

import java.sql.Types;

/**
* Customized Postgres dialect that supports {@literal jsonb}.
*
* @author Longze Chen
* @since 20.0.0
*/
public class OSFPostgreSQLDialect extends PostgreSQL9Dialect {

/** The default constructor. */
public OSFPostgreSQLDialect() {
this.registerColumnType(Types.JAVA_OBJECT, "jsonb");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@
*/
package io.cos.cas.adaptors.postgres.models;

import com.google.gson.JsonObject;

import io.cos.cas.adaptors.postgres.types.PostgresJsonbUserType;

import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
Expand All @@ -23,17 +32,17 @@
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;

/**
* The Open Science Framework User.
*
* @author Michael Haselton
* @author Longze Chen
* @since 4.1.0
* @since 19.0.0
*/
@Entity
@Table(name = "osf_osfuser")
@TypeDef(name = "PostgresJsonb", typeClass = PostgresJsonbUserType.class)
public final class OpenScienceFrameworkUser {

@Id
Expand All @@ -46,6 +55,10 @@ public final class OpenScienceFrameworkUser {
@Column(name = "password", nullable = false)
private String password;

@Column(name = "external_identity")
@Type(type = "PostgresJsonb")
private JsonObject externalIdentity;

@Column(name = "verification_key")
private String verificationKey;

Expand Down Expand Up @@ -85,6 +98,10 @@ public String getPassword() {
return password;
}

public JsonObject getExternalIdentity() {
return externalIdentity;
}

public String getVerificationKey() {
return verificationKey;
}
Expand Down
Loading

0 comments on commit a77b0d0

Please sign in to comment.