Skip to content

Commit

Permalink
Merge branch 'release/20.0.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cslzchen committed Feb 3, 2020
2 parents 33e5093 + b8df201 commit 37ecf45
Show file tree
Hide file tree
Showing 18 changed files with 516 additions and 180 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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

20.0.0 (2020-02-03)
===================

- Fixed user status check for new unconfirmed ORCiD user
- Updated the institution SSO guide for both SAML and CAS
- Added a guide for common apache / shibboleth errors

19.3.3 (2020-01-02)
===================

Expand Down
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 37ecf45

Please sign in to comment.