keycloak-aplcache
Changes
core/src/main/java/org/keycloak/TokenVerifier.java 284(+224 -60)
Details
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
new file mode 100644
index 0000000..2253f5e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.exceptions;
+
+import org.keycloak.common.VerificationException;
+
+/**
+ * Exception thrown for cases when token is invalid due to time constraints (expired, or not yet valid).
+ * Cf. {@link JsonWebToken#isActive()}.
+ * @author hmlnarik
+ */
+public class TokenNotActiveException extends VerificationException {
+
+ public TokenNotActiveException() {
+ }
+
+ public TokenNotActiveException(String message) {
+ super(message);
+ }
+
+ public TokenNotActiveException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TokenNotActiveException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
new file mode 100644
index 0000000..13225fa
--- /dev/null
+++ b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.exceptions;
+
+import org.keycloak.common.VerificationException;
+
+/**
+ * Thrown when token signature is invalid.
+ * @author hmlnarik
+ */
+public class TokenSignatureInvalidException extends VerificationException {
+
+ public TokenSignatureInvalidException() {
+ }
+
+ public TokenSignatureInvalidException(String message) {
+ super(message);
+ }
+
+ public TokenSignatureInvalidException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public TokenSignatureInvalidException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java
index db8fc5a..653f205 100755
--- a/core/src/main/java/org/keycloak/RSATokenVerifier.java
+++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java
@@ -29,10 +29,10 @@ import java.security.PublicKey;
*/
public class RSATokenVerifier {
- private TokenVerifier tokenVerifier;
+ private final TokenVerifier<AccessToken> tokenVerifier;
private RSATokenVerifier(String tokenString) {
- this.tokenVerifier = TokenVerifier.create(tokenString);
+ this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class);
}
public static RSATokenVerifier create(String tokenString) {
core/src/main/java/org/keycloak/TokenVerifier.java 284(+224 -60)
diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java
index 9c30bfd..6bfcb3b 100755
--- a/core/src/main/java/org/keycloak/TokenVerifier.java
+++ b/core/src/main/java/org/keycloak/TokenVerifier.java
@@ -18,7 +18,8 @@
package org.keycloak;
import org.keycloak.common.VerificationException;
-import org.keycloak.jose.jws.Algorithm;
+import org.keycloak.exceptions.TokenNotActiveException;
+import org.keycloak.exceptions.TokenSignatureInvalidException;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
@@ -26,67 +27,235 @@ import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.TokenUtil;
import javax.crypto.SecretKey;
import java.security.PublicKey;
+import java.util.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
-public class TokenVerifier {
+public class TokenVerifier<T extends JsonWebToken> {
+
+ // This interface is here as JDK 7 is a requirement for this project.
+ // Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead.
+
+ // @FunctionalInterface
+ public static interface Predicate<T extends JsonWebToken> {
+ /**
+ * Performs a single check on the given token verifier.
+ * @param t Token, guaranteed to be non-null.
+ * @return
+ * @throws VerificationException
+ */
+ boolean test(T t) throws VerificationException;
+ }
+
+ public static final Predicate<JsonWebToken> SUBJECT_EXISTS_CHECK = new Predicate<JsonWebToken>() {
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ String subject = t.getSubject();
+ if (subject == null) {
+ throw new VerificationException("Subject missing in token");
+ }
+
+ return true;
+ }
+ };
+
+ public static final Predicate<JsonWebToken> IS_ACTIVE = new Predicate<JsonWebToken>() {
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (! t.isActive()) {
+ throw new TokenNotActiveException("Token is not active");
+ }
+
+ return true;
+ }
+ };
+
+ public static class RealmUrlCheck implements Predicate<JsonWebToken> {
+
+ private static final RealmUrlCheck NULL_INSTANCE = new RealmUrlCheck(null);
+
+ private final String realmUrl;
+
+ public RealmUrlCheck(String realmUrl) {
+ this.realmUrl = realmUrl;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (this.realmUrl == null) {
+ throw new VerificationException("Realm URL not set");
+ }
+
+ if (! this.realmUrl.equals(t.getIssuer())) {
+ throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
+ }
+
+ return true;
+ }
+ };
- private final String tokenString;
+ public static class TokenTypeCheck implements Predicate<JsonWebToken> {
+
+ private static final TokenTypeCheck INSTANCE_BEARER = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_BEARER);
+
+ private final String tokenType;
+
+ public TokenTypeCheck(String tokenType) {
+ this.tokenType = tokenType;
+ }
+
+ @Override
+ public boolean test(JsonWebToken t) throws VerificationException {
+ if (! tokenType.equalsIgnoreCase(t.getType())) {
+ throw new VerificationException("Token type is incorrect. Expected '" + tokenType + "' but was '" + t.getType() + "'");
+ }
+ return true;
+ }
+ };
+
+ private String tokenString;
+ private Class<? extends T> clazz;
private PublicKey publicKey;
private SecretKey secretKey;
private String realmUrl;
+ private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER;
private boolean checkTokenType = true;
- private boolean checkActive = true;
private boolean checkRealmUrl = true;
+ private final LinkedList<Predicate<? super T>> checks = new LinkedList<>();
private JWSInput jws;
- private AccessToken token;
+ private T token;
- protected TokenVerifier(String tokenString) {
+ protected TokenVerifier(String tokenString, Class<T> clazz) {
this.tokenString = tokenString;
+ this.clazz = clazz;
+ }
+
+ protected TokenVerifier(T token) {
+ this.token = token;
+ }
+
+ /**
+ * Creates a {@code TokenVerifier<AccessToken> instance. The method is here for backwards compatibility.
+ * @param tokenString
+ * @return
+ * @deprecated use {@link #create(java.lang.String, java.lang.Class) } instead
+ */
+ public static TokenVerifier<AccessToken> create(String tokenString) {
+ return create(tokenString, AccessToken.class);
+ }
+
+ public static <T extends JsonWebToken> TokenVerifier<T> create(String tokenString, Class<T> clazz) {
+ return new TokenVerifier(tokenString, clazz)
+ .check(RealmUrlCheck.NULL_INSTANCE)
+ .check(SUBJECT_EXISTS_CHECK)
+ .check(TokenTypeCheck.INSTANCE_BEARER)
+ .check(IS_ACTIVE);
+ }
+
+ public static <T extends JsonWebToken> TokenVerifier<T> from(T token) {
+ return new TokenVerifier(token)
+ .check(RealmUrlCheck.NULL_INSTANCE)
+ .check(SUBJECT_EXISTS_CHECK)
+ .check(TokenTypeCheck.INSTANCE_BEARER)
+ .check(IS_ACTIVE);
+ }
+
+ private void removeCheck(Class<? extends Predicate<?>> checkClass) {
+ for (Iterator<Predicate<? super T>> it = checks.iterator(); it.hasNext();) {
+ if (it.next().getClass() == checkClass) {
+ it.remove();
+ }
+ }
}
- public static TokenVerifier create(String tokenString) {
- return new TokenVerifier(tokenString);
+ private void removeCheck(Predicate<? super T> check) {
+ checks.remove(check);
}
- public TokenVerifier publicKey(PublicKey publicKey) {
+ private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Class<? extends Predicate<?>> checkClass, boolean active, P predicate) {
+ removeCheck(checkClass);
+ if (active) {
+ checks.add(predicate);
+ }
+ return this;
+ }
+
+ private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Predicate<? super T> check, boolean active, P predicate) {
+ removeCheck(check);
+ if (active) {
+ checks.add(predicate);
+ }
+ return this;
+ }
+
+ /**
+ * Resets all preset checks and will test the given checks in {@link #verify()} method.
+ * @param checks
+ * @return
+ */
+ public TokenVerifier<T> checkOnly(Predicate<? super T>... checks) {
+ this.checks.clear();
+ if (checks != null) {
+ this.checks.addAll(Arrays.asList(checks));
+ }
+ return this;
+ }
+
+ /**
+ * Will test the given checks in {@link #verify()} method in addition to already set checks.
+ * @param checks
+ * @return
+ */
+ public TokenVerifier<T> check(Predicate<? super T>... checks) {
+ if (checks != null) {
+ this.checks.addAll(Arrays.asList(checks));
+ }
+ return this;
+ }
+
+ public TokenVerifier<T> publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
- public TokenVerifier secretKey(SecretKey secretKey) {
+ public TokenVerifier<T> secretKey(SecretKey secretKey) {
this.secretKey = secretKey;
return this;
}
- public TokenVerifier realmUrl(String realmUrl) {
+ public TokenVerifier<T> realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
- return this;
+ return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl));
}
- public TokenVerifier checkTokenType(boolean checkTokenType) {
+ public TokenVerifier<T> checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
- return this;
+ return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
}
- public TokenVerifier checkActive(boolean checkActive) {
- this.checkActive = checkActive;
- return this;
+ public TokenVerifier<T> tokenType(String tokenType) {
+ this.expectedTokenType = tokenType;
+ return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
}
- public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
+ public TokenVerifier<T> checkActive(boolean checkActive) {
+ return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE);
+ }
+
+ public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
- return this;
+ return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
}
- public TokenVerifier parse() throws VerificationException {
+ public TokenVerifier<T> parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
@@ -100,7 +269,7 @@ public class TokenVerifier {
try {
- token = jws.readJsonContent(AccessToken.class);
+ token = jws.readJsonContent(clazz);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
@@ -108,8 +277,10 @@ public class TokenVerifier {
return this;
}
- public AccessToken getToken() throws VerificationException {
- parse();
+ public T getToken() throws VerificationException {
+ if (token == null) {
+ parse();
+ }
return token;
}
@@ -118,50 +289,43 @@ public class TokenVerifier {
return jws.getHeader();
}
- public TokenVerifier verify() throws VerificationException {
- parse();
-
- if (checkRealmUrl && realmUrl == null) {
- throw new VerificationException("Realm URL not set");
- }
-
+ public void verifySignature() throws VerificationException {
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
- if (AlgorithmType.RSA.equals(algorithmType)) {
- if (publicKey == null) {
- throw new VerificationException("Public key not set");
- }
-
- if (!RSAProvider.verify(jws, publicKey)) {
- throw new VerificationException("Invalid token signature");
- }
- } else if (AlgorithmType.HMAC.equals(algorithmType)) {
- if (secretKey == null) {
- throw new VerificationException("Secret key not set");
- }
-
- if (!HMACProvider.verify(jws, secretKey)) {
- throw new VerificationException("Invalid token signature");
- }
- } else {
- throw new VerificationException("Unknown or unsupported token algorith");
- }
-
- String user = token.getSubject();
- if (user == null) {
- throw new VerificationException("Subject missing in token");
+ if (null == algorithmType) {
+ throw new VerificationException("Unknown or unsupported token algorithm");
+ } else switch (algorithmType) {
+ case RSA:
+ if (publicKey == null) {
+ throw new VerificationException("Public key not set");
+ }
+ if (!RSAProvider.verify(jws, publicKey)) {
+ throw new TokenSignatureInvalidException("Invalid token signature");
+ } break;
+ case HMAC:
+ if (secretKey == null) {
+ throw new VerificationException("Secret key not set");
+ }
+ if (!HMACProvider.verify(jws, secretKey)) {
+ throw new TokenSignatureInvalidException("Invalid token signature");
+ } break;
+ default:
+ throw new VerificationException("Unknown or unsupported token algorithm");
}
+ }
- if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
- throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
+ public TokenVerifier<T> verify() throws VerificationException {
+ if (getToken() == null) {
+ parse();
}
-
- if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
- throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
+ if (jws != null) {
+ verifySignature();
}
- if (checkActive && !token.isActive()) {
- throw new VerificationException("Token is not active");
+ for (Predicate<? super T> check : checks) {
+ if (! check.test(getToken())) {
+ throw new VerificationException("JWT check failed for check " + check);
+ }
}
return this;
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
index e74fa20..8f21ddf 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java
@@ -19,33 +19,27 @@ package org.keycloak.authentication.authenticators.resetcred;
import org.jboss.logging.Logger;
import org.keycloak.Config;
-import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.AuthenticationFlowError;
-import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.*;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.common.VerificationException;
+import org.keycloak.common.util.Time;
+import org.keycloak.credential.*;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.models.AuthenticationExecutionModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
-import org.keycloak.models.utils.HmacOTP;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
+import java.util.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
-import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@@ -53,9 +47,6 @@ import java.util.concurrent.TimeUnit;
* @version $Revision: 1 $
*/
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
- public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
-
- private static final Logger logger = Logger.getLogger(ResetCredentialEmail.class);
public static final String PROVIDER_ID = "reset-credential-email";
@@ -85,15 +76,25 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return;
}
- // We send the secret in the email in a link as a query param. We don't need to sign it or anything because
- // it can only be guessed once, and it must match watch is stored in the client session.
- String secret = HmacOTP.generateSecret(10);
- context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
- String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString();
- long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
+ int validityInSecs = context.getRealm().getAccessCodeLifespanUserAction();
+ int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
+
+ PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
+ CredentialModel password = passwordProvider.getPassword(context.getRealm(), user);
+ Long lastCreatedPassword = password == null ? null : password.getCreatedDate();
+
+ // We send the secret in the email in a link as a query param.
+ ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, null, lastCreatedPassword, context.getClientSession());
+ KeycloakSession keycloakSession = context.getSession();
+ String link = UriBuilder
+ .fromUri(context.getActionUrl())
+ .queryParam(Constants.KEY, token.serialize(keycloakSession, context.getRealm(), context.getUriInfo()))
+ .build()
+ .toString();
+ long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
try {
- context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration);
+ context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
event.clone().event(EventType.SEND_RESET_PASSWORD)
.user(user)
.detail(Details.USERNAME, username)
@@ -114,19 +115,56 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
@Override
public void action(AuthenticationFlowContext context) {
- /*String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
- String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
+ /*
+ KeycloakSession keycloakSession = context.getSession();
+ String actionTokenString = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
+ ResetCredentialsActionToken tokenFromMail = null;
+ try {
+ tokenFromMail = ResetCredentialsActionToken.deserialize(keycloakSession, context.getRealm(), context.getUriInfo(), actionTokenString);
+ } catch (VerificationException ex) {
+ context.getEvent().detail(Details.REASON, ex.getMessage()).error(Errors.INVALID_CODE);
+ Response challenge = context.form()
+ .setError(Messages.INVALID_CODE)
+ .createErrorPage();
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
+ }
+
+ String userId = tokenFromMail == null ? null : tokenFromMail.getUserId();
- // Can only guess once! We remove the note so another guess can't happen
- context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
- if (secret == null || key == null || !secret.equals(key)) {
- context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
+ if (tokenFromMail == null) {
+ context.getEvent()
+ .error(Errors.INVALID_CODE);
Response challenge = context.form()
- .setError(Messages.INVALID_ACCESS_CODE)
+ .setError(Messages.INVALID_CODE)
.createErrorPage();
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
return;
}
+
+ PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
+ CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser());
+
+ Long lastCreatedPasswordMail = tokenFromMail.getLastChangedPasswordTimestamp();
+ Long lastCreatedPasswordFromStore = password == null ? null : password.getCreatedDate();
+
+ String clientSessionId = tokenFromMail.getClientSessionId();
+ ClientSessionModel clientSession = clientSessionId == null ? null : keycloakSession.sessions().getClientSession(clientSessionId);
+
+ if (clientSession == null
+ || ! Objects.equals(lastCreatedPasswordMail, lastCreatedPasswordFromStore)
+ || ! Objects.equals(userId, context.getUser().getId())) {
+ context.getEvent()
+ .user(userId)
+ .detail(Details.USERNAME, context.getUser().getUsername())
+ .detail(Details.TOKEN_ID, tokenFromMail.getId())
+ .error(Errors.EXPIRED_CODE);
+ Response challenge = context.form()
+ .setError(Messages.INVALID_CODE)
+ .createErrorPage();
+ context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
+ return;
+ }
+
// We now know email is valid, so set it to valid.
context.getUser().setEmailVerified(true);
context.success();*/
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultActionToken.java b/services/src/main/java/org/keycloak/authentication/DefaultActionToken.java
new file mode 100644
index 0000000..8c51d1f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/DefaultActionToken.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.authentication;
+
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.common.VerificationException;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.*;
+
+/**
+ * Part of action token that is intended to be used e.g. in link sent in password-reset email.
+ * The token encapsulates user, expected action and its time of expiry.
+ *
+ * @author hmlnarik
+ */
+public class DefaultActionToken extends DefaultActionTokenKey {
+
+ public static final String JSON_FIELD_ACTION_VERIFICATION_NONCE = "nonce";
+
+ public static Predicate<DefaultActionToken> ACTION_TOKEN_BASIC_CHECKS = t -> {
+ if (t.getActionVerificationNonce() == null) {
+ throw new VerificationException("Nonce not present.");
+ }
+
+ return true;
+ };
+
+ /**
+ * Single-use random value used for verification whether the relevant action is allowed.
+ */
+ @JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true)
+ private final UUID actionVerificationNonce;
+
+ public DefaultActionToken(String userId, String actionId, int expirationInSecs) {
+ this(userId, actionId, expirationInSecs, UUID.randomUUID());
+ }
+
+ /**
+ *
+ * @param userId User ID
+ * @param actionId Action ID
+ * @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
+ * @param actionVerificationNonce
+ */
+ protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
+ super(userId, actionId);
+ this.actionVerificationNonce = actionVerificationNonce == null ? UUID.randomUUID() : actionVerificationNonce;
+ expiration = absoluteExpirationInSecs;
+ }
+
+ public UUID getActionVerificationNonce() {
+ return actionVerificationNonce;
+ }
+
+ @JsonIgnore
+ public Map<String, String> getNotes() {
+ Map<String, String> res = new HashMap<>();
+ return res;
+ }
+
+ public String getNote(String name) {
+ Object res = getOtherClaims().get(name);
+ return res instanceof String ? (String) res : null;
+ }
+
+ /**
+ * Sets value of the given note
+ * @return original value (or {@code null} when no value was present)
+ */
+ public final String setNote(String name, String value) {
+ Object res = value == null
+ ? getOtherClaims().remove(name)
+ : getOtherClaims().put(name, value);
+ return res instanceof String ? (String) res : null;
+ }
+
+ /**
+ * Removes given note, and returns original value (or {@code null} when no value was present)
+ * @return see description
+ */
+ public final String removeNote(String name) {
+ Object res = getOtherClaims().remove(name);
+ return res instanceof String ? (String) res : null;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/DefaultActionTokenKey.java
new file mode 100644
index 0000000..f9d44db
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/DefaultActionTokenKey.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.authentication;
+
+import org.keycloak.representations.JsonWebToken;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class DefaultActionTokenKey extends JsonWebToken {
+
+ public DefaultActionTokenKey(String userId, String actionId) {
+ subject = userId;
+ type = actionId;
+ }
+
+ @JsonIgnore
+ public String getUserId() {
+ return getSubject();
+ }
+
+ @JsonIgnore
+ public String getActionId() {
+ return getType();
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java b/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java
new file mode 100644
index 0000000..ef92770
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.authentication;
+
+import org.keycloak.TokenVerifier;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.common.VerificationException;
+import org.keycloak.common.util.Time;
+import org.keycloak.jose.jws.*;
+import org.keycloak.models.*;
+import org.keycloak.services.Urls;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
+import java.util.UUID;
+import javax.ws.rs.core.UriInfo;
+import org.jboss.logging.Logger;
+
+/**
+ * Representation of a token that represents a time-limited reset credentials action.
+ * <p>
+ * This implementation handles signature.
+ *
+ * @author hmlnarik
+ */
+public class ResetCredentialsActionToken extends DefaultActionToken {
+
+ private static final Logger LOG = Logger.getLogger(ResetCredentialsActionToken.class);
+
+ private static final String RESET_CREDENTIALS_ACTION = "reset-credentials";
+ public static final String NOTE_CLIENT_SESSION_ID = "clientSessionId";
+ private static final String JSON_FIELD_CLIENT_SESSION_ID = "csid";
+ private static final String JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP = "lcpt";
+
+ @JsonIgnore
+ private ClientSessionModel clientSession;
+
+ @JsonProperty(value = JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP)
+ private Long lastChangedPasswordTimestamp;
+
+ public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, String clientSessionId) {
+ super(userId, RESET_CREDENTIALS_ACTION, absoluteExpirationInSecs, actionVerificationNonce);
+ setNote(NOTE_CLIENT_SESSION_ID, clientSessionId);
+ this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
+ }
+
+ public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, ClientSessionModel clientSession) {
+ this(userId, absoluteExpirationInSecs, actionVerificationNonce, lastChangedPasswordTimestamp, clientSession == null ? null : clientSession.getId());
+ this.clientSession = clientSession;
+ }
+
+ private ResetCredentialsActionToken() {
+ super(null, null, -1, null);
+ }
+
+ public ClientSessionModel getClientSession() {
+ return this.clientSession;
+ }
+
+ public void setClientSession(ClientSessionModel clientSession) {
+ this.clientSession = clientSession;
+ setClientSessionId(clientSession == null ? null : clientSession.getId());
+ }
+
+ @JsonProperty(value = JSON_FIELD_CLIENT_SESSION_ID)
+ public String getClientSessionId() {
+ return getNote(NOTE_CLIENT_SESSION_ID);
+ }
+
+ public void setClientSessionId(String clientSessionId) {
+ setNote(NOTE_CLIENT_SESSION_ID, clientSessionId);
+ }
+
+ public Long getLastChangedPasswordTimestamp() {
+ return lastChangedPasswordTimestamp;
+ }
+
+ public void setLastChangedPasswordTimestamp(Long lastChangedPasswordTimestamp) {
+ this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
+ }
+
+ @Override
+ @JsonIgnore
+ public Map<String, String> getNotes() {
+ Map<String, String> res = super.getNotes();
+ if (this.clientSession != null) {
+ res.put(NOTE_CLIENT_SESSION_ID, getNote(NOTE_CLIENT_SESSION_ID));
+ }
+ return res;
+ }
+
+ public String serialize(KeycloakSession session, RealmModel realm, UriInfo uri) {
+ String issuerUri = getIssuer(realm, uri);
+ KeyManager.ActiveHmacKey keys = session.keys().getActiveHmacKey(realm);
+
+ this
+ .issuedAt(Time.currentTime())
+ .id(getActionVerificationNonce().toString())
+ .issuer(issuerUri)
+ .audience(issuerUri);
+
+ return new JWSBuilder()
+ .kid(keys.getKid())
+ .jsonContent(this)
+ .hmac512(keys.getSecretKey());
+ }
+
+ private static String getIssuer(RealmModel realm, UriInfo uri) {
+ return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
+ }
+
+ /**
+ * Returns a {@code DefaultActionToken} instance decoded from the given string. If decoding fails, returns {@code null}
+ *
+ * @param session
+ * @param actionTokenString
+ * @return
+ */
+ public static ResetCredentialsActionToken deserialize(KeycloakSession session, RealmModel realm, UriInfo uri, String token,
+ Predicate<? super ResetCredentialsActionToken>... checks) throws VerificationException {
+ return TokenVerifier.create(token, ResetCredentialsActionToken.class)
+ .secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
+ .realmUrl(getIssuer(realm, uri))
+ .tokenType(RESET_CREDENTIALS_ACTION)
+
+ .checkActive(false) // TODO: If this line is omitted, the following tests in ResetPasswordTest fail: resetPasswordExpiredCodeShort, resetPasswordExpiredCode
+
+ .check(ACTION_TOKEN_BASIC_CHECKS)
+ .check(checks)
+ .verify()
+ .getToken()
+ ;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
index 4fda889..109f2c9 100644
--- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
+++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java
@@ -154,6 +154,11 @@ public class RestartLoginCookie {
// TODO:mposolda
/*
public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception {
+ String[] parts = code.split("\\.");
+ return restartSessionByClientSession(session, realm, parts[1]);
+ }
+
+ public static ClientSessionModel restartSessionByClientSession(KeycloakSession session, RealmModel realm, String clientSessionId) throws Exception {
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
if (cook == null) {
logger.debug("KC_RESTART cookie doesn't exist");
@@ -167,8 +172,6 @@ public class RestartLoginCookie {
return null;
}
RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class);
- String[] parts = code.split("\\.");
- String clientSessionId = parts[1];
if (!clientSessionId.equals(cookie.getClientSession())) {
logger.debug("RestartLoginCookie clientSession does not match code's clientSession");
return null;
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 3cb8c68..e460a1d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -127,7 +127,10 @@ public class AuthenticationManager {
if (cookie == null) return;
String tokenString = cookie.getValue();
- TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
+ TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
+ .realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()))
+ .checkActive(false)
+ .checkTokenType(false);
String kid = verifier.getHeader().getKeyId();
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
@@ -710,7 +713,7 @@ public class AuthenticationManager {
public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers) {
try {
- TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
+ TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
String kid = verifier.getHeader().getKeyId();
AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 3c9b40b..9633212 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -24,13 +24,13 @@ import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.TokenVerifier;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.ResetCredentialsActionToken;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
-import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
-import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
-import org.keycloak.authentication.requiredactions.VerifyEmail;
-import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
+import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@@ -48,7 +48,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol;
@@ -57,11 +56,14 @@ import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
+import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.ClientSessionCode.ActionType;
+import org.keycloak.services.managers.ClientSessionCode.ParseResult;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.CookieHelper;
@@ -84,6 +86,7 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.net.URI;
+import java.util.Objects;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -166,24 +169,51 @@ public class LoginActionsService {
}
}
+ private <C extends CommonClientSessionModel> SessionCodeChecks<C> checksForCode(String code, Class<C> expectedClazz) {
+ SessionCodeChecks<C> res = new SessionCodeChecks<>(code, expectedClazz);
+ res.initialVerifyCode();
+ return res;
+ }
+
+
- private class Checks {
- // TODO: Merge with Hynek's code. This may not be just loginSession
- ClientSessionCode<LoginSessionModel> clientCode;
+ private class SessionCodeChecks<C extends CommonClientSessionModel> {
+ ClientSessionCode<C> clientCode;
Response response;
- ClientSessionCode.ParseResult result;
+ ClientSessionCode.ParseResult<C> result;
+ Class<C> expectedClazz;
+
+ private final String code;
+
+ public SessionCodeChecks(String code, Class<C> expectedClazz) {
+ this.code = code;
+ this.expectedClazz = expectedClazz;
+ }
+
+ public C getClientSession() {
+ return clientCode == null ? null : clientCode.getClientSession();
+ }
+
+ public boolean passed() {
+ return response == null;
+ }
+
+ public boolean failed() {
+ return response != null;
+ }
- boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) {
- if (!verifyCode(code)) {
+
+ boolean verifyCode(String requiredAction, ClientSessionCode.ActionType actionType) {
+ if (failed()) {
return false;
}
+
if (!clientCode.isValidAction(requiredAction)) {
- LoginSessionModel loginSession = clientCode.getClientSession();
- if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(loginSession.getAction())) {
+ C clientSession = getClientSession();
+ if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
response = redirectToRequiredActions(code);
return false;
-
- } // TODO:mposolda
+ } // TODO:mposolda
/*else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
response = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN)
@@ -191,9 +221,9 @@ public class LoginActionsService {
return false;
}*/
}
- if (!isActionActive(actionType)) return false;
- return true;
- }
+
+ return isActionActive(actionType);
+ }
private boolean isValidAction(String requiredAction) {
if (!clientCode.isValidAction(requiredAction)) {
@@ -204,18 +234,19 @@ public class LoginActionsService {
}
private void invalidAction() {
- event.client(clientCode.getClientSession().getClient());
+ event.client(getClientSession().getClient());
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);
}
private boolean isActionActive(ClientSessionCode.ActionType actionType) {
if (!clientCode.isActionActive(actionType)) {
- event.client(clientCode.getClientSession().getClient());
+ event.client(getClientSession().getClient());
event.clone().error(Errors.EXPIRED_CODE);
- if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
- AuthenticationProcessor.resetFlow(clientCode.getClientSession());
- response = processAuthentication(null, clientCode.getClientSession(), Messages.LOGIN_TIMEOUT);
+ if (getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
+ LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
+ AuthenticationProcessor.resetFlow(loginSession);
+ response = processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT);
return false;
}
response = ErrorPage.error(session, Messages.EXPIRED_CODE);
@@ -224,7 +255,7 @@ public class LoginActionsService {
return true;
}
- public boolean verifyCode(String code) {
+ private boolean initialVerifyCode() {
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
@@ -235,14 +266,12 @@ public class LoginActionsService {
response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
return false;
}
-
- // TODO:mposolda it may not be just loginSessionModel
- result = ClientSessionCode.parseResult(code, session, realm, LoginSessionModel.class);
+ result = ClientSessionCode.parseResult(code, session, realm, expectedClazz);
clientCode = result.getCode();
if (clientCode == null) {
- // TODO:mposolda
- /*
- if (result.isLoginSessionNotFound()) { // timeout
+ if (result.isLoginSessionNotFound()) { // timeout or loginSession already logged
+ // TODO:mposolda
+ /*
try {
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
if (clientSession != null) {
@@ -252,13 +281,14 @@ public class LoginActionsService {
}
} catch (Exception e) {
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
- }
+ }*/
}
event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.INVALID_CODE);*/
+ response = ErrorPage.error(session, Messages.INVALID_CODE);
return false;
}
- LoginSessionModel clientSession = clientCode.getClientSession();
+
+ C clientSession = getClientSession();
if (clientSession == null) {
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);
@@ -269,62 +299,48 @@ public class LoginActionsService {
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
- session.loginSessions().removeLoginSession(realm, clientSession);
+ // TODO:mposolda
+ //session.sessions().removeClientSession(realm, clientSession);
return false;
}
if (!client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
- session.loginSessions().removeLoginSession(realm, clientSession);
+ // TODO:mposolda
+ //session.sessions().removeClientSession(realm, clientSession);
return false;
}
session.getContext().setClient(client);
return true;
}
- public boolean verifyRequiredAction(String code, String executedAction) {
- // TODO:mposolda
- /*
- if (!verifyCode(code)) {
+ public boolean verifyRequiredAction(String executedAction) {
+ if (failed()) {
return false;
}
+
if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false;
if (!isActionActive(ClientSessionCode.ActionType.USER)) return false;
- final ClientSessionModel clientSession = clientCode.getClientSession();
+ final LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
- final UserSessionModel userSession = clientSession.getUserSession();
- if (userSession == null) {
- ServicesLogger.LOGGER.userSessionNull();
- event.error(Errors.USER_SESSION_NOT_FOUND);
- throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE));
- }
- if (!AuthenticationManager.isSessionValid(realm, userSession)) {
- AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
- event.error(Errors.INVALID_CODE);
- response = ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
- return false;
- }
-
- if (executedAction == null && userSession != null) { // do next required action only if user is already authenticated
- initEvent(clientSession);
+ if (executedAction == null) { // do next required action only if user is already authenticated
+ initLoginEvent(loginSession);
event.event(EventType.LOGIN);
- response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
+ response = AuthenticationManager.nextActionAfterAuthentication(session, loginSession, clientConnection, request, uriInfo, event);
return false;
}
- if (!executedAction.equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
+ if (!executedAction.equals(loginSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
logger.debug("required action doesn't match current required action");
- clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
+ loginSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
response = redirectToRequiredActions(code);
return false;
- }*/
+ }
return true;
-
}
}
-
/**
* protocol independent login page entry point
*
@@ -341,8 +357,8 @@ public class LoginActionsService {
if (loginSession != null && code.equals(loginSession.getNote(LAST_PROCESSED_CODE))) {
// Allow refresh of previous page
} else {
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
+ if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
@@ -402,8 +418,8 @@ public class LoginActionsService {
return authenticate(code, null);
}
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
+ if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
final ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
@@ -422,6 +438,163 @@ public class LoginActionsService {
return null;
}
+ private boolean isSslUsed(JsonWebToken t) throws VerificationException {
+ if (! checkSsl()) {
+ event.error(Errors.SSL_REQUIRED);
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.HTTPS_REQUIRED));
+ }
+ return true;
+ }
+
+ private boolean isRealmEnabled(JsonWebToken t) throws VerificationException {
+ if (! realm.isEnabled()) {
+ event.error(Errors.REALM_DISABLED);
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.REALM_NOT_ENABLED));
+ }
+ return true;
+ }
+
+ private boolean isResetCredentialsAllowed(ResetCredentialsActionToken t) throws VerificationException {
+ if (!realm.isResetPasswordAllowed()) {
+ event.client(t.getClientSession().getClient());
+ event.error(Errors.NOT_ALLOWED);
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED));
+ }
+ return true;
+ }
+
+ private boolean canResolveClientSession(ResetCredentialsActionToken t) throws VerificationException {
+ // TODO:mposolda
+ /*
+ String clientSessionId = t == null ? null : t.getNote(ResetCredentialsActionToken.NOTE_CLIENT_SESSION_ID);
+
+ if (t == null || clientSessionId == null) {
+ event.error(Errors.INVALID_CODE);
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
+ }
+
+ ClientSessionModel clientSession = session.sessions().getClientSession(clientSessionId);
+ t.setClientSession(clientSession);
+
+ if (clientSession == null) { // timeout
+ try {
+ clientSession = RestartLoginCookie.restartSessionByClientSession(session, realm, clientSessionId);
+ } catch (Exception e) {
+ ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
+ }
+
+ if (clientSession != null) {
+ event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
+ throw new LoginActionsServiceException(processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor()));
+ }
+ }
+
+ if (clientSession == null) {
+ event.error(Errors.INVALID_CODE);
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
+ }
+
+ event.detail(Details.CODE_ID, clientSession.getId());*/
+
+ return true;
+ }
+
+ private boolean canResolveClient(ResetCredentialsActionToken t) throws VerificationException {
+ ClientModel client = t.getClientSession().getClient();
+ if (client == null) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ session.sessions().removeClientSession(realm, t.getClientSession());
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
+ }
+
+ if (! client.isEnabled()) {
+ event.error(Errors.CLIENT_NOT_FOUND);
+ session.sessions().removeClientSession(realm, t.getClientSession());
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
+ }
+ session.getContext().setClient(client);
+
+ return true;
+ }
+
+ private class IsValidAction implements Predicate<ResetCredentialsActionToken> {
+
+ private final String requiredAction;
+
+ public IsValidAction(String requiredAction) {
+ this.requiredAction = requiredAction;
+ }
+
+ @Override
+ public boolean test(ResetCredentialsActionToken t) throws VerificationException {
+ ClientSessionModel clientSession = t.getClientSession();
+ if (! Objects.equals(clientSession.getAction(), this.requiredAction)) {
+
+ if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
+// TODO: Once login tokens would be implemented, this would have to be rewritten
+// String code = clientSession.getNote(ClientSessionCode.ACTIVE_CODE) + "." + clientSession.getId();
+ String code = clientSession.getNote("active_code") + "." + clientSession.getId();
+ throw new LoginActionsServiceException(redirectToRequiredActions(code));
+ } else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
+ throw new LoginActionsServiceException(
+ session.getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.ALREADY_LOGGED_IN)
+ .createInfoPage());
+ }
+ }
+
+ return true;
+ }
+ }
+
+ private class IsActiveAction implements Predicate<ResetCredentialsActionToken> {
+ private final ClientSessionCode.ActionType actionType;
+
+ public IsActiveAction(ActionType actionType) {
+ this.actionType = actionType;
+ }
+
+ @Override
+ public boolean test(ResetCredentialsActionToken t) throws VerificationException {
+ int timestamp = t.getClientSession().getTimestamp();
+ if (! isActionActive(actionType, timestamp)) {
+ event.client(t.getClientSession().getClient());
+ event.clone().error(Errors.EXPIRED_CODE);
+
+ if (t.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
+ // TODO:mposolda incompatible types
+ LoginSessionModel loginSession = (LoginSessionModel) t.getClientSession();
+
+ AuthenticationProcessor.resetFlow(loginSession);
+ throw new LoginActionsServiceException(processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT));
+ }
+
+ throw new LoginActionsServiceException(ErrorPage.error(session, Messages.EXPIRED_CODE));
+ }
+ return true;
+ }
+
+ public boolean isActionActive(ActionType actionType, int timestamp) {
+ int lifespan;
+ switch (actionType) {
+ case CLIENT:
+ lifespan = realm.getAccessCodeLifespan();
+ break;
+ case LOGIN:
+ lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction();
+ break;
+ case USER:
+ lifespan = realm.getAccessCodeLifespanUserAction();
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ return timestamp + lifespan > Time.currentTime();
+ }
+
+ }
+
/**
* Endpoint for executing reset credentials flow. If code is null, a client session is created with the account
* service as the client. Successful reset sends you to the account page. Note, account service must be enabled.
@@ -433,12 +606,10 @@ public class LoginActionsService {
@Path(RESET_CREDENTIALS_PATH)
@GET
public Response resetCredentialsGET(@QueryParam("code") String code,
- @QueryParam("execution") String execution) {
+ @QueryParam("execution") String execution,
+ @QueryParam("key") String key) {
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
- //
- // TODO:mposolda
- /*
- if (code == null) {
+ if (code == null && key == null) {
if (!realm.isResetPasswordAllowed()) {
event.event(EventType.RESET_PASSWORD);
event.error(Errors.NOT_ALLOWED);
@@ -450,7 +621,7 @@ public class LoginActionsService {
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
//clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
- clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ clientSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
clientSession.setRedirectUri(redirectUri);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
@@ -459,32 +630,96 @@ public class LoginActionsService {
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
return processResetCredentials(null, clientSession, null);
}
+
+ if (key != null) {
+ try {
+ ResetCredentialsActionToken token = ResetCredentialsActionToken.deserialize(
+ session, realm, session.getContext().getUri(), key);
+ return resetCredentials(code, token, execution);
+ } catch (VerificationException ex) {
+ event.event(EventType.RESET_PASSWORD)
+ .detail(Details.REASON, ex.getMessage())
+ .error(Errors.NOT_ALLOWED);
+ return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+ }
+ }
+
return resetCredentials(code, execution);
- */
- return null;
}
- /*
+
+ /**
+ * @deprecated In favor of {@link #resetCredentials(String, org.keycloak.authentication.ResetCredentialsActionToken, java.lang.String)}
+ * @param code
+ * @param execution
+ * @return
+ */
protected Response resetCredentials(String code, String execution) {
event.event(EventType.RESET_PASSWORD);
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
+ SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
+ if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
- final ClientSessionCode clientCode = checks.clientCode;
- final ClientSessionModel clientSession = clientCode.getClientSession();
+ final LoginSessionModel clientSession = checks.getClientSession();
if (!realm.isResetPasswordAllowed()) {
- event.client(clientCode.getClientSession().getClient());
+ event.client(clientSession.getClient());
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
+ // TODO:mposolda
+ //return processResetCredentials(execution, clientSession, null);
+ return null;
+ }
+
+ protected Response resetCredentials(String code, ResetCredentialsActionToken token, String execution) {
+ event.event(EventType.RESET_PASSWORD);
+
+ if (token == null) {
+ // TODO: Use more appropriate code
+ event.error(Errors.NOT_ALLOWED);
+ return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+ }
+
+ try {
+ TokenVerifier.from(token).checkOnly(
+ // Start basic checks
+ this::isRealmEnabled,
+ this::isSslUsed,
+ this::isResetCredentialsAllowed,
+ this::canResolveClientSession,
+ this::canResolveClient,
+ // End basic checks
+
+ new IsValidAction(ClientSessionModel.Action.AUTHENTICATE.name()),
+ new IsActiveAction(ActionType.USER)
+ ).verify();
+ } catch (LoginActionsServiceException ex) {
+ if (ex.getResponse() == null) {
+ event.event(EventType.RESET_PASSWORD)
+ .detail(Details.REASON, ex.getMessage())
+ .error(Errors.INVALID_REQUEST);
+ return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+ } else {
+ return ex.getResponse();
+ }
+ } catch (VerificationException ex) {
+ event.event(EventType.RESET_PASSWORD)
+ .detail(Details.REASON, ex.getMessage())
+ .error(Errors.NOT_ALLOWED);
+ return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+ }
+
+ final ClientSessionModel clientSession = token.getClientSession();
+
return processResetCredentials(execution, clientSession, null);
}
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
+ // TODO:mposolda
+ /*
AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
@Override
@@ -507,7 +742,9 @@ public class LoginActionsService {
};
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
- }*/
+ */
+ return null;
+ }
protected Response processRegistration(String execution, LoginSessionModel loginSession, String errorMessage) {
@@ -531,8 +768,8 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
+ if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@@ -561,14 +798,14 @@ public class LoginActionsService {
event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ SessionCodeChecks checks = checksForCode(code, LoginSessionModel.class);
+ if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
+
ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
LoginSessionModel loginSession = clientCode.getClientSession();
-
return processRegistration(execution, loginSession, null);
}
@@ -607,13 +844,12 @@ public class LoginActionsService {
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType);
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
+ SessionCodeChecks checks = checksForCode(code);
+ if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
- ClientSessionCode clientSessionCode = checks.clientCode;
- final ClientSessionModel clientSessionn = clientSessionCode.getClientSession();
+ final ClientSessionModel clientSessionn = checks.getClientSession();
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
@@ -681,10 +917,11 @@ public class LoginActionsService {
public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN);
String code = formData.getFirst("code");
- Checks checks = new Checks();
- if (!checks.verifyRequiredAction(code, ClientSessionModel.Action.OAUTH_GRANT.name())) {
+ SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
+ if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
return checks.response;
}
+
ClientSessionCode<LoginSessionModel> accessCode = checks.clientCode;
LoginSessionModel loginSession = accessCode.getClientSession();
@@ -750,16 +987,15 @@ public class LoginActionsService {
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
+ SessionCodeChecks checks = checksForCode(code);
+ if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
if (checks.clientCode == null && checks.result.isClientSessionNotFound() || checks.result.isIllegalHash()) {
return ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK);
}
return checks.response;
}
- ClientSessionCode accessCode = checks.clientCode;
- clientSession = accessCode.getClientSession();
+ clientSession = checks.getClientSession();
if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
ServicesLogger.LOGGER.reqdActionDoesNotMatch();
event.error(Errors.INVALID_CODE);
@@ -789,12 +1025,12 @@ public class LoginActionsService {
return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
} else {
- Checks checks = new Checks();
- if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
+ SessionCodeChecks checks = checksForCode(code);
+ if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
- ClientSessionModel clientSession = accessCode.getClientSession();
+ ClientSessionModel clientSession = checks.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
initEvent(clientSession);
@@ -824,11 +1060,11 @@ public class LoginActionsService {
/*
event.event(EventType.EXECUTE_ACTIONS);
if (key != null) {
- Checks checks = new Checks();
- if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
+ SessionCodeChecks checks = checksForCode(key);
+ if (!checks.verifyCode(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
- ClientSessionModel clientSession = checks.clientCode.getClientSession();
+ ClientSessionModel clientSession = checks.getClientSession();
// verify user email as we know it is valid as this entry point would never have gotten here.
clientSession.getUserSession().getUser().setEmailVerified(true);
clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
@@ -936,12 +1172,11 @@ public class LoginActionsService {
/*
event.event(EventType.CUSTOM_REQUIRED_ACTION);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
- Checks checks = new Checks();
- if (!checks.verifyRequiredAction(code, action)) {
+ SessionCodeChecks checks = checksForCode(code);
+ if (!checks.verifyRequiredAction(action)) {
return checks.response;
}
- final ClientSessionCode clientCode = checks.clientCode;
- final ClientSessionModel clientSession = clientCode.getClientSession();
+ final ClientSessionModel clientSession = checks.getClientSession();
final UserSessionModel userSession = clientSession.getUserSession();
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java
new file mode 100644
index 0000000..3e758df
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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 org.keycloak.services.resources;
+
+import org.keycloak.common.VerificationException;
+import javax.ws.rs.core.Response;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class LoginActionsServiceException extends VerificationException {
+
+ private final Response response;
+
+ public LoginActionsServiceException(Response response) {
+ this.response = response;
+ }
+
+ public LoginActionsServiceException(Response response, String message) {
+ super(message);
+ this.response = response;
+ }
+
+ public LoginActionsServiceException(Response response, String message, Throwable cause) {
+ super(message, cause);
+ this.response = response;
+ }
+
+ public LoginActionsServiceException(Response response, Throwable cause) {
+ super(cause);
+ this.response = response;
+ }
+
+ public Response getResponse() {
+ return response;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 3a12a76..201a625 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -50,6 +50,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -74,6 +75,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
+ expectedMessagesCount = 0;
getCleanup().addUserId(userId);
}
@@ -104,6 +106,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
+ private int expectedMessagesCount;
+
@Test
public void resetPasswordLink() throws IOException, MessagingException {
String username = "login-test";
@@ -168,21 +172,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
@Test
+ @Ignore
+ public void resetPasswordTwice() throws IOException, MessagingException {
+ String changePasswordUrl = resetPassword("login-test");
+ driver.navigate().to(changePasswordUrl.trim());
+
+ errorPage.assertCurrent();
+ assertEquals("An error occurred, please login again through your application.", errorPage.getError());
+
+ events.expect(EventType.RESET_PASSWORD)
+ .client((String) null)
+ .session((String) null)
+ .user(userId)
+ .detail(Details.USERNAME, "login-test")
+ .error(Errors.EXPIRED_CODE)
+ .assertEvent();
+ }
+
+ @Test
public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
resetPassword(" login-test ");
}
@Test
public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("test-user@localhost");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("test-user@localhost");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost")
.session((String) null)
@@ -206,16 +220,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPassword("login@test.com");
}
- private void resetPassword(String username) throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
+ private String resetPassword(String username) throws IOException, MessagingException {
+ return resetPassword(username, "resetPassword");
+ }
- resetPasswordPage.changePassword(username);
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ private String resetPassword(String username, String password) throws IOException, MessagingException {
+ initiateResetPasswordFromResetPasswordPage(username);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.user(userId)
@@ -224,9 +234,9 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.session((String)null)
.assertEvent();
- assertEquals(1, greenMail.getReceivedMessages().length);
+ assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
- MimeMessage message = greenMail.getReceivedMessages()[0];
+ MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
@@ -234,7 +244,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.assertCurrent();
- updatePasswordPage.changePassword("resetPassword", "resetPassword");
+ updatePasswordPage.changePassword(password, password);
String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
@@ -248,63 +258,27 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.open();
- loginPage.login("login-test", "resetPassword");
-
- events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
-
- assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- }
-
- private void resetPassword(String username, String password) throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword(username);
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
-
- events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
- .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
-
- MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
-
- String changePasswordUrl = getPasswordResetEmailLink(message);
-
- driver.navigate().to(changePasswordUrl.trim());
-
- updatePasswordPage.assertCurrent();
+ loginPage.login("login-test", password);
- updatePasswordPage.changePassword(password, password);
-
- String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId)
- .detail(Details.USERNAME, username).assertEvent().getSessionId();
+ sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
- events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
-
oauth.openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
+
+ return changePasswordUrl;
}
private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword(username);
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage(username);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
+ assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
+
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
@@ -320,17 +294,22 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
}
- @Test
- public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
+ public void initiateResetPasswordFromResetPasswordPage(String username) {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("invalid");
+
+ resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ expectedMessagesCount++;
+ }
+
+ @Test
+ public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
+ initiateResetPasswordFromResetPasswordPage("invalid");
assertEquals(0, greenMail.getReceivedMessages().length);
@@ -359,15 +338,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Test
public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException {
try {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("login-test");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null)
@@ -403,15 +374,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
testRealm().update(realmRep);
try {
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("login-test");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null)
@@ -434,55 +397,50 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent();
} finally {
setTimeOffset(0);
+
+ realmRep.setAccessCodeLifespanUserAction(originalValue.get());
+ testRealm().update(realmRep);
}
}
@Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
UserRepresentation user = findUser("login-test");
- user.setEnabled(false);
- updateUser(user);
-
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ try {
+ user.setEnabled(false);
+ updateUser(user);
- assertEquals(0, greenMail.getReceivedMessages().length);
+ initiateResetPasswordFromResetPasswordPage("login-test");
- events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
+ assertEquals(0, greenMail.getReceivedMessages().length);
- user.setEnabled(true);
- updateUser(user);
+ events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
+ } finally {
+ user.setEnabled(true);
+ updateUser(user);
+ }
}
@Test
public void resetPasswordNoEmail() throws IOException, MessagingException, InterruptedException {
- final String[] email = new String[1];
+ final String email;
UserRepresentation user = findUser("login-test");
- email[0] = user.getEmail();
- user.setEmail("");
- updateUser(user);
+ email = user.getEmail();
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
+ try {
+ user.setEmail("");
+ updateUser(user);
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("login-test");
- assertEquals(0, greenMail.getReceivedMessages().length);
+ assertEquals(0, greenMail.getReceivedMessages().length);
- events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
+ events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
+ } finally {
+ user.setEmail(email);
+ updateUser(user);
+ }
}
@Test
@@ -496,29 +454,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
RealmRepresentation realmRep = testRealm().toRepresentation();
Map<String, String> oldSmtp = realmRep.getSmtpServer();
- realmRep.setSmtpServer(smtpConfig);
- testRealm().update(realmRep);
-
- loginPage.open();
- loginPage.resetPassword();
+ try {
+ realmRep.setSmtpServer(smtpConfig);
+ testRealm().update(realmRep);
- resetPasswordPage.assertCurrent();
+ loginPage.open();
+ loginPage.resetPassword();
- resetPasswordPage.changePassword("login-test");
+ resetPasswordPage.assertCurrent();
- errorPage.assertCurrent();
+ resetPasswordPage.changePassword("login-test");
- assertEquals("Failed to send email, please try again later.", errorPage.getError());
+ errorPage.assertCurrent();
- assertEquals(0, greenMail.getReceivedMessages().length);
+ assertEquals("Failed to send email, please try again later.", errorPage.getError());
- events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
- .session((String)null)
- .detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
+ assertEquals(0, greenMail.getReceivedMessages().length);
- // Revert SMTP back
- realmRep.setSmtpServer(oldSmtp);
- testRealm().update(realmRep);
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
+ .session((String)null)
+ .detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
+ } finally {
+ // Revert SMTP back
+ realmRep.setSmtpServer(oldSmtp);
+ testRealm().update(realmRep);
+ }
}
private void setPasswordPolicy(String policy) {
@@ -531,15 +491,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException {
setPasswordPolicy("length");
- loginPage.open();
- loginPage.resetPassword();
-
- resetPasswordPage.assertCurrent();
-
- resetPasswordPage.changePassword("login-test");
-
- loginPage.assertCurrent();
- assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+ initiateResetPasswordFromResetPasswordPage("login-test");
assertEquals(1, greenMail.getReceivedMessages().length);