keycloak-aplcache

KEYCLOAK-4627 Refactor TokenVerifier to support more than

3/6/2017 10:45:57 AM

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) {
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);