diff --git a/CHANGELOG.md b/CHANGELOG.md index ec845562..92d3ef77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) =================== diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/handlers/OpenScienceFrameworkAuthenticationHandler.java b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/handlers/OpenScienceFrameworkAuthenticationHandler.java index f2b7469f..eea892e1 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/handlers/OpenScienceFrameworkAuthenticationHandler.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/handlers/OpenScienceFrameworkAuthenticationHandler.java @@ -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; @@ -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 { @@ -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"; @@ -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"); @@ -215,24 +222,28 @@ 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()) { @@ -240,17 +251,29 @@ private String verifyUserStatus(final OpenScienceFrameworkUser user) { 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 @@ -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 provider : externalIdentity.entrySet()) { + try { + for (final Map.Entry 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". * diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/hibernate/OSFPostgreSQLDialect.java b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/hibernate/OSFPostgreSQLDialect.java new file mode 100644 index 00000000..79baa113 --- /dev/null +++ b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/hibernate/OSFPostgreSQLDialect.java @@ -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"); + } +} diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkUser.java b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkUser.java index faceb683..69e6ea5d 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkUser.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/models/OpenScienceFrameworkUser.java @@ -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; @@ -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 @@ -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; @@ -85,6 +98,10 @@ public String getPassword() { return password; } + public JsonObject getExternalIdentity() { + return externalIdentity; + } + public String getVerificationKey() { return verificationKey; } diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/types/PostgresJsonbUserType.java b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/types/PostgresJsonbUserType.java new file mode 100644 index 00000000..925382e2 --- /dev/null +++ b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/types/PostgresJsonbUserType.java @@ -0,0 +1,133 @@ +/* + * 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.types; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + +import org.hibernate.cfg.NotYetImplementedException; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.HibernateException; +import org.hibernate.usertype.UserType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; + +/** + * Customized Hibernate data type for Postgres {@literal jsonb}. + * + * {@link com.google.gson.JsonObject} is used as the object type / class for Postgres {@literal jsonb}. + * + * CAS only has read-access to the OSF database. Thus, 1) the type is immutable; 2) {@link this#nullSafeGet} is not + * implemented; 3) {@link this#deepCopy} simply returns the argument. Several methods are implemented with default / + * minimal behavior by using the {@link this#deepCopy}. + * + * @author Longze Chen + * @since 20.0.0 + */ +public class PostgresJsonbUserType implements UserType { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public int[] sqlTypes() { + return new int[]{Types.JAVA_OBJECT}; + } + + @Override + public Class returnedClass() { + return JsonObject.class; + } + + @Override + public boolean equals(final Object x, final Object y) throws HibernateException { + if (x == null) { + return y == null; + } + return x.equals(y); + } + + @Override + public int hashCode(final Object x) throws HibernateException { + return x.hashCode(); + } + + @Override + public Object nullSafeGet( + final ResultSet rs, + final String[] names, + final SessionImplementor session, + final Object owner + ) throws HibernateException, SQLException { + final String jsonString = rs.getString(names[0]); + if (jsonString == null) { + return null; + } + try { + final JsonParser jsonParser = new JsonParser(); + return jsonParser.parse(jsonString).getAsJsonObject(); + } catch (final JsonSyntaxException | IllegalStateException e) { + logger.error("PostgresJsonbUserType.nullSafeGet(): failed to convert Java JSON String to GSON JsonObject:"); + throw new RuntimeException("Failed to convert Java JSON String to GSON JsonObject: " + e.getMessage()); + } + } + + // There is no need to implement this class since CAS only has read-access to the OSF DB. + @Override + public void nullSafeSet( + final PreparedStatement st, + final Object value, + final int index, + final SessionImplementor session + ) throws HibernateException { + throw new NotYetImplementedException(); + } + + // Immutable object: simply return the argument. + @Override + public Object deepCopy(final Object value) throws HibernateException { + return value; + } + + // Objects of this type is immutable. + @Override + public boolean isMutable() { + return false; + } + + @Override + public Serializable disassemble(final Object value) throws HibernateException { + return (Serializable) this.deepCopy(value); + } + + @Override + public Object assemble(final Serializable cached, final Object owner) throws HibernateException { + return this.deepCopy(cached); + } + + @Override + public Object replace(final Object original, final Object target, final Object owner) throws HibernateException { + return this.deepCopy(original); + } +} diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/types/StringListUserType.java b/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/types/StringListUserType.java deleted file mode 100644 index 53aa7472..00000000 --- a/cas-server-support-osf/src/main/java/io/cos/cas/adaptors/postgres/types/StringListUserType.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2016. 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.types; - -import org.hibernate.HibernateException; -import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.usertype.UserType; - -import java.io.Serializable; -import java.sql.Array; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.ResultSet; -import java.sql.Types; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * The String List User Type. - * - * @author Longze Chen - * @since 4.0.1 - */ -public class StringListUserType implements UserType { - - @Override - public int[] sqlTypes() { - return new int[] {Types.ARRAY}; - } - - @Override - public Class returnedClass() { - return List.class; - } - - @Override - public boolean equals(final Object o1, final Object o2) throws HibernateException { - return o1.equals(o2); - } - - @Override - public int hashCode(final Object o) throws HibernateException { - return o != null ? o.hashCode() : 0; - } - - @Override - public Object nullSafeGet( - final ResultSet resultSet, - final String[] names, - final SessionImplementor sessionImplementor, - final Object owner - ) throws HibernateException, SQLException { - - final Array array = resultSet.getArray(names[0]); - if (!resultSet.wasNull() && array != null) { - return new ArrayList<>(Arrays.asList((String[]) array.getArray())); - } - return null; - } - - @Override - public void nullSafeSet( - final PreparedStatement preparedStatement, - final Object value, - final int index, - final SessionImplementor sessionImplementor - ) throws HibernateException, SQLException { - // no need to implement this method since CAS is postgres readonly. - } - - @Override - public Object deepCopy(final Object value) throws HibernateException { - return value; - } - - @Override - public boolean isMutable() { - return false; - } - - @Override - public Serializable disassemble(final Object value) throws HibernateException { - return (Serializable) value; - } - - @Override - public Object assemble(final Serializable cached, final Object owner) throws HibernateException { - return cached; - } - - @Override - public Object replace(final Object original, final Object target, final Object owner) throws HibernateException { - return original; - } -} diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/AccountNotConfirmedIdPLoginException.java b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/AccountNotConfirmedIdPLoginException.java new file mode 100644 index 00000000..94c9ec02 --- /dev/null +++ b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/AccountNotConfirmedIdPLoginException.java @@ -0,0 +1,45 @@ +/* + * 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; + +import javax.security.auth.login.AccountException; + +/** + * Describes an error condition where authentication occurs from an unconfirmed account created by external identity + * provider (IdP) login. This exception only applies to IdPs that require user email confirmation. Currently, there + * is only one: ORCiD. Institution IdPs do not require user email confirmation. + * + * @author Longze Chen + * @since 20.0.0 + */ +public class AccountNotConfirmedIdPLoginException extends AccountException { + + private static final long serialVersionUID = 2165106893184566462L; + + /** Instantiates a new exception (default). */ + public AccountNotConfirmedIdPLoginException() { + super(); + } + + /** + * Instantiates a new exception with a given message. + * + * @param message the message + */ + public AccountNotConfirmedIdPLoginException(final String message) { + super(message); + } +} diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/LoginNotAllowedException.java b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/AccountNotConfirmedOsfLoginException.java similarity index 72% rename from cas-server-support-osf/src/main/java/io/cos/cas/authentication/LoginNotAllowedException.java rename to cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/AccountNotConfirmedOsfLoginException.java index 2add2e69..7f5d434a 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/LoginNotAllowedException.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/exceptions/AccountNotConfirmedOsfLoginException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015. Center for Open Science + * 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. @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.cos.cas.authentication; +package io.cos.cas.authentication.exceptions; import javax.security.auth.login.AccountException; /** - * Describes an error condition where authentication occurs from an registered but not confirmed account. + * Describes an error condition where authentication occurs from a registered (via OSF email-password sign-up) but + * not confirmed account. * - * @author Michael Haselton * @author Longze Chen - * @since 4.1.5 + * @since 20.0.0 */ -public class LoginNotAllowedException extends AccountException { +public class AccountNotConfirmedOsfLoginException extends AccountException { private static final long serialVersionUID = 3376259469680697722L; /** Instantiates a new exception (default). */ - public LoginNotAllowedException() { + public AccountNotConfirmedOsfLoginException() { super(); } @@ -38,7 +38,7 @@ public LoginNotAllowedException() { * * @param message the message */ - public LoginNotAllowedException(final String message) { + public AccountNotConfirmedOsfLoginException(final String message) { super(message); } } diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java b/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java index 6111cd62..9a3eb29a 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/web/flow/OpenScienceFrameworkAuthenticationExceptionHandler.java @@ -20,11 +20,12 @@ import java.util.List; import java.util.Set; +import io.cos.cas.authentication.exceptions.AccountNotConfirmedIdPLoginException; +import io.cos.cas.authentication.exceptions.AccountNotConfirmedOsfLoginException; import io.cos.cas.authentication.exceptions.CasClientLoginException; import io.cos.cas.authentication.exceptions.DelegatedLoginException; import io.cos.cas.authentication.exceptions.OrcidClientLoginException; 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.RemoteUserFailedLoginException; @@ -51,7 +52,7 @@ * * @author Michael Haselton * @author Longze Chen - * @since 4.1.5 + * @since 19.0.0 */ public class OpenScienceFrameworkAuthenticationExceptionHandler extends AuthenticationExceptionHandler { @@ -80,7 +81,8 @@ public class OpenScienceFrameworkAuthenticationExceptionHandler extends Authenti // Customized exceptions for OSF static { DEFAULT_ERROR_LIST.add(InvalidVerificationKeyException.class); - DEFAULT_ERROR_LIST.add(LoginNotAllowedException.class); + DEFAULT_ERROR_LIST.add(AccountNotConfirmedOsfLoginException.class); + DEFAULT_ERROR_LIST.add(AccountNotConfirmedIdPLoginException.class); DEFAULT_ERROR_LIST.add(ShouldNotHappenException.class); DEFAULT_ERROR_LIST.add(RemoteUserFailedLoginException.class); DEFAULT_ERROR_LIST.add(OneTimePasswordFailedLoginException.class); diff --git a/cas-server-webapp/src/main/resources/messages.properties b/cas-server-webapp/src/main/resources/messages.properties index 0fdc43e8..3d9c7b03 100644 --- a/cas-server-webapp/src/main/resources/messages.properties +++ b/cas-server-webapp/src/main/resources/messages.properties @@ -144,9 +144,11 @@ screen.badworkstation.heading=You cannot login from this workstation. screen.badworkstation.message=Please contact support@osf.io to regain access. # OSF Login Failure Pages -screen.loginnotallowed.heading=Account not confirmed -screen.loginnotallowed.message=The OSF account associated with the email has been registered but not confirmed. Please check your email (and spam folder) or click the button below to resend your confirmation email. -screen.loginnotallowed.button.resendConfirmation=Resend confirmation email +screen.accountnotconfirmed.osflogin.heading=Account not confirmed +screen.accountnotconfirmed.osflogin.message=The OSF account associated with the email has been registered but not confirmed. Please check your email (and spam folder) or click the button below to resend your confirmation email. +screen.accountnotconfirmed.osflogin.button.resendConfirmation=Resend confirmation email +screen.accountnotconfirmed.idplogin.heading=Account not confirmed +screen.accountnotconfirmed.idplogin.message=The OSF account associated with the email has been registered but not confirmed. Our records show that this account was created via ORCiD login. Please check your email (and spam folder) for the confirmation link. If you believe this should not happen, please contact OSF Support. screen.accountdisabled.heading=Account disabled screen.accountdisabled.message=The OSF account associated with the email has been disabled. Please contact OSF Support to regain access. screen.shouldnothappen.heading=Account not active diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/spring-configuration/dataSource.xml b/cas-server-webapp/src/main/webapp/WEB-INF/spring-configuration/dataSource.xml index 5a03b889..518105bb 100644 --- a/cas-server-webapp/src/main/webapp/WEB-INF/spring-configuration/dataSource.xml +++ b/cas-server-webapp/src/main/webapp/WEB-INF/spring-configuration/dataSource.xml @@ -69,9 +69,6 @@ - - - diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casAccountNotConfirmedIdPLoginView.jsp b/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casAccountNotConfirmedIdPLoginView.jsp new file mode 100644 index 00000000..37d9237d --- /dev/null +++ b/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casAccountNotConfirmedIdPLoginView.jsp @@ -0,0 +1,41 @@ +<%-- + + 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. + +--%> + +<%-- Login exception page: account created via external IdP login but not confirmed --%> + + + +
+

+

+
+ + + + + + + + + + diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casLoginNotAllowedView.jsp b/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casAccountNotConfirmedOsfLoginView.jsp similarity index 80% rename from cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casLoginNotAllowedView.jsp rename to cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casAccountNotConfirmedOsfLoginView.jsp index d5fa4844..2410a66e 100644 --- a/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casLoginNotAllowedView.jsp +++ b/cas-server-webapp/src/main/webapp/WEB-INF/view/jsp/default/ui/casAccountNotConfirmedOsfLoginView.jsp @@ -1,6 +1,6 @@ <%-- - Copyright (c) 2015. Center for Open Science + 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. @@ -16,19 +16,19 @@ --%> -<%-- Login exception page: account not confirmed --%> +<%-- Login exception page: account created via OSF email-password sign-up but not confirmed --%>
-

-

+

+



diff --git a/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml b/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml index 669a64cb..9c819409 100644 --- a/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml +++ b/cas-server-webapp/src/main/webapp/WEB-INF/webflow/login/login-webflow.xml @@ -204,7 +204,8 @@ - + + @@ -282,7 +283,8 @@ - + + diff --git a/docs/example-logs-for-common-errors.md b/docs/example-logs-for-common-errors.md new file mode 100644 index 00000000..47010fe2 --- /dev/null +++ b/docs/example-logs-for-common-errors.md @@ -0,0 +1,99 @@ +# Example Logs for Common CAS / Shibboleth Errors + +## Apache Shibboleth + +### Metadata File + +* General Metadata Error + +Any metadata related issue will lead to the following `CRIT` level logs. + +``` +CRIT OpenSAML.Metadata.Chaining : failure initializing MetadataProvider: XML error(s) during parsing, check log for specifics +CRIT OpenSAML.Metadata.Chaining : failure initializing MetadataProvider: XML error(s) during parsing, check log for specifics +``` + +* Online Resource Failure + +This error happens usually due to a typo in the URL or temporary network issues. It is rare that the resource is not available. + +``` +ERROR XMLTooling.ParserPool : fatal error on line 602394, column 20, message: unable to connect socket for URL 'http://md.incommon.org/InCommon/InCommon-metadata.xml' +ERROR XMLTooling.ParserPool : fatal error on line 602394, column 20, message: unable to connect socket for URL 'http://md.incommon.org/InCommon/InCommon-metadata.xml' +ERROR OpenSAML.Metadata.XML : error while loading resource (http://md.incommon.org/InCommon/InCommon-metadata.xml): XML error(s) during parsing, check log for specifics +ERROR OpenSAML.Metadata.XML : error while loading resource (http://md.incommon.org/InCommon/InCommon-metadata.xml): XML error(s) during parsing, check log for specifics +``` + +* Backup Resource or Local Resource Failure + +For online resource, this happens when there is no back-up metadata file. For locally stored resource, this happens when the metadata file path is incorrect. + +``` +ERROR XMLTooling.ParserPool : fatal error on line 0, column 0, message: unable to open primary document entity '/var/cache/shibboleth/incommon-full-metadata.xml' +ERROR XMLTooling.ParserPool : fatal error on line 0, column 0, message: unable to open primary document entity '/var/cache/shibboleth/incommon-full-metadata.xml' +ERROR OpenSAML.Metadata.XML : error while loading resource (/var/cache/shibboleth/incommon-full-metadata.xml): XML error(s) during parsing, check log for specifics +ERROR OpenSAML.Metadata.XML : error while loading resource (/var/cache/shibboleth/incommon-full-metadata.xml): XML error(s) during parsing, check log for specifics +``` + +* Metadata XML Parsing Failure + +It is very rare that a metadata file provided by InCommon or institution IdPs has syntax issues by itself. However, a typo or mis-indentation can happen when the file content is added to the Helm Charts. + +``` +ERROR XMLTooling.ParserPool : fatal error on line 14, column 20, message: unterminated end tag 'Extension' +ERROR XMLTooling.ParserPool : fatal error on line 14, column 20, message: unterminated end tag 'Extension' +ERROR OpenSAML.Metadata.XML : error while loading resource (/etc/shibboleth/example-idp-metadata.xml): XML error(s) during parsing, check log for specifics +ERROR OpenSAML.Metadata.XML : error while loading resource (/etc/shibboleth/example-idp-metadata.xml): XML error(s) during parsing, check log for specifics +``` + +### Attribute Mapping + +* General Attribute Mapping Error + +Any attribute mapping related issue will lead to the following `CRIT` level logs. + +``` +CRIT Shibboleth.Application : error building AttributeExtractor: XML error(s) during parsing, check log for specifics +CRIT Shibboleth.Application : error building AttributeExtractor: XML error(s) during parsing, check log for specifics +``` + +* Attribute Mapping XML Paring Failure + +This error can happen if we accidentally break the XML syntax of `attribute-map.xml` when adding new mappings. + +``` +ERROR XMLTooling.ParserPool : fatal error on line 181, column 98, message: attribute 'name' is already specified for element 'Attribute' +ERROR XMLTooling.ParserPool : fatal error on line 181, column 98, message: attribute 'name' is already specified for element 'Attribute' +ERROR Shibboleth.AttributeExtractor.XML : error while loading resource (/etc/shibboleth/attribute-map.xml): XML error(s) during parsing, check log for specifics +ERROR Shibboleth.AttributeExtractor.XML : error while loading resource (/etc/shibboleth/attribute-map.xml): XML error(s) during parsing, check log for specifics +``` + +### Institution Login - SAML Response Parsing + +* TODO - Response Authentication Failures + +Currently, this is not available due to lack of failure examples; and it is super rare to happen at all. + +* Retrieve Attributes from SAML Response + +The following warning `removed value at position` and `removing attribute` is usually OK since institutions (IdP) may provide us (SP) with extra attributes that are not mapped. However, it is worthwhile to take a look upon login failures and check if required attributes are dropped such as `eppn`, `uid`, `mail`, `displayname`, `givenName`, `sn` etc. + +``` +WARN Shibboleth.AttributeFilter [3]: removed value at position (0) of attribute (eppn) from (login.example.edu) +WARN Shibboleth.AttributeFilter [3]: removed value at position (0) of attribute (eppn) from (login.example.edu) +WARN Shibboleth.AttributeFilter [3]: no values left, removing attribute (eppn) from (login.example.edu) +WARN Shibboleth.AttributeFilter [3]: no values left, removing attribute (eppn) from (login.example.edu) +INFO Shibboleth.SessionCache [3]: new session created: ID (_ba7b8c32d313e2686e1391f40751ee10) IdP (login.example.edu) Protocol(urn:oasis:names:tc:SAML:2.0:protocol) Address (172.17.0.1) +INFO Shibboleth-TRANSACTION [3]: New session (ID: _ba7b8c32d313e2686e1391f40751ee10) with (applicationId: default) for principal from (IdP: login.example.edu) at (ClientAddress: 172.17.0.1) with (NameIdentifier: username@login.example.edu) using (Protocol: urn:oasis:names:tc:SAML:2.0:protocol) from (AssertionID: _104078ef246775d75c4a96b82d5a05dc) +``` + +At least a few attributes must be cached here by Shibboleth for CAS: one for identity, one for email, and another one or two for name(s). Please see the **Jetty CAS** section for example errors when CAS fails to receive required attributes or receives incorrect ones. + +``` +INFO Shibboleth-TRANSACTION [3]: Cached the following attributes with session (ID: _ba7b8c32d313e2686e1391f40751ee10) for (applicationId: default) { +INFO Shibboleth-TRANSACTION [3]: mail (1 values) +INFO Shibboleth-TRANSACTION [3]: displayName (1 values) +INFO Shibboleth-TRANSACTION [3]: } +``` + +## Jetty CAS diff --git a/docs/osf-institutions-sso-via-cas.md b/docs/osf-institutions-sso-via-cas.md index 5bc4444b..05855f89 100644 --- a/docs/osf-institutions-sso-via-cas.md +++ b/docs/osf-institutions-sso-via-cas.md @@ -1,10 +1,10 @@ # Connecting to the Open Science Framework (OSF) via CAS-based Single Sign-On (SSO) -COS's CAS-based SSO has limited functionality since it is just an alternative for institutions that can not use the Shibboleth-based SSO. Before proceeding, please read [Connecting to the Open Science Framework (OSF) via Shibboleth-based Single Sign-On (SSO)](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-institutions-sso-via-saml.md) first for non-technical information on connecting to OSF via SSO. +COS's CAS-based SSO has limited functionality since it is just an alternative for institutions that can not use the Shibboleth-based SSO. Before proceeding, read [Connecting to the Open Science Framework (OSF) via Shibboleth-based Single Sign-On (SSO)](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-institutions-sso-via-saml.md) first for non-technical information on connecting to OSF via SSO. ## Technical Implementation -This SSO is based on [`cas-4.1.x`](https://github.com/apereo/cas/tree/4.1.x) and [`pac4j-1.7.x`](https://github.com/pac4j/pac4j/tree/1.7.x). Please refer to the [CAS protocol](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol.html) and the [complete specification](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html) for how CAS works. +This SSO is based on [`cas-4.1.x`](https://github.com/apereo/cas/tree/4.1.x) and [`pac4j-1.7.x`](https://github.com/pac4j/pac4j/tree/1.7.x). Refer to the [CAS protocol](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol.html) and the [complete specification](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html) for how CAS works. When connecting to the OSF via CAS-based SSO, COS's CAS system (OSF CAS) acts as the **CAS Client** and your institution's CAS system acts as the **CAS Server**. To implement and test SSO for your institution, please follow the steps below. @@ -25,11 +25,16 @@ Inform COS of the domain of your CAS system. More specifically, OSF CAS (as a cl ### Service Validation and Attribute Release -OSF CAS makes a `POST` request to your CAS system's [`/samlValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#42-samlvalidate-cas-30) endpoint for ticket validation and attribute release. Please release the following required attributes and inform us of the attribute name for each. +OSF CAS makes a `POST` request to your CAS system's [`/samlValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#42-samlvalidate-cas-30) endpoint for ticket validation and attribute release. Release the following required attributes (optional ones are highly recommended if possible) and inform us of the attribute name for each. -* Unique identifier for the user (e.g. `eppn`) -* User's institutional email (e.g. `mail`) -* User's full name (e.g. `displayName`) +* Required + * Unique identifier for the user (e.g. `eppn`) + * User's institutional email (e.g. `mail`) + * User's full name (e.g. `displayName`) +* Optional + * User's first and last name (e.g. a pair of `givenName` and `sn`) + * User's department(s) at your institution (e.g. `eduPersonOrgUnitDN` or `eduPersonPrimaryOrgUnitDN`) + * User's relationship(s) (e.g. student, faculty, staff, alum, etc.) to the institution (e.g. `eduPersonAffiliation` or `eduPersonPrimaryAffiliation`) Please note that OSF CAS can not use your [`/p3/serviceValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#28-p3servicevalidate-cas-30) endpoint due to an old version of the library it uses, namely [`pac4j-1.7.x`](https://github.com/pac4j/pac4j/tree/1.7.x) and [`cas-server-support-pac4j-1.7.x`](https://github.com/apereo/cas/tree/4.1.x/cas-server-support-pac4j). In addition, OSF CAS does not use your [`/validate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#24-validate-cas-10) and [`/serviceValidate`](https://apereo.github.io/cas/4.1.x/protocol/CAS-Protocol-Specification.html#25-servicevalidate-cas-20) endpoints since these two can not release required attributes. diff --git a/docs/osf-institutions-sso-via-saml.md b/docs/osf-institutions-sso-via-saml.md index c127ab79..4150a362 100644 --- a/docs/osf-institutions-sso-via-saml.md +++ b/docs/osf-institutions-sso-via-saml.md @@ -20,10 +20,14 @@ Any organization that has implemented a SAML 2.0 Identity Provider (IdP) and sig ### InCommon Research & Scholarship Institutions -COS is an [Research & Scholarship Entity Category (R&S)](https://refeds.org/category/research-and-scholarship) Service Provider (SP) registered by the [InCommon Federation](https://www.incommon.org/federation/). +COS is a [Research & Scholarship Entity Category (R&S)](https://refeds.org/category/research-and-scholarship) Service Provider (SP) registered by the [InCommon Federation](https://www.incommon.org/federation/). * Entity ID: `https://accounts.osf.io/shibboleth` -* Requested Attributes: `eduPersonPrincipalName` (SAML2), `mail` (SAML2) and `displayName` (SAML2) +* Required Attributes: `eduPersonPrincipalName` (SAML2), `mail` (SAML2) and `displayName` (SAML2) +* Optional Attributes: + * `givenName` and `sn` pair which specifies the user's given name and surname + * `eduPersonOrgUnitDN` or `eduPersonPrimaryOrgUnitDN` which specifies the person's Organizational Unit(s) (i.e. the department(s)) + * `eduPersonAffiliation` or `eduPersonPrimaryAffiliation` which specifies the person's relationship(s) to the institution in broad categories such as student, faculty, staff, alum, etc. Full technical details can be found at https://www.incommon.org/federation/research-scholarship-adopters/. @@ -37,19 +41,26 @@ COS offers a Service Provider (SP) based on [SAML 2.0](https://docs.oasis-open.o * Production: https://accounts.osf.io/Shibboleth.sso/Metadata * Test and/or staging: https://accounts.test.osf.io/Shibboleth.sso/Metadata -* Ensure that your IT administrators are releasing the three required pieces of information listed below and inform COS of the attributes you use for each of them. - * Unique identifier for the user (e.g. `eppn`) - * User's institutional email (e.g. `mail`) - * User's full name (e.g. `displayName` or **a pair of** `givenName` and `sn`) +* Ensure that your IT administrators are releasing the three required pieces of information listed below. Optional ones are highly recommended if possible. Inform COS of the attributes you use for each of them. + * Required + * Unique identifier for the user (e.g. `eppn`) + * User's institutional email (e.g. `mail`) + * User's full name (e.g. `displayName`) + * Optional + * User's first and last name (e.g. a pair of `givenName` and `sn`) + * User's department(s) at your institution (e.g. `eduPersonOrgUnitDN` or `eduPersonPrimaryOrgUnitDN`) + * User's relationship(s) (e.g. student, faculty, staff, alum, etc.) to the institution (e.g. `eduPersonAffiliation` or `eduPersonPrimaryAffiliation`) +* Provide COS with IdP metadata for your test / stage (if available) and prod servers. A URL to the metadata is preferred over an XML file so that our SP server can periodically reload and refresh the metadata. + +* It is recommended that a temporary institution test account can be created for COS engineers if possible, which will significantly aid and accelerate the process. ### For All Institutions Inform COS of the user you would like to test with; your COS contact will ensure your account is ready to go and will send you a link to test the SSO configuration setup for your institution. - ## Alternative SSO Options -COS strongly recommends using this Shibboleth-based SSO when connecting to the OSF. However, if this is not available at your institution, please inform COS of alternative SSO options you have. We may support them in the future. +COS strongly recommends using this Shibboleth-based SSO when connecting to the OSF. However, if this is not available at your institution, inform COS of alternative SSO options you have. We may support them in the future. One alternative that COS currently supports is the CAS-based SSO, please refer to [Connecting to the Open Science Framework (OSF) via CAS-based Single Sign-On (SSO)](https://github.com/CenterForOpenScience/cas-overlay/blob/develop/docs/osf-institutions-sso-via-cas.md) for technical details. diff --git a/etc/cas.properties b/etc/cas.properties index e578b977..207985ff 100644 --- a/etc/cas.properties +++ b/etc/cas.properties @@ -99,7 +99,7 @@ osf.database.driverClass=org.postgresql.Driver osf.database.url=jdbc:postgresql://192.168.168.167:5432/osf?targetServerType=master osf.database.user=postgres osf.database.password= -osf.database.hibernate.dialect=org.hibernate.dialect.PostgreSQL82Dialect +osf.database.hibernate.dialect=io.cos.cas.adaptors.postgres.hibernate.OSFPostgreSQLDialect ## # OAuth Provider