KEYCLOAK-4627 Refactor TokenVerifier to support more than just access token checks. Action tokens implementation with reset e-mail action converted to AT

This commit is contained in:
Hynek Mlnarik 2017-03-06 14:45:57 +01:00 committed by mposolda
parent 83b29c5080
commit 19a41c8704
13 changed files with 1171 additions and 344 deletions

View file

@ -29,10 +29,10 @@ import java.security.PublicKey;
*/
public class RSATokenVerifier {
private TokenVerifier tokenVerifier;
private final TokenVerifier<AccessToken> tokenVerifier;
private RSATokenVerifier(String tokenString) {
this.tokenVerifier = TokenVerifier.create(tokenString);
this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class);
}
public static RSATokenVerifier create(String tokenString) {

View file

@ -18,7 +18,8 @@
package org.keycloak;
import org.keycloak.common.VerificationException;
import org.keycloak.jose.jws.Algorithm;
import org.keycloak.exceptions.TokenNotActiveException;
import org.keycloak.exceptions.TokenSignatureInvalidException;
import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput;
@ -26,67 +27,235 @@ import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.jose.jws.crypto.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.TokenUtil;
import javax.crypto.SecretKey;
import java.security.PublicKey;
import java.util.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TokenVerifier {
public class TokenVerifier<T extends JsonWebToken> {
private final String tokenString;
// This interface is here as JDK 7 is a requirement for this project.
// Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead.
// @FunctionalInterface
public static interface Predicate<T extends JsonWebToken> {
/**
* Performs a single check on the given token verifier.
* @param t Token, guaranteed to be non-null.
* @return
* @throws VerificationException
*/
boolean test(T t) throws VerificationException;
}
public static final Predicate<JsonWebToken> SUBJECT_EXISTS_CHECK = new Predicate<JsonWebToken>() {
@Override
public boolean test(JsonWebToken t) throws VerificationException {
String subject = t.getSubject();
if (subject == null) {
throw new VerificationException("Subject missing in token");
}
return true;
}
};
public static final Predicate<JsonWebToken> IS_ACTIVE = new Predicate<JsonWebToken>() {
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (! t.isActive()) {
throw new TokenNotActiveException("Token is not active");
}
return true;
}
};
public static class RealmUrlCheck implements Predicate<JsonWebToken> {
private static final RealmUrlCheck NULL_INSTANCE = new RealmUrlCheck(null);
private final String realmUrl;
public RealmUrlCheck(String realmUrl) {
this.realmUrl = realmUrl;
}
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (this.realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
if (! this.realmUrl.equals(t.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
}
return true;
}
};
public static class TokenTypeCheck implements Predicate<JsonWebToken> {
private static final TokenTypeCheck INSTANCE_BEARER = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_BEARER);
private final String tokenType;
public TokenTypeCheck(String tokenType) {
this.tokenType = tokenType;
}
@Override
public boolean test(JsonWebToken t) throws VerificationException {
if (! tokenType.equalsIgnoreCase(t.getType())) {
throw new VerificationException("Token type is incorrect. Expected '" + tokenType + "' but was '" + t.getType() + "'");
}
return true;
}
};
private String tokenString;
private Class<? extends T> clazz;
private PublicKey publicKey;
private SecretKey secretKey;
private String realmUrl;
private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER;
private boolean checkTokenType = true;
private boolean checkActive = true;
private boolean checkRealmUrl = true;
private final LinkedList<Predicate<? super T>> checks = new LinkedList<>();
private JWSInput jws;
private AccessToken token;
private T token;
protected TokenVerifier(String tokenString) {
protected TokenVerifier(String tokenString, Class<T> clazz) {
this.tokenString = tokenString;
this.clazz = clazz;
}
public static TokenVerifier create(String tokenString) {
return new TokenVerifier(tokenString);
protected TokenVerifier(T token) {
this.token = token;
}
public TokenVerifier publicKey(PublicKey publicKey) {
/**
* Creates a {@code TokenVerifier<AccessToken> instance. The method is here for backwards compatibility.
* @param tokenString
* @return
* @deprecated use {@link #create(java.lang.String, java.lang.Class) } instead
*/
public static TokenVerifier<AccessToken> create(String tokenString) {
return create(tokenString, AccessToken.class);
}
public static <T extends JsonWebToken> TokenVerifier<T> create(String tokenString, Class<T> clazz) {
return new TokenVerifier(tokenString, clazz)
.check(RealmUrlCheck.NULL_INSTANCE)
.check(SUBJECT_EXISTS_CHECK)
.check(TokenTypeCheck.INSTANCE_BEARER)
.check(IS_ACTIVE);
}
public static <T extends JsonWebToken> TokenVerifier<T> from(T token) {
return new TokenVerifier(token)
.check(RealmUrlCheck.NULL_INSTANCE)
.check(SUBJECT_EXISTS_CHECK)
.check(TokenTypeCheck.INSTANCE_BEARER)
.check(IS_ACTIVE);
}
private void removeCheck(Class<? extends Predicate<?>> checkClass) {
for (Iterator<Predicate<? super T>> it = checks.iterator(); it.hasNext();) {
if (it.next().getClass() == checkClass) {
it.remove();
}
}
}
private void removeCheck(Predicate<? super T> check) {
checks.remove(check);
}
private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Class<? extends Predicate<?>> checkClass, boolean active, P predicate) {
removeCheck(checkClass);
if (active) {
checks.add(predicate);
}
return this;
}
private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Predicate<? super T> check, boolean active, P predicate) {
removeCheck(check);
if (active) {
checks.add(predicate);
}
return this;
}
/**
* Resets all preset checks and will test the given checks in {@link #verify()} method.
* @param checks
* @return
*/
public TokenVerifier<T> checkOnly(Predicate<? super T>... checks) {
this.checks.clear();
if (checks != null) {
this.checks.addAll(Arrays.asList(checks));
}
return this;
}
/**
* Will test the given checks in {@link #verify()} method in addition to already set checks.
* @param checks
* @return
*/
public TokenVerifier<T> check(Predicate<? super T>... checks) {
if (checks != null) {
this.checks.addAll(Arrays.asList(checks));
}
return this;
}
public TokenVerifier<T> publicKey(PublicKey publicKey) {
this.publicKey = publicKey;
return this;
}
public TokenVerifier secretKey(SecretKey secretKey) {
public TokenVerifier<T> secretKey(SecretKey secretKey) {
this.secretKey = secretKey;
return this;
}
public TokenVerifier realmUrl(String realmUrl) {
public TokenVerifier<T> realmUrl(String realmUrl) {
this.realmUrl = realmUrl;
return this;
return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl));
}
public TokenVerifier checkTokenType(boolean checkTokenType) {
public TokenVerifier<T> checkTokenType(boolean checkTokenType) {
this.checkTokenType = checkTokenType;
return this;
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
}
public TokenVerifier checkActive(boolean checkActive) {
this.checkActive = checkActive;
return this;
public TokenVerifier<T> tokenType(String tokenType) {
this.expectedTokenType = tokenType;
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
}
public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
public TokenVerifier<T> checkActive(boolean checkActive) {
return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE);
}
public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) {
this.checkRealmUrl = checkRealmUrl;
return this;
return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
}
public TokenVerifier parse() throws VerificationException {
public TokenVerifier<T> parse() throws VerificationException {
if (jws == null) {
if (tokenString == null) {
throw new VerificationException("Token not set");
@ -100,7 +269,7 @@ public class TokenVerifier {
try {
token = jws.readJsonContent(AccessToken.class);
token = jws.readJsonContent(clazz);
} catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e);
}
@ -108,8 +277,10 @@ public class TokenVerifier {
return this;
}
public AccessToken getToken() throws VerificationException {
parse();
public T getToken() throws VerificationException {
if (token == null) {
parse();
}
return token;
}
@ -118,50 +289,43 @@ public class TokenVerifier {
return jws.getHeader();
}
public TokenVerifier verify() throws VerificationException {
parse();
if (checkRealmUrl && realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
public void verifySignature() throws VerificationException {
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
if (AlgorithmType.RSA.equals(algorithmType)) {
if (publicKey == null) {
throw new VerificationException("Public key not set");
}
if (null == algorithmType) {
throw new VerificationException("Unknown or unsupported token algorithm");
} else switch (algorithmType) {
case RSA:
if (publicKey == null) {
throw new VerificationException("Public key not set");
}
if (!RSAProvider.verify(jws, publicKey)) {
throw new TokenSignatureInvalidException("Invalid token signature");
} break;
case HMAC:
if (secretKey == null) {
throw new VerificationException("Secret key not set");
}
if (!HMACProvider.verify(jws, secretKey)) {
throw new TokenSignatureInvalidException("Invalid token signature");
} break;
default:
throw new VerificationException("Unknown or unsupported token algorithm");
}
}
if (!RSAProvider.verify(jws, publicKey)) {
throw new VerificationException("Invalid token signature");
}
} else if (AlgorithmType.HMAC.equals(algorithmType)) {
if (secretKey == null) {
throw new VerificationException("Secret key not set");
}
if (!HMACProvider.verify(jws, secretKey)) {
throw new VerificationException("Invalid token signature");
}
} else {
throw new VerificationException("Unknown or unsupported token algorith");
public TokenVerifier<T> verify() throws VerificationException {
if (getToken() == null) {
parse();
}
if (jws != null) {
verifySignature();
}
String user = token.getSubject();
if (user == null) {
throw new VerificationException("Subject missing in token");
}
if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
}
if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
}
if (checkActive && !token.isActive()) {
throw new VerificationException("Token is not active");
for (Predicate<? super T> check : checks) {
if (! check.test(getToken())) {
throw new VerificationException("JWT check failed for check " + check);
}
}
return this;

View file

@ -0,0 +1,43 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.exceptions;
import org.keycloak.common.VerificationException;
/**
* Exception thrown for cases when token is invalid due to time constraints (expired, or not yet valid).
* Cf. {@link JsonWebToken#isActive()}.
* @author hmlnarik
*/
public class TokenNotActiveException extends VerificationException {
public TokenNotActiveException() {
}
public TokenNotActiveException(String message) {
super(message);
}
public TokenNotActiveException(String message, Throwable cause) {
super(message, cause);
}
public TokenNotActiveException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.exceptions;
import org.keycloak.common.VerificationException;
/**
* Thrown when token signature is invalid.
* @author hmlnarik
*/
public class TokenSignatureInvalidException extends VerificationException {
public TokenSignatureInvalidException() {
}
public TokenSignatureInvalidException(String message) {
super(message);
}
public TokenSignatureInvalidException(String message, Throwable cause) {
super(message, cause);
}
public TokenSignatureInvalidException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.authentication;
import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.common.VerificationException;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.*;
/**
* Part of action token that is intended to be used e.g. in link sent in password-reset email.
* The token encapsulates user, expected action and its time of expiry.
*
* @author hmlnarik
*/
public class DefaultActionToken extends DefaultActionTokenKey {
public static final String JSON_FIELD_ACTION_VERIFICATION_NONCE = "nonce";
public static Predicate<DefaultActionToken> ACTION_TOKEN_BASIC_CHECKS = t -> {
if (t.getActionVerificationNonce() == null) {
throw new VerificationException("Nonce not present.");
}
return true;
};
/**
* Single-use random value used for verification whether the relevant action is allowed.
*/
@JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true)
private final UUID actionVerificationNonce;
public DefaultActionToken(String userId, String actionId, int expirationInSecs) {
this(userId, actionId, expirationInSecs, UUID.randomUUID());
}
/**
*
* @param userId User ID
* @param actionId Action ID
* @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
* @param actionVerificationNonce
*/
protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
super(userId, actionId);
this.actionVerificationNonce = actionVerificationNonce == null ? UUID.randomUUID() : actionVerificationNonce;
expiration = absoluteExpirationInSecs;
}
public UUID getActionVerificationNonce() {
return actionVerificationNonce;
}
@JsonIgnore
public Map<String, String> getNotes() {
Map<String, String> res = new HashMap<>();
return res;
}
public String getNote(String name) {
Object res = getOtherClaims().get(name);
return res instanceof String ? (String) res : null;
}
/**
* Sets value of the given note
* @return original value (or {@code null} when no value was present)
*/
public final String setNote(String name, String value) {
Object res = value == null
? getOtherClaims().remove(name)
: getOtherClaims().put(name, value);
return res instanceof String ? (String) res : null;
}
/**
* Removes given note, and returns original value (or {@code null} when no value was present)
* @return see description
*/
public final String removeNote(String name) {
Object res = getOtherClaims().remove(name);
return res instanceof String ? (String) res : null;
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.authentication;
import org.keycloak.representations.JsonWebToken;
import com.fasterxml.jackson.annotation.JsonIgnore;
/**
*
* @author hmlnarik
*/
public class DefaultActionTokenKey extends JsonWebToken {
public DefaultActionTokenKey(String userId, String actionId) {
subject = userId;
type = actionId;
}
@JsonIgnore
public String getUserId() {
return getSubject();
}
@JsonIgnore
public String getActionId() {
return getType();
}
}

View file

@ -0,0 +1,148 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.authentication;
import org.keycloak.TokenVerifier;
import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.*;
import org.keycloak.models.*;
import org.keycloak.services.Urls;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
/**
* Representation of a token that represents a time-limited reset credentials action.
* <p>
* This implementation handles signature.
*
* @author hmlnarik
*/
public class ResetCredentialsActionToken extends DefaultActionToken {
private static final Logger LOG = Logger.getLogger(ResetCredentialsActionToken.class);
private static final String RESET_CREDENTIALS_ACTION = "reset-credentials";
public static final String NOTE_CLIENT_SESSION_ID = "clientSessionId";
private static final String JSON_FIELD_CLIENT_SESSION_ID = "csid";
private static final String JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP = "lcpt";
@JsonIgnore
private ClientSessionModel clientSession;
@JsonProperty(value = JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP)
private Long lastChangedPasswordTimestamp;
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, String clientSessionId) {
super(userId, RESET_CREDENTIALS_ACTION, absoluteExpirationInSecs, actionVerificationNonce);
setNote(NOTE_CLIENT_SESSION_ID, clientSessionId);
this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
}
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, Long lastChangedPasswordTimestamp, ClientSessionModel clientSession) {
this(userId, absoluteExpirationInSecs, actionVerificationNonce, lastChangedPasswordTimestamp, clientSession == null ? null : clientSession.getId());
this.clientSession = clientSession;
}
private ResetCredentialsActionToken() {
super(null, null, -1, null);
}
public ClientSessionModel getClientSession() {
return this.clientSession;
}
public void setClientSession(ClientSessionModel clientSession) {
this.clientSession = clientSession;
setClientSessionId(clientSession == null ? null : clientSession.getId());
}
@JsonProperty(value = JSON_FIELD_CLIENT_SESSION_ID)
public String getClientSessionId() {
return getNote(NOTE_CLIENT_SESSION_ID);
}
public void setClientSessionId(String clientSessionId) {
setNote(NOTE_CLIENT_SESSION_ID, clientSessionId);
}
public Long getLastChangedPasswordTimestamp() {
return lastChangedPasswordTimestamp;
}
public void setLastChangedPasswordTimestamp(Long lastChangedPasswordTimestamp) {
this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
}
@Override
@JsonIgnore
public Map<String, String> getNotes() {
Map<String, String> res = super.getNotes();
if (this.clientSession != null) {
res.put(NOTE_CLIENT_SESSION_ID, getNote(NOTE_CLIENT_SESSION_ID));
}
return res;
}
public String serialize(KeycloakSession session, RealmModel realm, UriInfo uri) {
String issuerUri = getIssuer(realm, uri);
KeyManager.ActiveHmacKey keys = session.keys().getActiveHmacKey(realm);
this
.issuedAt(Time.currentTime())
.id(getActionVerificationNonce().toString())
.issuer(issuerUri)
.audience(issuerUri);
return new JWSBuilder()
.kid(keys.getKid())
.jsonContent(this)
.hmac512(keys.getSecretKey());
}
private static String getIssuer(RealmModel realm, UriInfo uri) {
return Urls.realmIssuer(uri.getBaseUri(), realm.getName());
}
/**
* Returns a {@code DefaultActionToken} instance decoded from the given string. If decoding fails, returns {@code null}
*
* @param session
* @param actionTokenString
* @return
*/
public static ResetCredentialsActionToken deserialize(KeycloakSession session, RealmModel realm, UriInfo uri, String token,
Predicate<? super ResetCredentialsActionToken>... checks) throws VerificationException {
return TokenVerifier.create(token, ResetCredentialsActionToken.class)
.secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
.realmUrl(getIssuer(realm, uri))
.tokenType(RESET_CREDENTIALS_ACTION)
.checkActive(false) // TODO: If this line is omitted, the following tests in ResetPasswordTest fail: resetPasswordExpiredCodeShort, resetPasswordExpiredCode
.check(ACTION_TOKEN_BASIC_CHECKS)
.check(checks)
.verify()
.getToken()
;
}
}

View file

@ -19,33 +19,27 @@ package org.keycloak.authentication.authenticators.resetcred;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.*;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.credential.*;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import java.util.*;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@ -53,9 +47,6 @@ import java.util.concurrent.TimeUnit;
* @version $Revision: 1 $
*/
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
private static final Logger logger = Logger.getLogger(ResetCredentialEmail.class);
public static final String PROVIDER_ID = "reset-credential-email";
@ -85,15 +76,25 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return;
}
// We send the secret in the email in a link as a query param. We don't need to sign it or anything because
// it can only be guessed once, and it must match watch is stored in the client session.
String secret = HmacOTP.generateSecret(10);
context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString();
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
int validityInSecs = context.getRealm().getAccessCodeLifespanUserAction();
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
CredentialModel password = passwordProvider.getPassword(context.getRealm(), user);
Long lastCreatedPassword = password == null ? null : password.getCreatedDate();
// We send the secret in the email in a link as a query param.
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, null, lastCreatedPassword, context.getClientSession());
KeycloakSession keycloakSession = context.getSession();
String link = UriBuilder
.fromUri(context.getActionUrl())
.queryParam(Constants.KEY, token.serialize(keycloakSession, context.getRealm(), context.getUriInfo()))
.build()
.toString();
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
try {
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration);
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
event.clone().event(EventType.SEND_RESET_PASSWORD)
.user(user)
.detail(Details.USERNAME, username)
@ -114,19 +115,56 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
@Override
public void action(AuthenticationFlowContext context) {
/*String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
// Can only guess once! We remove the note so another guess can't happen
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
if (secret == null || key == null || !secret.equals(key)) {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
/*
KeycloakSession keycloakSession = context.getSession();
String actionTokenString = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
ResetCredentialsActionToken tokenFromMail = null;
try {
tokenFromMail = ResetCredentialsActionToken.deserialize(keycloakSession, context.getRealm(), context.getUriInfo(), actionTokenString);
} catch (VerificationException ex) {
context.getEvent().detail(Details.REASON, ex.getMessage()).error(Errors.INVALID_CODE);
Response challenge = context.form()
.setError(Messages.INVALID_ACCESS_CODE)
.setError(Messages.INVALID_CODE)
.createErrorPage();
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
}
String userId = tokenFromMail == null ? null : tokenFromMail.getUserId();
if (tokenFromMail == null) {
context.getEvent()
.error(Errors.INVALID_CODE);
Response challenge = context.form()
.setError(Messages.INVALID_CODE)
.createErrorPage();
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
return;
}
PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser());
Long lastCreatedPasswordMail = tokenFromMail.getLastChangedPasswordTimestamp();
Long lastCreatedPasswordFromStore = password == null ? null : password.getCreatedDate();
String clientSessionId = tokenFromMail.getClientSessionId();
ClientSessionModel clientSession = clientSessionId == null ? null : keycloakSession.sessions().getClientSession(clientSessionId);
if (clientSession == null
|| ! Objects.equals(lastCreatedPasswordMail, lastCreatedPasswordFromStore)
|| ! Objects.equals(userId, context.getUser().getId())) {
context.getEvent()
.user(userId)
.detail(Details.USERNAME, context.getUser().getUsername())
.detail(Details.TOKEN_ID, tokenFromMail.getId())
.error(Errors.EXPIRED_CODE);
Response challenge = context.form()
.setError(Messages.INVALID_CODE)
.createErrorPage();
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
return;
}
// We now know email is valid, so set it to valid.
context.getUser().setEmailVerified(true);
context.success();*/

View file

@ -154,6 +154,11 @@ public class RestartLoginCookie {
// TODO:mposolda
/*
public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception {
String[] parts = code.split("\\.");
return restartSessionByClientSession(session, realm, parts[1]);
}
public static ClientSessionModel restartSessionByClientSession(KeycloakSession session, RealmModel realm, String clientSessionId) throws Exception {
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
if (cook == null) {
logger.debug("KC_RESTART cookie doesn't exist");
@ -167,8 +172,6 @@ public class RestartLoginCookie {
return null;
}
RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class);
String[] parts = code.split("\\.");
String clientSessionId = parts[1];
if (!clientSessionId.equals(cookie.getClientSession())) {
logger.debug("RestartLoginCookie clientSession does not match code's clientSession");
return null;

View file

@ -127,7 +127,10 @@ public class AuthenticationManager {
if (cookie == null) return;
String tokenString = cookie.getValue();
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
.realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()))
.checkActive(false)
.checkTokenType(false);
String kid = verifier.getHeader().getKeyId();
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
@ -710,7 +713,7 @@ public class AuthenticationManager {
public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers) {
try {
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
String kid = verifier.getHeader().getKeyId();
AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();

View file

@ -24,13 +24,13 @@ import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.TokenVerifier;
import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.authentication.ResetCredentialsActionToken;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@ -48,7 +48,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol;
@ -57,11 +56,14 @@ import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ClientSessionCode.ActionType;
import org.keycloak.services.managers.ClientSessionCode.ParseResult;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.CookieHelper;
@ -84,6 +86,7 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.net.URI;
import java.util.Objects;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -166,24 +169,51 @@ public class LoginActionsService {
}
}
private <C extends CommonClientSessionModel> SessionCodeChecks<C> checksForCode(String code, Class<C> expectedClazz) {
SessionCodeChecks<C> res = new SessionCodeChecks<>(code, expectedClazz);
res.initialVerifyCode();
return res;
}
private class Checks {
// TODO: Merge with Hynek's code. This may not be just loginSession
ClientSessionCode<LoginSessionModel> clientCode;
private class SessionCodeChecks<C extends CommonClientSessionModel> {
ClientSessionCode<C> clientCode;
Response response;
ClientSessionCode.ParseResult result;
ClientSessionCode.ParseResult<C> result;
Class<C> expectedClazz;
boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) {
if (!verifyCode(code)) {
private final String code;
public SessionCodeChecks(String code, Class<C> expectedClazz) {
this.code = code;
this.expectedClazz = expectedClazz;
}
public C getClientSession() {
return clientCode == null ? null : clientCode.getClientSession();
}
public boolean passed() {
return response == null;
}
public boolean failed() {
return response != null;
}
boolean verifyCode(String requiredAction, ClientSessionCode.ActionType actionType) {
if (failed()) {
return false;
}
if (!clientCode.isValidAction(requiredAction)) {
LoginSessionModel loginSession = clientCode.getClientSession();
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(loginSession.getAction())) {
C clientSession = getClientSession();
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
response = redirectToRequiredActions(code);
return false;
} // TODO:mposolda
} // TODO:mposolda
/*else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
response = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN)
@ -191,9 +221,9 @@ public class LoginActionsService {
return false;
}*/
}
if (!isActionActive(actionType)) return false;
return true;
}
return isActionActive(actionType);
}
private boolean isValidAction(String requiredAction) {
if (!clientCode.isValidAction(requiredAction)) {
@ -204,18 +234,19 @@ public class LoginActionsService {
}
private void invalidAction() {
event.client(clientCode.getClientSession().getClient());
event.client(getClientSession().getClient());
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);
}
private boolean isActionActive(ClientSessionCode.ActionType actionType) {
if (!clientCode.isActionActive(actionType)) {
event.client(clientCode.getClientSession().getClient());
event.client(getClientSession().getClient());
event.clone().error(Errors.EXPIRED_CODE);
if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
AuthenticationProcessor.resetFlow(clientCode.getClientSession());
response = processAuthentication(null, clientCode.getClientSession(), Messages.LOGIN_TIMEOUT);
if (getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
AuthenticationProcessor.resetFlow(loginSession);
response = processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT);
return false;
}
response = ErrorPage.error(session, Messages.EXPIRED_CODE);
@ -224,7 +255,7 @@ public class LoginActionsService {
return true;
}
public boolean verifyCode(String code) {
private boolean initialVerifyCode() {
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
@ -235,14 +266,12 @@ public class LoginActionsService {
response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
return false;
}
// TODO:mposolda it may not be just loginSessionModel
result = ClientSessionCode.parseResult(code, session, realm, LoginSessionModel.class);
result = ClientSessionCode.parseResult(code, session, realm, expectedClazz);
clientCode = result.getCode();
if (clientCode == null) {
// TODO:mposolda
/*
if (result.isLoginSessionNotFound()) { // timeout
if (result.isLoginSessionNotFound()) { // timeout or loginSession already logged
// TODO:mposolda
/*
try {
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
if (clientSession != null) {
@ -252,13 +281,14 @@ public class LoginActionsService {
}
} catch (Exception e) {
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
}
}*/
}
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);*/
response = ErrorPage.error(session, Messages.INVALID_CODE);
return false;
}
LoginSessionModel clientSession = clientCode.getClientSession();
C clientSession = getClientSession();
if (clientSession == null) {
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);
@ -269,62 +299,48 @@ public class LoginActionsService {
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
session.loginSessions().removeLoginSession(realm, clientSession);
// TODO:mposolda
//session.sessions().removeClientSession(realm, clientSession);
return false;
}
if (!client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
session.loginSessions().removeLoginSession(realm, clientSession);
// TODO:mposolda
//session.sessions().removeClientSession(realm, clientSession);
return false;
}
session.getContext().setClient(client);
return true;
}
public boolean verifyRequiredAction(String code, String executedAction) {
// TODO:mposolda
/*
if (!verifyCode(code)) {
public boolean verifyRequiredAction(String executedAction) {
if (failed()) {
return false;
}
if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false;
if (!isActionActive(ClientSessionCode.ActionType.USER)) return false;
final ClientSessionModel clientSession = clientCode.getClientSession();
final LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
final UserSessionModel userSession = clientSession.getUserSession();
if (userSession == null) {
ServicesLogger.LOGGER.userSessionNull();
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE));
}
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
return false;
}
if (executedAction == null && userSession != null) { // do next required action only if user is already authenticated
initEvent(clientSession);
if (executedAction == null) { // do next required action only if user is already authenticated
initLoginEvent(loginSession);
event.event(EventType.LOGIN);
response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
response = AuthenticationManager.nextActionAfterAuthentication(session, loginSession, clientConnection, request, uriInfo, event);
return false;
}
if (!executedAction.equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
if (!executedAction.equals(loginSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
logger.debug("required action doesn't match current required action");
clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
loginSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
response = redirectToRequiredActions(code);
return false;
}*/
}
return true;
}
}
/**
* protocol independent login page entry point
*
@ -341,8 +357,8 @@ public class LoginActionsService {
if (loginSession != null && code.equals(loginSession.getNote(LAST_PROCESSED_CODE))) {
// Allow refresh of previous page
} else {
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
@ -402,8 +418,8 @@ public class LoginActionsService {
return authenticate(code, null);
}
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
final ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
@ -422,6 +438,163 @@ public class LoginActionsService {
return null;
}
private boolean isSslUsed(JsonWebToken t) throws VerificationException {
if (! checkSsl()) {
event.error(Errors.SSL_REQUIRED);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.HTTPS_REQUIRED));
}
return true;
}
private boolean isRealmEnabled(JsonWebToken t) throws VerificationException {
if (! realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.REALM_NOT_ENABLED));
}
return true;
}
private boolean isResetCredentialsAllowed(ResetCredentialsActionToken t) throws VerificationException {
if (!realm.isResetPasswordAllowed()) {
event.client(t.getClientSession().getClient());
event.error(Errors.NOT_ALLOWED);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED));
}
return true;
}
private boolean canResolveClientSession(ResetCredentialsActionToken t) throws VerificationException {
// TODO:mposolda
/*
String clientSessionId = t == null ? null : t.getNote(ResetCredentialsActionToken.NOTE_CLIENT_SESSION_ID);
if (t == null || clientSessionId == null) {
event.error(Errors.INVALID_CODE);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
}
ClientSessionModel clientSession = session.sessions().getClientSession(clientSessionId);
t.setClientSession(clientSession);
if (clientSession == null) { // timeout
try {
clientSession = RestartLoginCookie.restartSessionByClientSession(session, realm, clientSessionId);
} catch (Exception e) {
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
}
if (clientSession != null) {
event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
throw new LoginActionsServiceException(processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor()));
}
}
if (clientSession == null) {
event.error(Errors.INVALID_CODE);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
}
event.detail(Details.CODE_ID, clientSession.getId());*/
return true;
}
private boolean canResolveClient(ResetCredentialsActionToken t) throws VerificationException {
ClientModel client = t.getClientSession().getClient();
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
session.sessions().removeClientSession(realm, t.getClientSession());
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
}
if (! client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND);
session.sessions().removeClientSession(realm, t.getClientSession());
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
}
session.getContext().setClient(client);
return true;
}
private class IsValidAction implements Predicate<ResetCredentialsActionToken> {
private final String requiredAction;
public IsValidAction(String requiredAction) {
this.requiredAction = requiredAction;
}
@Override
public boolean test(ResetCredentialsActionToken t) throws VerificationException {
ClientSessionModel clientSession = t.getClientSession();
if (! Objects.equals(clientSession.getAction(), this.requiredAction)) {
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
// TODO: Once login tokens would be implemented, this would have to be rewritten
// String code = clientSession.getNote(ClientSessionCode.ACTIVE_CODE) + "." + clientSession.getId();
String code = clientSession.getNote("active_code") + "." + clientSession.getId();
throw new LoginActionsServiceException(redirectToRequiredActions(code));
} else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
throw new LoginActionsServiceException(
session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN)
.createInfoPage());
}
}
return true;
}
}
private class IsActiveAction implements Predicate<ResetCredentialsActionToken> {
private final ClientSessionCode.ActionType actionType;
public IsActiveAction(ActionType actionType) {
this.actionType = actionType;
}
@Override
public boolean test(ResetCredentialsActionToken t) throws VerificationException {
int timestamp = t.getClientSession().getTimestamp();
if (! isActionActive(actionType, timestamp)) {
event.client(t.getClientSession().getClient());
event.clone().error(Errors.EXPIRED_CODE);
if (t.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
// TODO:mposolda incompatible types
LoginSessionModel loginSession = (LoginSessionModel) t.getClientSession();
AuthenticationProcessor.resetFlow(loginSession);
throw new LoginActionsServiceException(processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT));
}
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.EXPIRED_CODE));
}
return true;
}
public boolean isActionActive(ActionType actionType, int timestamp) {
int lifespan;
switch (actionType) {
case CLIENT:
lifespan = realm.getAccessCodeLifespan();
break;
case LOGIN:
lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction();
break;
case USER:
lifespan = realm.getAccessCodeLifespanUserAction();
break;
default:
throw new IllegalArgumentException();
}
return timestamp + lifespan > Time.currentTime();
}
}
/**
* Endpoint for executing reset credentials flow. If code is null, a client session is created with the account
* service as the client. Successful reset sends you to the account page. Note, account service must be enabled.
@ -433,12 +606,10 @@ public class LoginActionsService {
@Path(RESET_CREDENTIALS_PATH)
@GET
public Response resetCredentialsGET(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
@QueryParam("execution") String execution,
@QueryParam("key") String key) {
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
//
// TODO:mposolda
/*
if (code == null) {
if (code == null && key == null) {
if (!realm.isResetPasswordAllowed()) {
event.event(EventType.RESET_PASSWORD);
event.error(Errors.NOT_ALLOWED);
@ -450,7 +621,7 @@ public class LoginActionsService {
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
//clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
clientSession.setRedirectUri(redirectUri);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
@ -459,32 +630,96 @@ public class LoginActionsService {
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
return processResetCredentials(null, clientSession, null);
}
if (key != null) {
try {
ResetCredentialsActionToken token = ResetCredentialsActionToken.deserialize(
session, realm, session.getContext().getUri(), key);
return resetCredentials(code, token, execution);
} catch (VerificationException ex) {
event.event(EventType.RESET_PASSWORD)
.detail(Details.REASON, ex.getMessage())
.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
}
return resetCredentials(code, execution);
*/
return null;
}
/*
/**
* @deprecated In favor of {@link #resetCredentials(String, org.keycloak.authentication.ResetCredentialsActionToken, java.lang.String)}
* @param code
* @param execution
* @return
*/
protected Response resetCredentials(String code, String execution) {
event.event(EventType.RESET_PASSWORD);
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
final ClientSessionModel clientSession = clientCode.getClientSession();
final LoginSessionModel clientSession = checks.getClientSession();
if (!realm.isResetPasswordAllowed()) {
event.client(clientCode.getClientSession().getClient());
event.client(clientSession.getClient());
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
// TODO:mposolda
//return processResetCredentials(execution, clientSession, null);
return null;
}
protected Response resetCredentials(String code, ResetCredentialsActionToken token, String execution) {
event.event(EventType.RESET_PASSWORD);
if (token == null) {
// TODO: Use more appropriate code
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
try {
TokenVerifier.from(token).checkOnly(
// Start basic checks
this::isRealmEnabled,
this::isSslUsed,
this::isResetCredentialsAllowed,
this::canResolveClientSession,
this::canResolveClient,
// End basic checks
new IsValidAction(ClientSessionModel.Action.AUTHENTICATE.name()),
new IsActiveAction(ActionType.USER)
).verify();
} catch (LoginActionsServiceException ex) {
if (ex.getResponse() == null) {
event.event(EventType.RESET_PASSWORD)
.detail(Details.REASON, ex.getMessage())
.error(Errors.INVALID_REQUEST);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
} else {
return ex.getResponse();
}
} catch (VerificationException ex) {
event.event(EventType.RESET_PASSWORD)
.detail(Details.REASON, ex.getMessage())
.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
final ClientSessionModel clientSession = token.getClientSession();
return processResetCredentials(execution, clientSession, null);
}
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
// TODO:mposolda
/*
AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
@Override
@ -507,7 +742,9 @@ public class LoginActionsService {
};
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
}*/
*/
return null;
}
protected Response processRegistration(String execution, LoginSessionModel loginSession, String errorMessage) {
@ -531,8 +768,8 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
@ -561,14 +798,14 @@ public class LoginActionsService {
event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
SessionCodeChecks checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
LoginSessionModel loginSession = clientCode.getClientSession();
return processRegistration(execution, loginSession, null);
}
@ -607,13 +844,12 @@ public class LoginActionsService {
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType);
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
ClientSessionCode clientSessionCode = checks.clientCode;
final ClientSessionModel clientSessionn = clientSessionCode.getClientSession();
final ClientSessionModel clientSessionn = checks.getClientSession();
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
@ -681,10 +917,11 @@ public class LoginActionsService {
public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN);
String code = formData.getFirst("code");
Checks checks = new Checks();
if (!checks.verifyRequiredAction(code, ClientSessionModel.Action.OAUTH_GRANT.name())) {
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
return checks.response;
}
ClientSessionCode<LoginSessionModel> accessCode = checks.clientCode;
LoginSessionModel loginSession = accessCode.getClientSession();
@ -750,16 +987,15 @@ public class LoginActionsService {
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
if (checks.clientCode == null && checks.result.isClientSessionNotFound() || checks.result.isIllegalHash()) {
return ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK);
}
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
clientSession = accessCode.getClientSession();
clientSession = checks.getClientSession();
if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
ServicesLogger.LOGGER.reqdActionDoesNotMatch();
event.error(Errors.INVALID_CODE);
@ -789,12 +1025,12 @@ public class LoginActionsService {
return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
} else {
Checks checks = new Checks();
if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession();
ClientSessionModel clientSession = checks.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
initEvent(clientSession);
@ -824,11 +1060,11 @@ public class LoginActionsService {
/*
event.event(EventType.EXECUTE_ACTIONS);
if (key != null) {
Checks checks = new Checks();
if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
SessionCodeChecks checks = checksForCode(key);
if (!checks.verifyCode(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
ClientSessionModel clientSession = checks.clientCode.getClientSession();
ClientSessionModel clientSession = checks.getClientSession();
// verify user email as we know it is valid as this entry point would never have gotten here.
clientSession.getUserSession().getUser().setEmailVerified(true);
clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
@ -936,12 +1172,11 @@ public class LoginActionsService {
/*
event.event(EventType.CUSTOM_REQUIRED_ACTION);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
Checks checks = new Checks();
if (!checks.verifyRequiredAction(code, action)) {
SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyRequiredAction(action)) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
final ClientSessionModel clientSession = clientCode.getClientSession();
final ClientSessionModel clientSession = checks.getClientSession();
final UserSessionModel userSession = clientSession.getUserSession();

View file

@ -0,0 +1,53 @@
/*
* Copyright 2017 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.resources;
import org.keycloak.common.VerificationException;
import javax.ws.rs.core.Response;
/**
*
* @author hmlnarik
*/
public class LoginActionsServiceException extends VerificationException {
private final Response response;
public LoginActionsServiceException(Response response) {
this.response = response;
}
public LoginActionsServiceException(Response response, String message) {
super(message);
this.response = response;
}
public LoginActionsServiceException(Response response, String message, Throwable cause) {
super(message, cause);
this.response = response;
}
public LoginActionsServiceException(Response response, Throwable cause) {
super(cause);
this.response = response;
}
public Response getResponse() {
return response;
}
}

View file

@ -50,6 +50,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@ -74,6 +75,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
expectedMessagesCount = 0;
getCleanup().addUserId(userId);
}
@ -104,6 +106,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
private int expectedMessagesCount;
@Test
public void resetPasswordLink() throws IOException, MessagingException {
String username = "login-test";
@ -167,6 +171,24 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPassword("login-test");
}
@Test
@Ignore
public void resetPasswordTwice() throws IOException, MessagingException {
String changePasswordUrl = resetPassword("login-test");
driver.navigate().to(changePasswordUrl.trim());
errorPage.assertCurrent();
assertEquals("An error occurred, please login again through your application.", errorPage.getError());
events.expect(EventType.RESET_PASSWORD)
.client((String) null)
.session((String) null)
.user(userId)
.detail(Details.USERNAME, "login-test")
.error(Errors.EXPIRED_CODE)
.assertEvent();
}
@Test
public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
resetPassword(" login-test ");
@ -174,15 +196,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Test
public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("test-user@localhost");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
initiateResetPasswordFromResetPasswordPage("test-user@localhost");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost")
.session((String) null)
@ -206,16 +220,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPassword("login@test.com");
}
private void resetPassword(String username) throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
private String resetPassword(String username) throws IOException, MessagingException {
return resetPassword(username, "resetPassword");
}
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
private String resetPassword(String username, String password) throws IOException, MessagingException {
initiateResetPasswordFromResetPasswordPage(username);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.user(userId)
@ -224,9 +234,9 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.session((String)null)
.assertEvent();
assertEquals(1, greenMail.getReceivedMessages().length);
assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[0];
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
@ -234,7 +244,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("resetPassword", "resetPassword");
updatePasswordPage.changePassword(password, password);
String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
@ -248,63 +258,27 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
loginPage.open();
loginPage.login("login-test", "resetPassword");
loginPage.login("login-test", password);
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
}
private void resetPassword(String username, String password) throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword(password, password);
String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId)
.detail(Details.USERNAME, username).assertEvent().getSessionId();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
oauth.openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
return changePasswordUrl;
}
private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
initiateResetPasswordFromResetPasswordPage(username);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message);
@ -320,17 +294,22 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
}
@Test
public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
public void initiateResetPasswordFromResetPasswordPage(String username) {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("invalid");
resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
expectedMessagesCount++;
}
@Test
public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
initiateResetPasswordFromResetPasswordPage("invalid");
assertEquals(0, greenMail.getReceivedMessages().length);
@ -359,15 +338,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Test
public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException {
try {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
initiateResetPasswordFromResetPasswordPage("login-test");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null)
@ -403,15 +374,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
testRealm().update(realmRep);
try {
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
initiateResetPasswordFromResetPasswordPage("login-test");
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null)
@ -434,55 +397,50 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent();
} finally {
setTimeOffset(0);
realmRep.setAccessCodeLifespanUserAction(originalValue.get());
testRealm().update(realmRep);
}
}
@Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
UserRepresentation user = findUser("login-test");
user.setEnabled(false);
updateUser(user);
try {
user.setEnabled(false);
updateUser(user);
loginPage.open();
loginPage.resetPassword();
initiateResetPasswordFromResetPasswordPage("login-test");
resetPasswordPage.assertCurrent();
assertEquals(0, greenMail.getReceivedMessages().length);
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
assertEquals(0, greenMail.getReceivedMessages().length);
events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
user.setEnabled(true);
updateUser(user);
events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
} finally {
user.setEnabled(true);
updateUser(user);
}
}
@Test
public void resetPasswordNoEmail() throws IOException, MessagingException, InterruptedException {
final String[] email = new String[1];
final String email;
UserRepresentation user = findUser("login-test");
email[0] = user.getEmail();
user.setEmail("");
updateUser(user);
email = user.getEmail();
loginPage.open();
loginPage.resetPassword();
try {
user.setEmail("");
updateUser(user);
resetPasswordPage.assertCurrent();
initiateResetPasswordFromResetPasswordPage("login-test");
resetPasswordPage.changePassword("login-test");
assertEquals(0, greenMail.getReceivedMessages().length);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
assertEquals(0, greenMail.getReceivedMessages().length);
events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
} finally {
user.setEmail(email);
updateUser(user);
}
}
@Test
@ -496,29 +454,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
RealmRepresentation realmRep = testRealm().toRepresentation();
Map<String, String> oldSmtp = realmRep.getSmtpServer();
realmRep.setSmtpServer(smtpConfig);
testRealm().update(realmRep);
try {
realmRep.setSmtpServer(smtpConfig);
testRealm().update(realmRep);
loginPage.open();
loginPage.resetPassword();
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
resetPasswordPage.changePassword("login-test");
errorPage.assertCurrent();
errorPage.assertCurrent();
assertEquals("Failed to send email, please try again later.", errorPage.getError());
assertEquals("Failed to send email, please try again later.", errorPage.getError());
assertEquals(0, greenMail.getReceivedMessages().length);
assertEquals(0, greenMail.getReceivedMessages().length);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
.session((String)null)
.detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
// Revert SMTP back
realmRep.setSmtpServer(oldSmtp);
testRealm().update(realmRep);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
.session((String)null)
.detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
} finally {
// Revert SMTP back
realmRep.setSmtpServer(oldSmtp);
testRealm().update(realmRep);
}
}
private void setPasswordPolicy(String policy) {
@ -531,15 +491,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException {
setPasswordPolicy("length");
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
initiateResetPasswordFromResetPasswordPage("login-test");
assertEquals(1, greenMail.getReceivedMessages().length);