KEYCLOAK-4627 Changes in TokenVerifier to include token in exceptions. Reset credentials uses checks to validate individual token aspects

This commit is contained in:
Hynek Mlnarik 2017-03-30 14:05:41 +02:00 committed by mposolda
parent a9ec69e424
commit b55b089355
12 changed files with 599 additions and 310 deletions

View file

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

View file

@ -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)
.check(RealmUrlCheck.NULL_INSTANCE)
.check(SUBJECT_EXISTS_CHECK)
.check(TokenTypeCheck.INSTANCE_BEARER)
.check(IS_ACTIVE);
return new TokenVerifier(tokenString, clazz);
}
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);
/**
* 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);
}
/**
* 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;
}
};
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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,22 +108,26 @@ 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);
} 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();
try {
tokenFromMail = ResetCredentialsActionToken.deserialize(actionTokenString);
} catch (VerificationException ex) {
context.getEvent().detail(Details.REASON, ex.getMessage());
// flow returns in the next condition so no "return" statmenent here
}
if (tokenFromMail == null) {
context.getEvent()
@ -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;

View file

@ -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();

View file

@ -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,171 +524,192 @@ 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 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.getAuthenticationSession().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 {
String authSessionId = t == null ? null : t.getAuthenticationSessionId();
if (t == null || authSessionId == null) {
event.error(Errors.INVALID_CODE);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
}
AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
t.setAuthenticationSession(authSession);
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
// If not, try to restart authSession from the cookie
AuthenticationSessionModel restartedAuthSession = RestartLoginCookie.restartSession(session, realm);
// IDs must match with the ID from cookie
if (restartedAuthSession!=null && restartedAuthSession.getId().equals(authSessionId)) {
authSession = restartedAuthSession;
}
} catch (Exception e) {
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
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));
}
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()));
return true;
};
}
/**
* 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;
}
@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;
}
if (authSession == null) {
event.error(Errors.INVALID_CODE);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
}
event.detail(Details.CODE_ID, authSession.getId());
return true;
}
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));
/**
* 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> {
private final Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken;
public IsClientValid(Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken) {
this.getAuthenticationSessionFromToken = getAuthenticationSessionFromToken;
}
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));
}
session.getContext().setClient(client);
@Override
public boolean test(T t) throws VerificationException {
AuthenticationSessionModel authenticationSession = getAuthenticationSessionFromToken.apply(t);
return true;
ClientModel client = authenticationSession == null ? null : authenticationSession.getClient();
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 (! client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND);
session.authenticationSessions().removeAuthenticationSession(realm, authenticationSession);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
}
session.getContext().setClient(client);
return true;
}
}
private class IsValidAction implements Predicate<ResetCredentialsActionToken> {
/**
* 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> {
private final String requiredAction;
private final Function<T, String> getAuthenticationSessionIdFromToken;
public IsValidAction(String requiredAction) {
this.requiredAction = requiredAction;
private final BiConsumer<T, AuthenticationSessionModel> setAuthenticationSessionToToken;
public CanResolveAuthenticationSession(Function<T, String> getAuthenticationSessionIdFromToken,
BiConsumer<T, AuthenticationSessionModel> setAuthenticationSessionToToken) {
this.getAuthenticationSessionIdFromToken = getAuthenticationSessionIdFromToken;
this.setAuthenticationSessionToToken = setAuthenticationSessionToToken;
}
@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;
}
}
/**
* 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 defaultClientId;
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 (authSession == null) {
authSession = createAuthenticationSessionForClient(this.defaultClientId);
throw new LoginActionsServiceException(processResetCredentials(false, null, authSession, null));
}
return true;
}
}
/**
* 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 IsActionRequired(Action expectedAction, Function<T, AuthenticationSessionModel> getAuthenticationSessionFromToken) {
this.expectedAction = expectedAction;
this.getAuthenticationSessionFromToken = getAuthenticationSessionFromToken;
}
@Override
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())) {
// 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());
}*/
}
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.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));
}
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.
@ -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());

View file

@ -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()) {

View file

@ -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 {
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();
assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String changePasswordUrl = getPasswordResetEmailLink(message);
try {
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();
assertEquals(1, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
String changePasswordUrl = getPasswordResetEmailLink(message);
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());