keycloak-aplcache

KEYCLOAK-4627 Changes in TokenVerifier to include token in

3/30/2017 9:05:41 AM

Details

diff --git a/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
index 2253f5e..4740567 100644
--- a/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
+++ b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java
@@ -16,28 +16,29 @@
  */
 package org.keycloak.exceptions;
 
-import org.keycloak.common.VerificationException;
+import org.keycloak.representations.JsonWebToken;
 
 /**
  * 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 class TokenNotActiveException extends TokenVerificationException {
 
-    public TokenNotActiveException() {
+    public TokenNotActiveException(JsonWebToken token) {
+        super(token);
     }
 
-    public TokenNotActiveException(String message) {
-        super(message);
+    public TokenNotActiveException(JsonWebToken token, String message) {
+        super(token, message);
     }
 
-    public TokenNotActiveException(String message, Throwable cause) {
-        super(message, cause);
+    public TokenNotActiveException(JsonWebToken token, String message, Throwable cause) {
+        super(token, message, cause);
     }
 
-    public TokenNotActiveException(Throwable cause) {
-        super(cause);
+    public TokenNotActiveException(JsonWebToken token, Throwable cause) {
+        super(token, cause);
     }
 
 }
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
index 13225fa..4d389eb 100644
--- a/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
+++ b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java
@@ -16,27 +16,28 @@
  */
 package org.keycloak.exceptions;
 
-import org.keycloak.common.VerificationException;
+import org.keycloak.representations.JsonWebToken;
 
 /**
  * Thrown when token signature is invalid.
  * @author hmlnarik
  */
-public class TokenSignatureInvalidException extends VerificationException {
+public class TokenSignatureInvalidException extends TokenVerificationException {
 
-    public TokenSignatureInvalidException() {
+    public TokenSignatureInvalidException(JsonWebToken token) {
+        super(token);
     }
 
-    public TokenSignatureInvalidException(String message) {
-        super(message);
+    public TokenSignatureInvalidException(JsonWebToken token, String message) {
+        super(token, message);
     }
 
-    public TokenSignatureInvalidException(String message, Throwable cause) {
-        super(message, cause);
+    public TokenSignatureInvalidException(JsonWebToken token, String message, Throwable cause) {
+        super(token, message, cause);
     }
 
-    public TokenSignatureInvalidException(Throwable cause) {
-        super(cause);
+    public TokenSignatureInvalidException(JsonWebToken token, Throwable cause) {
+        super(token, cause);
     }
 
 }
diff --git a/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java b/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java
new file mode 100644
index 0000000..4d6b7d0
--- /dev/null
+++ b/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+import org.keycloak.representations.JsonWebToken;
+
+/**
+ * Exception thrown on failed verification of a token.
+ *
+ * @author hmlnarik
+ */
+public class TokenVerificationException extends VerificationException {
+
+    private final JsonWebToken token;
+
+    public TokenVerificationException(JsonWebToken token) {
+        this.token = token;
+    }
+
+    public TokenVerificationException(JsonWebToken token, String message) {
+        super(message);
+        this.token = token;
+    }
+
+    public TokenVerificationException(JsonWebToken token, String message, Throwable cause) {
+        super(message, cause);
+        this.token = token;
+    }
+
+    public TokenVerificationException(JsonWebToken token, Throwable cause) {
+        super(cause);
+        this.token = token;
+    }
+
+    public JsonWebToken getToken() {
+        return token;
+    }
+
+}
diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java
index 653f205..0e3c08b 100755
--- a/core/src/main/java/org/keycloak/RSATokenVerifier.java
+++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java
@@ -32,7 +32,7 @@ public class RSATokenVerifier {
     private final TokenVerifier<AccessToken> tokenVerifier;
 
     private RSATokenVerifier(String tokenString) {
-        this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class);
+        this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class).withDefaultChecks();
     }
 
     public static RSATokenVerifier create(String tokenString) {
diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java
index 6bfcb3b..0c6e2db 100755
--- a/core/src/main/java/org/keycloak/TokenVerifier.java
+++ b/core/src/main/java/org/keycloak/TokenVerifier.java
@@ -33,6 +33,8 @@ import org.keycloak.util.TokenUtil;
 import javax.crypto.SecretKey;
 import java.security.PublicKey;
 import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -40,9 +42,15 @@ import java.util.*;
  */
 public class TokenVerifier<T extends JsonWebToken> {
 
+    private static final Logger LOG = Logger.getLogger(TokenVerifier.class.getName());
+
     // 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.
 
+    /**
+     * Functional interface of checks that verify some part of a JWT.
+     * @param <T> Type of the token handled by this predicate.
+     */
     // @FunctionalInterface
     public static interface Predicate<T extends JsonWebToken> {
         /**
@@ -66,11 +74,15 @@ public class TokenVerifier<T extends JsonWebToken> {
         }
     };
 
+    /**
+     * Check for token being neither expired nor used before it gets valid.
+     * @see JsonWebToken#isActive()
+     */
     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");
+                throw new TokenNotActiveException(t, "Token is not active");
             }
 
             return true;
@@ -143,29 +155,45 @@ public class TokenVerifier<T extends JsonWebToken> {
     }
 
     /**
-     * Creates a {@code TokenVerifier<AccessToken> instance. The method is here for backwards compatibility.
-     * @param tokenString
+     * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class.
+     * The token verifier has no checks defined. Note that the checks are only tested when
+     * {@link #verify()} method is invoked.
+     * @param <T> Type of the token
+     * @param tokenString String representation of JWT
+     * @param clazz Class of the token
      * @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);
     }
 
-    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);
+    /**
+     * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class.
+     * The token verifier has no checks defined. Note that the checks are only tested when
+     * {@link #verify()} method is invoked.
+     * @return
+     */
+    public static <T extends JsonWebToken> TokenVerifier<T> create(T token) {
+        return new TokenVerifier(token);
     }
 
-    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);
+    /**
+     * Adds default checks to the token verification:
+     * <ul>
+     * <li>Realm URL (JWT issuer field: {@code iss}) has to be defined and match realm set via {@link #realmUrl(java.lang.String)} method</li>
+     * <li>Subject (JWT subject field: {@code sub}) has to be defined</li>
+     * <li>Token type (JWT type field: {@code typ}) has to be {@code Bearer}. The type can be set via {@link #tokenType(java.lang.String)} method</li>
+     * <li>Token has to be active, ie. both not expired and not used before its validity (JWT issuer fields: {@code exp} and {@code nbf})</li>
+     * </ul>
+     * @return This token verifier.
+     */
+    public TokenVerifier<T> withDefaultChecks()  {
+        return withChecks(
+          RealmUrlCheck.NULL_INSTANCE,
+          SUBJECT_EXISTS_CHECK,
+          TokenTypeCheck.INSTANCE_BEARER,
+          IS_ACTIVE
+        );
     }
 
     private void removeCheck(Class<? extends Predicate<?>> checkClass) {
@@ -197,12 +225,11 @@ public class TokenVerifier<T extends JsonWebToken> {
     }
 
     /**
-     * Resets all preset checks and will test the given checks in {@link #verify()} method.
+     * Will test the given checks in {@link #verify()} method in addition to already set checks.
      * @param checks
      * @return
      */
-    public TokenVerifier<T> checkOnly(Predicate<? super T>... checks) {
-        this.checks.clear();
+    public TokenVerifier<T> withChecks(Predicate<? super T>... checks) {
         if (checks != null) {
             this.checks.addAll(Arrays.asList(checks));
         }
@@ -210,46 +237,64 @@ public class TokenVerifier<T extends JsonWebToken> {
     }
 
     /**
-     * Will test the given checks in {@link #verify()} method in addition to already set checks.
-     * @param checks
+     * Sets the key for verification of RSA-based signature.
+     * @param publicKey
      * @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;
     }
 
+    /**
+     * Sets the key for verification of HMAC-based signature.
+     * @param secretKey
+     * @return 
+     */
     public TokenVerifier<T> secretKey(SecretKey secretKey) {
         this.secretKey = secretKey;
         return this;
     }
 
+    /**
+     * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+     * @return This token verifier
+     */
     public TokenVerifier<T> realmUrl(String realmUrl) {
         this.realmUrl = realmUrl;
         return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl));
     }
 
+    /**
+     * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+     * @return This token verifier
+     */
     public TokenVerifier<T> checkTokenType(boolean checkTokenType) {
         this.checkTokenType = checkTokenType;
         return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
     }
 
+    /**
+     * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+     * @return This token verifier
+     */
     public TokenVerifier<T> tokenType(String tokenType) {
         this.expectedTokenType = tokenType;
         return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
     }
 
+    /**
+     * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+     * @return This token verifier
+     */
     public TokenVerifier<T> checkActive(boolean checkActive) {
         return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE);
     }
 
+    /**
+     * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}.
+     * @return This token verifier
+     */
     public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) {
         this.checkRealmUrl = checkRealmUrl;
         return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
@@ -300,14 +345,14 @@ public class TokenVerifier<T extends JsonWebToken> {
                     throw new VerificationException("Public key not set");
                 }
                 if (!RSAProvider.verify(jws, publicKey)) {
-                    throw new TokenSignatureInvalidException("Invalid token signature");
+                    throw new TokenSignatureInvalidException(token, "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");
+                    throw new TokenSignatureInvalidException(token, "Invalid token signature");
                 }   break;
             default:
                 throw new VerificationException("Unknown or unsupported token algorithm");
@@ -331,4 +376,55 @@ public class TokenVerifier<T extends JsonWebToken> {
         return this;
     }
 
+    /**
+     * Creates an optional predicate from a predicate that will proceed with check but always pass.
+     * @param <T>
+     * @param mandatoryPredicate
+     * @return
+     */
+    public static <T extends JsonWebToken> Predicate<T> optional(final Predicate<T> mandatoryPredicate) {
+        return new Predicate<T>() {
+            @Override
+            public boolean test(T t) throws VerificationException {
+                try {
+                    if (! mandatoryPredicate.test(t)) {
+                        LOG.finer("[optional] predicate failed: " + mandatoryPredicate);
+                    }
+
+                    return true;
+                } catch (VerificationException ex) {
+                    LOG.log(Level.FINER, "[optional] predicate " + mandatoryPredicate + " failed.", ex);
+                    return true;
+                }
+            }
+        };
+    }
+
+    /**
+     * Creates a predicate that will proceed with checks of the given predicates
+     * and will pass if and only if at least one of the given predicates passes.
+     * @param <T>
+     * @param predicates
+     * @return
+     */
+    public static <T extends JsonWebToken> Predicate<T> alternative(final Predicate<? super T>... predicates) {
+        return new Predicate<T>() {
+            @Override
+            public boolean test(T t) throws VerificationException {
+                for (Predicate<? super T> predicate : predicates) {
+                    try {
+                        if (predicate.test(t)) {
+                            return true;
+                        }
+
+                        LOG.finer("[alternative] predicate failed: " + predicate);
+                    } catch (VerificationException ex) {
+                        LOG.log(Level.FINER, "[alternative] predicate " + predicate + " failed.", ex);
+                    }
+                }
+
+                return false;
+            }
+        };
+    }
 }
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 462b5d2..05ed948 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
@@ -17,7 +17,6 @@
 
 package org.keycloak.authentication.authenticators.resetcred;
 
-import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.authentication.*;
 import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
@@ -35,7 +34,6 @@ import org.keycloak.models.utils.FormMessage;
 import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.services.ServicesLogger;
 import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsService;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
 import java.util.*;
@@ -78,13 +76,11 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
         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();
+        KeycloakSession keycloakSession = context.getSession();
+        Long lastCreatedPassword = getLastChangedTimestamp(keycloakSession, context.getRealm(), user);
 
         // We send the secret in the email in a link as a query param.
         ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, null, lastCreatedPassword, context.getAuthenticationSession());
-        KeycloakSession keycloakSession = context.getSession();
         String link = UriBuilder
           .fromUri(context.getRefreshExecutionUrl())
           .queryParam(Constants.KEY, token.serialize(keycloakSession, context.getRealm(), context.getUriInfo()))
@@ -112,23 +108,27 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
         }
     }
 
+    public static Long getLastChangedTimestamp(KeycloakSession session, RealmModel realm, UserModel user) {
+        // TODO(hmlnarik): Make this more generic to support non-password credential types
+        PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) session.getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
+        CredentialModel password = passwordProvider.getPassword(realm, user);
+
+        return password == null ? null : password.getCreatedDate();
+    }
+
     @Override
     public void action(AuthenticationFlowContext context) {
         KeycloakSession keycloakSession = context.getSession();
-        String actionTokenString = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
+        String actionTokenString = context.getAuthenticationSession().getAuthNote(ResetCredentialsActionToken.class.getName());
         ResetCredentialsActionToken tokenFromMail = null;
+
         try {
-            tokenFromMail = ResetCredentialsActionToken.deserialize(keycloakSession, context.getRealm(), context.getUriInfo(), actionTokenString);
+            tokenFromMail = ResetCredentialsActionToken.deserialize(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);
+            context.getEvent().detail(Details.REASON, ex.getMessage());
+            // flow returns in the next condition so no "return" statmenent here
         }
 
-        String userId = tokenFromMail == null ? null : tokenFromMail.getUserId();
-
         if (tokenFromMail == null) {
             context.getEvent()
               .error(Errors.INVALID_CODE);
@@ -139,14 +139,15 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
             return;
         }
 
-        PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
-        CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser());
+        String userId = tokenFromMail.getUserId();
 
         Long lastCreatedPasswordMail = tokenFromMail.getLastChangedPasswordTimestamp();
-        Long lastCreatedPasswordFromStore = password == null ? null : password.getCreatedDate();
+        Long lastCreatedPasswordFromStore = getLastChangedTimestamp(keycloakSession, context.getRealm(), context.getUser());
 
         String authenticationSessionId = tokenFromMail.getAuthenticationSessionId();
-        AuthenticationSessionModel authenticationSession = authenticationSessionId == null ? null : keycloakSession.authenticationSessions().getAuthenticationSession(context.getRealm(), authenticationSessionId);
+        AuthenticationSessionModel authenticationSession = authenticationSessionId == null
+          ? null
+          : keycloakSession.authenticationSessions().getAuthenticationSession(context.getRealm(), authenticationSessionId);
 
         if (authenticationSession == null
           || ! Objects.equals(lastCreatedPasswordMail, lastCreatedPasswordFromStore)
@@ -157,7 +158,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
               .detail(Details.TOKEN_ID, tokenFromMail.getId())
               .error(Errors.EXPIRED_CODE);
             Response challenge = context.form()
-                    .setError(Messages.INVALID_CODE)
+                    .setError(Messages.EXPIRED_CODE)
                     .createErrorPage();
             context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
             return;
diff --git a/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java b/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java
index 4018212..5612a35 100644
--- a/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionToken.java
@@ -42,7 +42,7 @@ 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 RESET_CREDENTIALS_TYPE = "reset-credentials";
     public static final String NOTE_AUTHENTICATION_SESSION_ID = "clientSessionId";
     private static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid";
     private static final String JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP = "lcpt";
@@ -54,7 +54,7 @@ public class ResetCredentialsActionToken extends DefaultActionToken {
     private Long lastChangedPasswordTimestamp;
 
     public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, String authenticationSessionId) {
-        super(userId, RESET_CREDENTIALS_ACTION, absoluteExpirationInSecs, actionVerificationNonce);
+        super(userId, RESET_CREDENTIALS_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
         setNote(NOTE_AUTHENTICATION_SESSION_ID, authenticationSessionId);
         this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
     }
@@ -131,19 +131,7 @@ public class ResetCredentialsActionToken extends DefaultActionToken {
      * @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()
-        ;
+    public static ResetCredentialsActionToken deserialize(String token) throws VerificationException {
+        return TokenVerifier.create(token, ResetCredentialsActionToken.class).getToken();
     }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionTokenChecks.java b/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionTokenChecks.java
new file mode 100644
index 0000000..9afc25c
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/ResetCredentialsActionTokenChecks.java
@@ -0,0 +1,74 @@
+/*
+ * 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.authentication.authenticators.resetcred.ResetCredentialEmail;
+import org.keycloak.common.VerificationException;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.ErrorPage;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.LoginActionsServiceException;
+import java.util.Objects;
+
+/**
+ * Additional checks for {@link ResetCredentialsActionToken}.
+ *
+ * @author hmlnarik
+ */
+public class ResetCredentialsActionTokenChecks implements Predicate<ResetCredentialsActionToken> {
+
+    private final KeycloakSession session;
+
+    private final RealmModel realm;
+
+    private final EventBuilder event;
+
+    public ResetCredentialsActionTokenChecks(KeycloakSession session, RealmModel realm, EventBuilder event) {
+        this.session = session;
+        this.realm = realm;
+        this.event = event;
+    }
+
+    public boolean lastChangedTimestampMatches(ResetCredentialsActionToken t) throws VerificationException {
+        // TODO:hmlnarik Update to use single-use cache
+        UserModel m = session.users().getUserById(t.getSubject(), realm);
+        Long lastChanged = m == null ? null : ResetCredentialEmail.getLastChangedTimestamp(session, realm, m);
+
+        if (! Objects.equals(lastChanged, t.getLastChangedPasswordTimestamp())) {
+            if (m != null) {
+                event.detail(Details.USERNAME, m.getUsername());
+            }
+            event.user(t.getSubject()).error(Errors.EXPIRED_CODE);
+            throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean test(ResetCredentialsActionToken t) throws VerificationException {
+        return lastChangedTimestampMatches(t);
+
+    }
+
+}
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 3c68370..10b5e3d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -750,7 +750,11 @@ 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<AccessToken> verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
+              .withDefaultChecks()
+              .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 5033395..6792964 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -26,17 +26,18 @@ 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.TokenVerifier.TokenTypeCheck;
+import org.keycloak.authentication.*;
 import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
 import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.common.VerificationException;
 import org.keycloak.common.util.ObjectUtil;
-import org.keycloak.common.util.Time;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
 import org.keycloak.events.EventType;
+import org.keycloak.exceptions.TokenNotActiveException;
 import org.keycloak.forms.login.LoginFormsProvider;
 import org.keycloak.models.AuthenticationFlowModel;
 import org.keycloak.models.AuthenticatedClientSessionModel;
@@ -62,12 +63,12 @@ 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.messages.Messages;
 import org.keycloak.services.util.CacheControlUtil;
 import org.keycloak.services.util.CookieHelper;
 import org.keycloak.sessions.AuthenticationSessionModel;
 
+import org.keycloak.sessions.CommonClientSessionModel.Action;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
@@ -85,6 +86,11 @@ import javax.ws.rs.core.UriInfo;
 import javax.ws.rs.ext.Providers;
 import java.net.URI;
 import java.util.Objects;
+import java.util.function.*;
+import javax.ws.rs.core.*;
+import static org.keycloak.TokenVerifier.optional;
+import static org.keycloak.authentication.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS;
+import static org.keycloak.authentication.ResetCredentialsActionToken.RESET_CREDENTIALS_TYPE;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -518,169 +524,190 @@ public class LoginActionsService {
         return resetCredentials(code, execution);
     }
 
-    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 Predicate<JsonWebToken> checkThat(BooleanSupplier function, String errorEvent, String errorMessage) {
+        return t -> {
+            if (! function.getAsBoolean()) {
+                event.error(errorEvent);
+                throw new LoginActionsServiceException(ErrorPage.error(session, errorMessage));
+            }
+
+            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));
+    /**
+     * Verifies that the authentication session has not yet been converted to user session, in other words
+     * that the user has not yet completed authentication and logged in.
+     * @param t token
+     */
+    private class IsAuthenticationSessionNotConvertedToUserSession<T extends JsonWebToken> implements Predicate<T> {
+
+        private final Function<T, String> getAuthenticationSessionIdFromToken;
+
+        public IsAuthenticationSessionNotConvertedToUserSession(Function<T, String> getAuthenticationSessionIdFromToken) {
+            this.getAuthenticationSessionIdFromToken = getAuthenticationSessionIdFromToken;
         }
-        return true;
-    }
 
-    private boolean isResetCredentialsAllowed(ResetCredentialsActionToken t) throws VerificationException {
-        if (!realm.isResetPasswordAllowed()) {
-            event.client(t.getAuthenticationSession().getClient());
-            event.error(Errors.NOT_ALLOWED);
-            throw new LoginActionsServiceException(ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED));
+        @Override
+        public boolean test(T t) throws VerificationException {
+            String authSessionId = t == null ? null : getAuthenticationSessionIdFromToken.apply(t);
+            if (authSessionId == null) {
+                return false;
+            }
+
+            if (session.sessions().getUserSession(realm, authSessionId) != null) {
+                throw new LoginActionsServiceException(
+                  session.getProvider(LoginFormsProvider.class)
+                        .setSuccess(Messages.ALREADY_LOGGED_IN)
+                        .createInfoPage());
+            }
+
+            return true;
         }
-        return true;
     }
 
-    private boolean canResolveClientSession(ResetCredentialsActionToken t) throws VerificationException {
-        String authSessionId = t == null ? null : t.getAuthenticationSessionId();
+    /**
+     * Verifies whether client stored in the authentication session both exists and is enabled. If yes, it also sets the client
+     * into session context.
+     * @param <T>
+     */
+    private class IsClientValid<T extends JsonWebToken> implements Predicate<T> {
 
-        if (t == null || authSessionId == null) {
-            event.error(Errors.INVALID_CODE);
-            throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
-        }
+        private final Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken;
 
-        AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
-        t.setAuthenticationSession(authSession);
+        public IsClientValid(Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken) {
+            this.getAuthenticationSessionFromToken = getAuthenticationSessionFromToken;
+        }
 
-        if (authSession == null) { // timeout or logged-already
-            try {
-                // Check if we are logged-already (it means userSession with same ID already exists). If yes, just showing the INFO or ERROR that user is already authenticated
-                // TODO:mposolda
+        @Override
+        public boolean test(T t) throws VerificationException {
+            AuthenticationSessionModel authenticationSession = getAuthenticationSessionFromToken.apply(t);
 
-                // If not, try to restart authSession from the cookie
-                AuthenticationSessionModel restartedAuthSession = RestartLoginCookie.restartSession(session, realm);
+            ClientModel client = authenticationSession == null ? null : authenticationSession.getClient();
 
-                // IDs must match with the ID from cookie
-                if (restartedAuthSession!=null && restartedAuthSession.getId().equals(authSessionId)) {
-                    authSession = restartedAuthSession;
-                }
-            } catch (Exception e) {
-                ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
+            if (client == null) {
+                event.error(Errors.CLIENT_NOT_FOUND);
+                session.authenticationSessions().removeAuthenticationSession(realm, authenticationSession);
+                throw new LoginActionsServiceException(ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
             }
 
-            if (authSession != null) {
-                event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
-                throw new LoginActionsServiceException(processFlow(false, null, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor()));
+            if (! client.isEnabled()) {
+                event.error(Errors.CLIENT_NOT_FOUND);
+                session.authenticationSessions().removeAuthenticationSession(realm, authenticationSession);
+                throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
             }
-        }
 
-        if (authSession == null) {
-            event.error(Errors.INVALID_CODE);
-            throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
+            session.getContext().setClient(client);
+
+            return true;
         }
+    }
 
-        event.detail(Details.CODE_ID, authSession.getId());
+    /**
+     * This check verifies that:
+     * <ul>
+     * <li>If authentication session ID is not set in the token, passes.</li>
+     * <li>If auth session ID is set in the token, then the corresponding authentication session exists.
+     *     Then it is set into the token.</li>
+     * </ul>
+     *
+     * @param <T>
+     */
+    private class CanResolveAuthenticationSession<T extends JsonWebToken> implements Predicate<T> {
 
-        return true;
-    }
+        private final Function<T, String> getAuthenticationSessionIdFromToken;
 
-    private boolean canResolveClient(ResetCredentialsActionToken t) throws VerificationException {
-        ClientModel client = t.getAuthenticationSession().getClient();
-        if (client == null) {
-            event.error(Errors.CLIENT_NOT_FOUND);
-            session.authenticationSessions().removeAuthenticationSession(realm, t.getAuthenticationSession());
-            throw new LoginActionsServiceException(ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
-        }
+        private final BiConsumer<T, AuthenticationSessionModel> setAuthenticationSessionToToken;
 
-        if (! client.isEnabled()) {
-            event.error(Errors.CLIENT_NOT_FOUND);
-            session.authenticationSessions().removeAuthenticationSession(realm, t.getAuthenticationSession());
-            throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
+        public CanResolveAuthenticationSession(Function<T, String> getAuthenticationSessionIdFromToken,
+          BiConsumer<T, AuthenticationSessionModel> setAuthenticationSessionToToken) {
+            this.getAuthenticationSessionIdFromToken = getAuthenticationSessionIdFromToken;
+            this.setAuthenticationSessionToToken = setAuthenticationSessionToToken;
         }
-        session.getContext().setClient(client);
 
-        return true;
+        @Override
+        public boolean test(T t) throws VerificationException {
+            String authSessionId = t == null ? null : getAuthenticationSessionIdFromToken.apply(t);
+
+            AuthenticationSessionModel authSession;
+            if (authSessionId == null) {
+                return true;
+            } else {
+                authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
+            }
+
+            if (authSession == null) { // timeout or logged-already (NOPE - this is handled by IsAuthenticationSessionNotConvertedToUserSession)
+                throw new LoginActionsServiceException(restartAuthenticationSession(false));
+            }
+
+            event
+              .detail(Details.CODE_ID, authSession.getId())
+              .client(authSession.getClient());
+
+            setAuthenticationSessionToToken.accept(t, authSession);
+
+            return true;
+        }
     }
 
-    private class IsValidAction implements Predicate<ResetCredentialsActionToken> {
+    /**
+     * This check verifies that if the token has not authentication session set, a new authentication session is introduced
+     * for the given client and reset-credentials flow is started with this new session.
+     * @param <T>
+     */
+    private class ResetCredsIntroduceAuthenticationSessionIfNotSet implements Predicate<ResetCredentialsActionToken> {
 
-        private final String requiredAction;
+        private final String defaultClientId;
 
-        public IsValidAction(String requiredAction) {
-            this.requiredAction = requiredAction;
+        public ResetCredsIntroduceAuthenticationSessionIfNotSet(String defaultClientId) {
+            this.defaultClientId = defaultClientId;
         }
-        
+
         @Override
         public boolean test(ResetCredentialsActionToken t) throws VerificationException {
             AuthenticationSessionModel authSession = t.getAuthenticationSession();
-            if (! Objects.equals(authSession.getAction(), this.requiredAction)) {
 
-                if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.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(null, authSession));
-                }
-                // TODO:mposolda Similar stuff is in SessionCodeChecks as well. The case when authSession is already logged should be handled similarly
-                /*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());
-                }*/
+            if (authSession == null) {
+                authSession = createAuthenticationSessionForClient(this.defaultClientId);
+                throw new LoginActionsServiceException(processResetCredentials(false, null, authSession, null));
             }
 
             return true;
         }
     }
 
-    private class IsActiveAction implements Predicate<ResetCredentialsActionToken> {
-        private final ClientSessionCode.ActionType actionType;
+    /**
+     * Verifies that if authentication session exists and any action is required according to it, then it is
+     * the expected one.
+     *
+     * If there is an action required in the session, furthermore it is not the expected one, and the required
+     * action is redirection to "required actions", it throws with response performing the redirect to required
+     * actions.
+     * @param <T>
+     */
+    private class IsActionRequired<T extends JsonWebToken> implements Predicate<T> {
+
+        private final ClientSessionModel.Action expectedAction;
+        
+        private final Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken;
 
-        public IsActiveAction(ActionType actionType) {
-            this.actionType = actionType;
+        public IsActionRequired(Action expectedAction, Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken) {
+            this.expectedAction = expectedAction;
+            this.getAuthenticationSessionFromToken = getAuthenticationSessionFromToken;
         }
 
         @Override
-        public boolean test(ResetCredentialsActionToken t) throws VerificationException {
-            int timestamp = t.getAuthenticationSession().getTimestamp();
-            if (! isActionActive(actionType, timestamp)) {
-                event.client(t.getAuthenticationSession().getClient());
-                event.clone().error(Errors.EXPIRED_CODE);
-
-                if (t.getAuthenticationSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
-                    AuthenticationSessionModel authSession = t.getAuthenticationSession();
-
-                    AuthenticationProcessor.resetFlow(authSession);
-                    throw new LoginActionsServiceException(processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT));
+        public boolean test(T t) throws VerificationException {
+            AuthenticationSessionModel authSession = getAuthenticationSessionFromToken.apply(t);
+            
+            if (authSession != null && ! Objects.equals(authSession.getAction(), this.expectedAction.name())) {
+                if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.getAction())) {
+                    throw new LoginActionsServiceException(redirectToRequiredActions(null, authSession));
                 }
-
-                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();
+            return true;
         }
-
     }
 
     /**
@@ -695,7 +722,7 @@ public class LoginActionsService {
     @GET
     public Response resetCredentialsGET(@QueryParam("code") String code,
                                         @QueryParam("execution") String execution,
-                                        @QueryParam("key") String key) {
+                                        @QueryParam(Constants.KEY) String key) {
         if (code != null && key != null) {
             // TODO:mposolda better handling of error
             throw new IllegalStateException("Illegal state");
@@ -704,48 +731,45 @@ public class LoginActionsService {
         AuthenticationSessionModel authSession = session.authenticationSessions().getCurrentAuthenticationSession(realm);
 
         // we allow applications to link to reset credentials without going through OAuth or SAML handshakes
-        if (authSession == null && key == null) {
+        if (authSession == null && key == null && code == null) {
             if (!realm.isResetPasswordAllowed()) {
                 event.event(EventType.RESET_PASSWORD);
                 event.error(Errors.NOT_ALLOWED);
                 return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
 
             }
-            // set up the account service as the endpoint to call.
-            ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
-            authSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
-            authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
-            //authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
-            authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
-            String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
-            authSession.setRedirectUri(redirectUri);
-            authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
-            authSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
-            authSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
-            authSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+            authSession = createAuthenticationSessionForClient(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
             return processResetCredentials(false, null, authSession, null);
         }
 
         if (key != null) {
-            try {
-                ResetCredentialsActionToken token = ResetCredentialsActionToken.deserialize(
-                  session, realm, session.getContext().getUri(), key);
-
-                return resetCredentials(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 resetCredentialsByToken(key, execution);
         }
         
         return resetCredentials(code, execution);
     }
 
+    private AuthenticationSessionModel createAuthenticationSessionForClient(String clientId)
+      throws UriBuilderException, IllegalArgumentException {
+        AuthenticationSessionModel authSession;
+
+        // set up the account service as the endpoint to call.
+        ClientModel client = realm.getClientByClientId(clientId);
+        authSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
+        authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+        //authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
+        authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
+        authSession.setRedirectUri(redirectUri);
+        authSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
+        authSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+        authSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+
+        return authSession;
+    }
 
     /**
-     * @deprecated In favor of {@link #resetCredentials(org.keycloak.authentication.ResetCredentialsActionToken, java.lang.String)}
+     * @deprecated In favor of {@link #resetCredentialsByToken(String, String)}
      * @param code
      * @param execution
      * @return
@@ -768,49 +792,79 @@ public class LoginActionsService {
         return processResetCredentials(checks.actionRequest, execution, authSession, null);
     }
 
-    protected Response resetCredentials(ResetCredentialsActionToken token, String execution) {
+    protected Response resetCredentialsByToken(String tokenString, 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);
-        }
-
+        ResetCredentialsActionToken token;
+        ResetCredentialsActionTokenChecks singleUseCheck = new ResetCredentialsActionTokenChecks(session, realm, event);
         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();
+            token = TokenVerifier.createHollow(tokenString, ResetCredentialsActionToken.class)
+              .secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
+
+              .withChecks(
+                new TokenTypeCheck(RESET_CREDENTIALS_TYPE),
+
+                checkThat(realm::isEnabled, Errors.REALM_DISABLED, Messages.REALM_NOT_ENABLED),
+                checkThat(realm::isResetPasswordAllowed, Errors.NOT_ALLOWED, Messages.RESET_CREDENTIAL_NOT_ALLOWED),
+                checkThat(this::checkSsl, Errors.SSL_REQUIRED, Messages.HTTPS_REQUIRED),
+
+                new IsAuthenticationSessionNotConvertedToUserSession<>(ResetCredentialsActionToken::getAuthenticationSessionId),
+
+                // Authentication session might not be part of the token, hence the following check is optional
+                optional(new CanResolveAuthenticationSession<>(ResetCredentialsActionToken::getAuthenticationSessionId, ResetCredentialsActionToken::setAuthenticationSession)),
+
+                // Check for being active has to be after authentication session is resolved so that it can be used in error handling
+                TokenVerifier.IS_ACTIVE,
+
+                singleUseCheck, // TODO:hmlnarik make it use a check via generic single-use cache
+
+                new ResetCredsIntroduceAuthenticationSessionIfNotSet(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID),
+
+                new IsActionRequired<>(Action.AUTHENTICATE, ResetCredentialsActionToken::getAuthenticationSession),
+                new IsClientValid<>(ResetCredentialsActionToken::getAuthenticationSession)
+              )
+              .withChecks(ACTION_TOKEN_BASIC_CHECKS)
+
+              .verify()
+              .getToken();
+        } catch (TokenNotActiveException ex) {
+            token = (ResetCredentialsActionToken) ex.getToken();
+
+            if (token != null && token.getAuthenticationSession() != null) {
+                event.clone()
+                  .client(token.getAuthenticationSession().getClient())
+                  .error(Errors.EXPIRED_CODE);
+                AuthenticationSessionModel authSession = token.getAuthenticationSession();
+                AuthenticationProcessor.resetFlow(authSession);
+                return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT);
+            }
+
+            event
+              .detail(Details.REASON, ex.getMessage())
+              .error(Errors.NOT_ALLOWED);
+            return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
         } catch (LoginActionsServiceException ex) {
             if (ex.getResponse() == null) {
-                event.event(EventType.RESET_PASSWORD)
+                event
                   .detail(Details.REASON, ex.getMessage())
-                  .error(Errors.INVALID_REQUEST);
+                  .error(Errors.NOT_ALLOWED);
                 return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
             } else {
                 return ex.getResponse();
             }
         } catch (VerificationException ex) {
-            event.event(EventType.RESET_PASSWORD)
+            event
               .detail(Details.REASON, ex.getMessage())
               .error(Errors.NOT_ALLOWED);
             return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
         }
 
         final AuthenticationSessionModel authSession = token.getAuthenticationSession();
+        authSession.setAuthNote(ResetCredentialsActionToken.class.getName(), tokenString);
 
         // Verify if action is processed in same browser.
         if (!isSameBrowser(authSession)) {
-            logger.infof("Action request processed in different browser!");
+            logger.debug("Action request processed in different browser.");
 
             // TODO:mposolda improve this. The code should be merged with the InfinispanLoginSessionProvider code and rather extracted from the infinispan provider
             setAuthSessionCookie(authSession.getId());
@@ -846,7 +900,7 @@ public class LoginActionsService {
         }
 
         if (actionTokenSession.getId().equals(parentSessionId)) {
-            // It's the the correct browser. Let's remove forked session as we won't continue from the login form (browser flow) but from the resetCredentials flow
+            // It's the correct browser. Let's remove forked session as we won't continue from the login form (browser flow) but from the resetCredentialsByToken flow
             session.authenticationSessions().removeAuthenticationSession(realm, forkedSession);
             logger.infof("Removed forked session: %s", forkedSession.getId());
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
index eaf5ab5..454d205 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java
@@ -40,6 +40,7 @@ import javax.ws.rs.core.Response;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import static org.hamcrest.Matchers.is;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -271,16 +272,16 @@ public class AssertEvents implements TestRule {
         }
 
         public EventRepresentation assertEvent(EventRepresentation actual) {
-            if (expected.getError() != null && !expected.getType().toString().endsWith("_ERROR")) {
+            if (expected.getError() != null && ! expected.getType().toString().endsWith("_ERROR")) {
                 expected.setType(expected.getType() + "_ERROR");
             }
-            Assert.assertEquals(expected.getType(), actual.getType());
-            Assert.assertThat(actual.getRealmId(), realmId);
-            Assert.assertEquals(expected.getClientId(), actual.getClientId());
-            Assert.assertEquals(expected.getError(), actual.getError());
-            Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress());
-            Assert.assertThat(actual.getUserId(), userId);
-            Assert.assertThat(actual.getSessionId(), sessionId);
+            Assert.assertThat("type", actual.getType(), is(expected.getType()));
+            Assert.assertThat("realm ID", actual.getRealmId(), is(realmId));
+            Assert.assertThat("client ID", actual.getClientId(), is(expected.getClientId()));
+            Assert.assertThat("error", actual.getError(), is(expected.getError()));
+            Assert.assertThat("ip address", actual.getIpAddress(), is(expected.getIpAddress()));
+            Assert.assertThat("user ID", actual.getUserId(), is(userId));
+            Assert.assertThat("session ID", actual.getSessionId(), is(sessionId));
 
             if (details == null || details.isEmpty()) {
 //                Assert.assertNull(actual.getDetails());
@@ -292,7 +293,7 @@ public class AssertEvents implements TestRule {
                         Assert.fail(d.getKey() + " missing");
                     }
 
-                    Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
+                    Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, is(d.getValue()));
                 }
                 /*
                 for (String k : actual.getDetails().keySet()) {
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 787db5b..9afade5 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
@@ -17,9 +17,6 @@
 package org.keycloak.testsuite.forms;
 
 import org.jboss.arquillian.graphene.page.Page;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventType;
@@ -51,8 +48,7 @@ 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;
+import static org.junit.Assert.*;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -172,16 +168,34 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
     }
 
     @Test
-    @Ignore
     public void resetPasswordTwice() throws IOException, MessagingException {
         String changePasswordUrl = resetPassword("login-test");
+        events.clear();
+
+        // TODO:hmlnarik is this correct??
+        assertSecondPasswordResetFails(changePasswordUrl, "test-app"); // KC_RESTART exists, hence client-ID is taken from it.
+    }
+
+    @Test
+    public void resetPasswordTwiceInNewBrowser() throws IOException, MessagingException {
+        String changePasswordUrl = resetPassword("login-test");
+        events.clear();
+
+        String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials";
+        driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
+        driver.manage().deleteAllCookies();
+
+        assertSecondPasswordResetFails(changePasswordUrl, null);
+    }
+
+    public void assertSecondPasswordResetFails(String changePasswordUrl, String clientId) {
         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)
+          .client(clientId)
           .session((String) null)
           .user(userId)
           .detail(Details.USERNAME, "login-test")
@@ -337,19 +351,19 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
 
     @Test
     public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException {
-        try {
-            initiateResetPasswordFromResetPasswordPage("login-test");
+        initiateResetPasswordFromResetPasswordPage("login-test");
 
-            events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
-                    .session((String)null)
-                    .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
+        events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+                .session((String)null)
+                .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent();
 
-            assertEquals(1, greenMail.getReceivedMessages().length);
+        assertEquals(1, greenMail.getReceivedMessages().length);
 
-            MimeMessage message = greenMail.getReceivedMessages()[0];
+        MimeMessage message = greenMail.getReceivedMessages()[0];
 
-            String changePasswordUrl = getPasswordResetEmailLink(message);
+        String changePasswordUrl = getPasswordResetEmailLink(message);
 
+        try {
             setTimeOffset(1800 + 23);
 
             driver.navigate().to(changePasswordUrl.trim());
@@ -590,6 +604,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
 
         String changePasswordUrl = getPasswordResetEmailLink(message);
 
+        driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path
         driver.manage().deleteAllCookies();
         driver.navigate().to(changePasswordUrl.trim());