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