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 authored and mposolda committed 2017-05-11 22:16:26 +02:00
1 parent 83b29c5080
commit 19a41c8704
13 files changed
+1171 -344

No files matched your search

@@ -29,10 +29,10 @@ import java.security.PublicKey;
*/ */
public class RSATokenVerifier { public class RSATokenVerifier {
private TokenVerifier tokenVerifier; private final TokenVerifier<AccessToken> tokenVerifier;
private RSATokenVerifier(String tokenString) { private RSATokenVerifier(String tokenString) {
this.tokenVerifier = TokenVerifier.create(tokenString); this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class);
} }
public static RSATokenVerifier create(String tokenString) { public static RSATokenVerifier create(String tokenString) {
@@ -18,7 +18,8 @@
package org.keycloak; package org.keycloak;
import org.keycloak.common.VerificationException; 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.AlgorithmType;
import org.keycloak.jose.jws.JWSHeader; import org.keycloak.jose.jws.JWSHeader;
import org.keycloak.jose.jws.JWSInput; 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.HMACProvider;
import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.TokenUtil; import org.keycloak.util.TokenUtil;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.*;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @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 PublicKey publicKey;
private SecretKey secretKey; private SecretKey secretKey;
private String realmUrl; private String realmUrl;
private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER;
private boolean checkTokenType = true; private boolean checkTokenType = true;
private boolean checkActive = true;
private boolean checkRealmUrl = true; private boolean checkRealmUrl = true;
private final LinkedList<Predicate<? super T>> checks = new LinkedList<>();
private JWSInput jws; private JWSInput jws;
private AccessToken token; private T token;
protected TokenVerifier(String tokenString) { protected TokenVerifier(String tokenString, Class<T> clazz) {
this.tokenString = tokenString; this.tokenString = tokenString;
this.clazz = clazz;
} }
public static TokenVerifier create(String tokenString) { protected TokenVerifier(T token) {
return new TokenVerifier(tokenString); 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; this.publicKey = publicKey;
return this; return this;
} }
public TokenVerifier secretKey(SecretKey secretKey) { public TokenVerifier<T> secretKey(SecretKey secretKey) {
this.secretKey = secretKey; this.secretKey = secretKey;
return this; return this;
} }
public TokenVerifier realmUrl(String realmUrl) { public TokenVerifier<T> realmUrl(String realmUrl) {
this.realmUrl = 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; this.checkTokenType = checkTokenType;
return this; return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
} }
public TokenVerifier checkActive(boolean checkActive) { public TokenVerifier<T> tokenType(String tokenType) {
this.checkActive = checkActive; this.expectedTokenType = tokenType;
return this; 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; 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 (jws == null) {
if (tokenString == null) { if (tokenString == null) {
throw new VerificationException("Token not set"); throw new VerificationException("Token not set");
@@ -100,7 +269,7 @@ public class TokenVerifier {
try { try {
token = jws.readJsonContent(AccessToken.class); token = jws.readJsonContent(clazz);
} catch (JWSInputException e) { } catch (JWSInputException e) {
throw new VerificationException("Failed to read access token from JWT", e); throw new VerificationException("Failed to read access token from JWT", e);
} }
@@ -108,8 +277,10 @@ public class TokenVerifier {
return this; return this;
} }
public AccessToken getToken() throws VerificationException { public T getToken() throws VerificationException {
parse(); if (token == null) {
parse();
}
return token; return token;
} }
@@ -118,50 +289,43 @@ public class TokenVerifier {
return jws.getHeader(); return jws.getHeader();
} }
public TokenVerifier verify() throws VerificationException { public void verifySignature() throws VerificationException {
parse();
if (checkRealmUrl && realmUrl == null) {
throw new VerificationException("Realm URL not set");
}
AlgorithmType algorithmType = getHeader().getAlgorithm().getType(); AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
if (AlgorithmType.RSA.equals(algorithmType)) { if (null == algorithmType) {
if (publicKey == null) { throw new VerificationException("Unknown or unsupported token algorithm");
throw new VerificationException("Public key not set"); } 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)) { public TokenVerifier<T> verify() throws VerificationException {
throw new VerificationException("Invalid token signature"); if (getToken() == null) {
} parse();
} else if (AlgorithmType.HMAC.equals(algorithmType)) { }
if (secretKey == null) { if (jws != null) {
throw new VerificationException("Secret key not set"); verifySignature();
}
if (!HMACProvider.verify(jws, secretKey)) {
throw new VerificationException("Invalid token signature");
}
} else {
throw new VerificationException("Unknown or unsupported token algorith");
} }
String user = token.getSubject(); for (Predicate<? super T> check : checks) {
if (user == null) { if (! check.test(getToken())) {
throw new VerificationException("Subject missing in token"); throw new VerificationException("JWT check failed for check " + check);
} }
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");
} }
return this; return this;
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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()
;
}
}
@@ -19,33 +19,27 @@ package org.keycloak.authentication.authenticators.resetcred;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.*;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; 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.EmailException;
import org.keycloak.email.EmailTemplateProvider; import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.*;
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.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.HmacOTP;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.LoginActionsService;
import java.util.*;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@@ -53,9 +47,6 @@ import java.util.concurrent.TimeUnit;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory { 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"; public static final String PROVIDER_ID = "reset-credential-email";
@@ -85,15 +76,25 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return; 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 int validityInSecs = context.getRealm().getAccessCodeLifespanUserAction();
// it can only be guessed once, and it must match watch is stored in the client session. int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
String secret = HmacOTP.generateSecret(10);
context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret); PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString(); CredentialModel password = passwordProvider.getPassword(context.getRealm(), user);
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction()); 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 { 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) event.clone().event(EventType.SEND_RESET_PASSWORD)
.user(user) .user(user)
.detail(Details.USERNAME, username) .detail(Details.USERNAME, username)
@@ -114,19 +115,56 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
@Override @Override
public void action(AuthenticationFlowContext context) { public void action(AuthenticationFlowContext context) {
/*String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET); /*
String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY); KeycloakSession keycloakSession = context.getSession();
String actionTokenString = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
// Can only guess once! We remove the note so another guess can't happen ResetCredentialsActionToken tokenFromMail = null;
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET); try {
if (secret == null || key == null || !secret.equals(key)) { tokenFromMail = ResetCredentialsActionToken.deserialize(keycloakSession, context.getRealm(), context.getUriInfo(), actionTokenString);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); } catch (VerificationException ex) {
context.getEvent().detail(Details.REASON, ex.getMessage()).error(Errors.INVALID_CODE);
Response challenge = context.form() 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(); .createErrorPage();
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge); context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
return; 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. // We now know email is valid, so set it to valid.
context.getUser().setEmailVerified(true); context.getUser().setEmailVerified(true);
context.success();*/ context.success();*/
@@ -154,6 +154,11 @@ public class RestartLoginCookie {
// TODO:mposolda // TODO:mposolda
/* /*
public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception { 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); Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
if (cook == null) { if (cook == null) {
logger.debug("KC_RESTART cookie doesn't exist"); logger.debug("KC_RESTART cookie doesn't exist");
@@ -167,8 +172,6 @@ public class RestartLoginCookie {
return null; return null;
} }
RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class); RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class);
String[] parts = code.split("\\.");
String clientSessionId = parts[1];
if (!clientSessionId.equals(cookie.getClientSession())) { if (!clientSessionId.equals(cookie.getClientSession())) {
logger.debug("RestartLoginCookie clientSession does not match code's clientSession"); logger.debug("RestartLoginCookie clientSession does not match code's clientSession");
return null; return null;
@@ -127,7 +127,10 @@ public class AuthenticationManager {
if (cookie == null) return; if (cookie == null) return;
String tokenString = cookie.getValue(); 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(); String kid = verifier.getHeader().getKeyId();
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid); 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, public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
boolean isCookie, String tokenString, HttpHeaders headers) { boolean isCookie, String tokenString, HttpHeaders headers) {
try { 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(); String kid = verifier.getHeader().getKeyId();
AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType(); AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();
@@ -24,13 +24,13 @@ import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult; import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; 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.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.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
@@ -48,7 +48,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.LoginProtocol; 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.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; 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.messages.Messages;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.CookieHelper; 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.core.UriInfo;
import javax.ws.rs.ext.Providers; import javax.ws.rs.ext.Providers;
import java.net.URI; import java.net.URI;
import java.util.Objects;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @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; Response response;
ClientSessionCode.ParseResult result; ClientSessionCode.ParseResult<C> result;
Class<C> expectedClazz;
boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) { private final String code;
if (!verifyCode(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; return false;
} }
if (!clientCode.isValidAction(requiredAction)) { if (!clientCode.isValidAction(requiredAction)) {
LoginSessionModel loginSession = clientCode.getClientSession(); C clientSession = getClientSession();
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(loginSession.getAction())) { if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
response = redirectToRequiredActions(code); response = redirectToRequiredActions(code);
return false; return false;
} // TODO:mposolda
} // TODO:mposolda
/*else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) { /*else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
response = session.getProvider(LoginFormsProvider.class) response = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN) .setSuccess(Messages.ALREADY_LOGGED_IN)
@@ -191,9 +221,9 @@ public class LoginActionsService {
return false; return false;
}*/ }*/
} }
if (!isActionActive(actionType)) return false;
return true; return isActionActive(actionType);
} }
private boolean isValidAction(String requiredAction) { private boolean isValidAction(String requiredAction) {
if (!clientCode.isValidAction(requiredAction)) { if (!clientCode.isValidAction(requiredAction)) {
@@ -204,18 +234,19 @@ public class LoginActionsService {
} }
private void invalidAction() { private void invalidAction() {
event.client(clientCode.getClientSession().getClient()); event.client(getClientSession().getClient());
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE); response = ErrorPage.error(session, Messages.INVALID_CODE);
} }
private boolean isActionActive(ClientSessionCode.ActionType actionType) { private boolean isActionActive(ClientSessionCode.ActionType actionType) {
if (!clientCode.isActionActive(actionType)) { if (!clientCode.isActionActive(actionType)) {
event.client(clientCode.getClientSession().getClient()); event.client(getClientSession().getClient());
event.clone().error(Errors.EXPIRED_CODE); event.clone().error(Errors.EXPIRED_CODE);
if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) { if (getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
AuthenticationProcessor.resetFlow(clientCode.getClientSession()); LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
response = processAuthentication(null, clientCode.getClientSession(), Messages.LOGIN_TIMEOUT); AuthenticationProcessor.resetFlow(loginSession);
response = processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT);
return false; return false;
} }
response = ErrorPage.error(session, Messages.EXPIRED_CODE); response = ErrorPage.error(session, Messages.EXPIRED_CODE);
@@ -224,7 +255,7 @@ public class LoginActionsService {
return true; return true;
} }
public boolean verifyCode(String code) { private boolean initialVerifyCode() {
if (!checkSsl()) { if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED); event.error(Errors.SSL_REQUIRED);
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED); response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
@@ -235,14 +266,12 @@ public class LoginActionsService {
response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED); response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
return false; return false;
} }
result = ClientSessionCode.parseResult(code, session, realm, expectedClazz);
// TODO:mposolda it may not be just loginSessionModel
result = ClientSessionCode.parseResult(code, session, realm, LoginSessionModel.class);
clientCode = result.getCode(); clientCode = result.getCode();
if (clientCode == null) { if (clientCode == null) {
// TODO:mposolda if (result.isLoginSessionNotFound()) { // timeout or loginSession already logged
/* // TODO:mposolda
if (result.isLoginSessionNotFound()) { // timeout /*
try { try {
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code); ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
if (clientSession != null) { if (clientSession != null) {
@@ -252,13 +281,14 @@ public class LoginActionsService {
} }
} catch (Exception e) { } catch (Exception e) {
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e); ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
} }*/
} }
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);*/ response = ErrorPage.error(session, Messages.INVALID_CODE);
return false; return false;
} }
LoginSessionModel clientSession = clientCode.getClientSession();
C clientSession = getClientSession();
if (clientSession == null) { if (clientSession == null) {
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE); response = ErrorPage.error(session, Messages.INVALID_CODE);
@@ -269,62 +299,48 @@ public class LoginActionsService {
if (client == null) { if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER); response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
session.loginSessions().removeLoginSession(realm, clientSession); // TODO:mposolda
//session.sessions().removeClientSession(realm, clientSession);
return false; return false;
} }
if (!client.isEnabled()) { if (!client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND); event.error(Errors.CLIENT_NOT_FOUND);
response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED); response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
session.loginSessions().removeLoginSession(realm, clientSession); // TODO:mposolda
//session.sessions().removeClientSession(realm, clientSession);
return false; return false;
} }
session.getContext().setClient(client); session.getContext().setClient(client);
return true; return true;
} }
public boolean verifyRequiredAction(String code, String executedAction) { public boolean verifyRequiredAction(String executedAction) {
// TODO:mposolda if (failed()) {
/*
if (!verifyCode(code)) {
return false; return false;
} }
if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false; if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false;
if (!isActionActive(ClientSessionCode.ActionType.USER)) 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 (executedAction == null) { // do next required action only if user is already authenticated
if (userSession == null) { initLoginEvent(loginSession);
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);
event.event(EventType.LOGIN); 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; 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"); 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); response = redirectToRequiredActions(code);
return false; return false;
}*/ }
return true; return true;
} }
} }
/** /**
* protocol independent login page entry point * protocol independent login page entry point
* *
@@ -341,8 +357,8 @@ public class LoginActionsService {
if (loginSession != null && code.equals(loginSession.getNote(LAST_PROCESSED_CODE))) { if (loginSession != null && code.equals(loginSession.getNote(LAST_PROCESSED_CODE))) {
// Allow refresh of previous page // Allow refresh of previous page
} else { } else {
Checks checks = new Checks(); SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response; return checks.response;
} }
@@ -402,8 +418,8 @@ public class LoginActionsService {
return authenticate(code, null); return authenticate(code, null);
} }
Checks checks = new Checks(); SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response; return checks.response;
} }
final ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode; final ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
@@ -422,6 +438,163 @@ public class LoginActionsService {
return null; 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 * 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. * 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) @Path(RESET_CREDENTIALS_PATH)
@GET @GET
public Response resetCredentialsGET(@QueryParam("code") String code, 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 // we allow applications to link to reset credentials without going through OAuth or SAML handshakes
// if (code == null && key == null) {
// TODO:mposolda
/*
if (code == null) {
if (!realm.isResetPasswordAllowed()) { if (!realm.isResetPasswordAllowed()) {
event.event(EventType.RESET_PASSWORD); event.event(EventType.RESET_PASSWORD);
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
@@ -450,7 +621,7 @@ public class LoginActionsService {
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
//clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); //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(); String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
clientSession.setRedirectUri(redirectUri); clientSession.setRedirectUri(redirectUri);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
@@ -459,32 +630,96 @@ public class LoginActionsService {
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
return processResetCredentials(null, clientSession, null); 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 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) { protected Response resetCredentials(String code, String execution) {
event.event(EventType.RESET_PASSWORD); event.event(EventType.RESET_PASSWORD);
Checks checks = new Checks(); SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) { if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.response; return checks.response;
} }
final ClientSessionCode clientCode = checks.clientCode; final LoginSessionModel clientSession = checks.getClientSession();
final ClientSessionModel clientSession = clientCode.getClientSession();
if (!realm.isResetPasswordAllowed()) { if (!realm.isResetPasswordAllowed()) {
event.client(clientCode.getClientSession().getClient()); event.client(clientSession.getClient());
event.error(Errors.NOT_ALLOWED); event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_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); return processResetCredentials(execution, clientSession, null);
} }
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) { protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
// TODO:mposolda
/*
AuthenticationProcessor authProcessor = new AuthenticationProcessor() { AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
@Override @Override
@@ -507,7 +742,9 @@ public class LoginActionsService {
}; };
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor); return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
}*/ */
return null;
}
protected Response processRegistration(String execution, LoginSessionModel loginSession, String errorMessage) { protected Response processRegistration(String execution, LoginSessionModel loginSession, String errorMessage) {
@@ -531,8 +768,8 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED); return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
} }
Checks checks = new Checks(); SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response; return checks.response;
} }
event.detail(Details.CODE_ID, code); event.detail(Details.CODE_ID, code);
@@ -561,14 +798,14 @@ public class LoginActionsService {
event.error(Errors.REGISTRATION_DISABLED); event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED); return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
} }
Checks checks = new Checks(); SessionCodeChecks checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response; return checks.response;
} }
ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode; ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
LoginSessionModel loginSession = clientCode.getClientSession(); LoginSessionModel loginSession = clientCode.getClientSession();
return processRegistration(execution, loginSession, null); 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; EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType); event.event(eventType);
Checks checks = new Checks(); SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response; return checks.response;
} }
event.detail(Details.CODE_ID, code); event.detail(Details.CODE_ID, code);
ClientSessionCode clientSessionCode = checks.clientCode; final ClientSessionModel clientSessionn = checks.getClientSession();
final ClientSessionModel clientSessionn = clientSessionCode.getClientSession();
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT; String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey); SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
@@ -681,10 +917,11 @@ public class LoginActionsService {
public Response processConsent(final MultivaluedMap<String, String> formData) { public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN); event.event(EventType.LOGIN);
String code = formData.getFirst("code"); String code = formData.getFirst("code");
Checks checks = new Checks(); SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
if (!checks.verifyRequiredAction(code, ClientSessionModel.Action.OAUTH_GRANT.name())) { if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
return checks.response; return checks.response;
} }
ClientSessionCode<LoginSessionModel> accessCode = checks.clientCode; ClientSessionCode<LoginSessionModel> accessCode = checks.clientCode;
LoginSessionModel loginSession = accessCode.getClientSession(); LoginSessionModel loginSession = accessCode.getClientSession();
@@ -750,16 +987,15 @@ public class LoginActionsService {
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY); clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
Checks checks = new Checks(); SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) { if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
if (checks.clientCode == null && checks.result.isClientSessionNotFound() || checks.result.isIllegalHash()) { if (checks.clientCode == null && checks.result.isClientSessionNotFound() || checks.result.isIllegalHash()) {
return ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK); return ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK);
} }
return checks.response; return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode; clientSession = checks.getClientSession();
clientSession = accessCode.getClientSession();
if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) { if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
ServicesLogger.LOGGER.reqdActionDoesNotMatch(); ServicesLogger.LOGGER.reqdActionDoesNotMatch();
event.error(Errors.INVALID_CODE); event.error(Errors.INVALID_CODE);
@@ -789,12 +1025,12 @@ public class LoginActionsService {
return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo); return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
} else { } else {
Checks checks = new Checks(); SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) { if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response; return checks.response;
} }
ClientSessionCode accessCode = checks.clientCode; ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession(); ClientSessionModel clientSession = checks.getClientSession();
UserSessionModel userSession = clientSession.getUserSession(); UserSessionModel userSession = clientSession.getUserSession();
initEvent(clientSession); initEvent(clientSession);
@@ -824,11 +1060,11 @@ public class LoginActionsService {
/* /*
event.event(EventType.EXECUTE_ACTIONS); event.event(EventType.EXECUTE_ACTIONS);
if (key != null) { if (key != null) {
Checks checks = new Checks(); SessionCodeChecks checks = checksForCode(key);
if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) { if (!checks.verifyCode(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
return checks.response; 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. // verify user email as we know it is valid as this entry point would never have gotten here.
clientSession.getUserSession().getUser().setEmailVerified(true); clientSession.getUserSession().getUser().setEmailVerified(true);
clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
@@ -936,12 +1172,11 @@ public class LoginActionsService {
/* /*
event.event(EventType.CUSTOM_REQUIRED_ACTION); event.event(EventType.CUSTOM_REQUIRED_ACTION);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action); event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
Checks checks = new Checks(); SessionCodeChecks checks = checksForCode(code);
if (!checks.verifyRequiredAction(code, action)) { if (!checks.verifyRequiredAction(action)) {
return checks.response; return checks.response;
} }
final ClientSessionCode clientCode = checks.clientCode; final ClientSessionModel clientSession = checks.getClientSession();
final ClientSessionModel clientSession = clientCode.getClientSession();
final UserSessionModel userSession = clientSession.getUserSession(); final UserSessionModel userSession = clientSession.getUserSession();
@@ -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;
}
}
@@ -50,6 +50,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.*;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -74,6 +75,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.build(); .build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
expectedMessagesCount = 0;
getCleanup().addUserId(userId); getCleanup().addUserId(userId);
} }
@@ -104,6 +106,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Rule @Rule
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
private int expectedMessagesCount;
@Test @Test
public void resetPasswordLink() throws IOException, MessagingException { public void resetPasswordLink() throws IOException, MessagingException {
String username = "login-test"; String username = "login-test";
@@ -167,6 +171,24 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPassword("login-test"); 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 @Test
public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException { public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
resetPassword(" login-test "); resetPassword(" login-test ");
@@ -174,15 +196,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void resetPasswordCancelChangeUser() throws IOException, MessagingException { public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
loginPage.open(); initiateResetPasswordFromResetPasswordPage("test-user@localhost");
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("test-user@localhost");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost") events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost")
.session((String) null) .session((String) null)
@@ -206,16 +220,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPassword("login@test.com"); resetPassword("login@test.com");
} }
private void resetPassword(String username) throws IOException, MessagingException { private String resetPassword(String username) throws IOException, MessagingException {
loginPage.open(); return resetPassword(username, "resetPassword");
loginPage.resetPassword(); }
resetPasswordPage.assertCurrent(); private String resetPassword(String username, String password) throws IOException, MessagingException {
initiateResetPasswordFromResetPasswordPage(username);
resetPasswordPage.changePassword(username);
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.user(userId) .user(userId)
@@ -224,9 +234,9 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
.session((String)null) .session((String)null)
.assertEvent(); .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); String changePasswordUrl = getPasswordResetEmailLink(message);
@@ -234,7 +244,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
updatePasswordPage.assertCurrent(); 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(); 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.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()); 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(); oauth.openLogout();
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent(); events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
return changePasswordUrl;
} }
private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException { private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException {
loginPage.open(); initiateResetPasswordFromResetPasswordPage(username);
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) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent(); .detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = getPasswordResetEmailLink(message); 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(); events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
} }
@Test public void initiateResetPasswordFromResetPasswordPage(String username) {
public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
loginPage.open(); loginPage.open();
loginPage.resetPassword(); loginPage.resetPassword();
resetPasswordPage.assertCurrent(); resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("invalid"); resetPasswordPage.changePassword(username);
loginPage.assertCurrent(); loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); 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); assertEquals(0, greenMail.getReceivedMessages().length);
@@ -359,15 +338,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
@Test @Test
public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException { public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException {
try { try {
loginPage.open(); initiateResetPasswordFromResetPasswordPage("login-test");
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null) .session((String)null)
@@ -403,15 +374,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
testRealm().update(realmRep); testRealm().update(realmRep);
try { try {
loginPage.open(); initiateResetPasswordFromResetPasswordPage("login-test");
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
.session((String)null) .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(); events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent();
} finally { } finally {
setTimeOffset(0); setTimeOffset(0);
realmRep.setAccessCodeLifespanUserAction(originalValue.get());
testRealm().update(realmRep);
} }
} }
@Test @Test
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException { public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
UserRepresentation user = findUser("login-test"); UserRepresentation user = findUser("login-test");
user.setEnabled(false); try {
updateUser(user); user.setEnabled(false);
updateUser(user);
loginPage.open(); initiateResetPasswordFromResetPasswordPage("login-test");
loginPage.resetPassword();
resetPasswordPage.assertCurrent(); assertEquals(0, greenMail.getReceivedMessages().length);
resetPasswordPage.changePassword("login-test"); events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
} finally {
loginPage.assertCurrent(); user.setEnabled(true);
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); updateUser(user);
}
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);
} }
@Test @Test
public void resetPasswordNoEmail() throws IOException, MessagingException, InterruptedException { public void resetPasswordNoEmail() throws IOException, MessagingException, InterruptedException {
final String[] email = new String[1]; final String email;
UserRepresentation user = findUser("login-test"); UserRepresentation user = findUser("login-test");
email[0] = user.getEmail(); email = user.getEmail();
user.setEmail("");
updateUser(user);
loginPage.open(); try {
loginPage.resetPassword(); user.setEmail("");
updateUser(user);
resetPasswordPage.assertCurrent(); initiateResetPasswordFromResetPasswordPage("login-test");
resetPasswordPage.changePassword("login-test"); assertEquals(0, greenMail.getReceivedMessages().length);
loginPage.assertCurrent(); events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); } finally {
user.setEmail(email);
assertEquals(0, greenMail.getReceivedMessages().length); updateUser(user);
}
events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
} }
@Test @Test
@@ -496,29 +454,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
RealmRepresentation realmRep = testRealm().toRepresentation(); RealmRepresentation realmRep = testRealm().toRepresentation();
Map<String, String> oldSmtp = realmRep.getSmtpServer(); Map<String, String> oldSmtp = realmRep.getSmtpServer();
realmRep.setSmtpServer(smtpConfig); try {
testRealm().update(realmRep); realmRep.setSmtpServer(smtpConfig);
testRealm().update(realmRep);
loginPage.open(); loginPage.open();
loginPage.resetPassword(); 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) events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
.session((String)null) .session((String)null)
.detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent(); .detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
} finally {
// Revert SMTP back // Revert SMTP back
realmRep.setSmtpServer(oldSmtp); realmRep.setSmtpServer(oldSmtp);
testRealm().update(realmRep); testRealm().update(realmRep);
}
} }
private void setPasswordPolicy(String policy) { private void setPasswordPolicy(String policy) {
@@ -531,15 +491,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException { public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException {
setPasswordPolicy("length"); setPasswordPolicy("length");
loginPage.open(); initiateResetPasswordFromResetPasswordPage("login-test");
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
assertEquals(1, greenMail.getReceivedMessages().length); assertEquals(1, greenMail.getReceivedMessages().length);