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:
parent
83b29c5080
commit
19a41c8704
13 changed files with 1171 additions and 344 deletions
|
@ -29,10 +29,10 @@ import java.security.PublicKey;
|
|||
*/
|
||||
public class RSATokenVerifier {
|
||||
|
||||
private TokenVerifier tokenVerifier;
|
||||
private final TokenVerifier<AccessToken> tokenVerifier;
|
||||
|
||||
private RSATokenVerifier(String tokenString) {
|
||||
this.tokenVerifier = TokenVerifier.create(tokenString);
|
||||
this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class);
|
||||
}
|
||||
|
||||
public static RSATokenVerifier create(String tokenString) {
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
package org.keycloak;
|
||||
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.exceptions.TokenNotActiveException;
|
||||
import org.keycloak.exceptions.TokenSignatureInvalidException;
|
||||
import org.keycloak.jose.jws.AlgorithmType;
|
||||
import org.keycloak.jose.jws.JWSHeader;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
|
@ -26,67 +27,235 @@ import org.keycloak.jose.jws.JWSInputException;
|
|||
import org.keycloak.jose.jws.crypto.HMACProvider;
|
||||
import org.keycloak.jose.jws.crypto.RSAProvider;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class TokenVerifier {
|
||||
public class TokenVerifier<T extends JsonWebToken> {
|
||||
|
||||
private final String tokenString;
|
||||
// This interface is here as JDK 7 is a requirement for this project.
|
||||
// Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead.
|
||||
|
||||
// @FunctionalInterface
|
||||
public static interface Predicate<T extends JsonWebToken> {
|
||||
/**
|
||||
* Performs a single check on the given token verifier.
|
||||
* @param t Token, guaranteed to be non-null.
|
||||
* @return
|
||||
* @throws VerificationException
|
||||
*/
|
||||
boolean test(T t) throws VerificationException;
|
||||
}
|
||||
|
||||
public static final Predicate<JsonWebToken> SUBJECT_EXISTS_CHECK = new Predicate<JsonWebToken>() {
|
||||
@Override
|
||||
public boolean test(JsonWebToken t) throws VerificationException {
|
||||
String subject = t.getSubject();
|
||||
if (subject == null) {
|
||||
throw new VerificationException("Subject missing in token");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public static final Predicate<JsonWebToken> IS_ACTIVE = new Predicate<JsonWebToken>() {
|
||||
@Override
|
||||
public boolean test(JsonWebToken t) throws VerificationException {
|
||||
if (! t.isActive()) {
|
||||
throw new TokenNotActiveException("Token is not active");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public static class RealmUrlCheck implements Predicate<JsonWebToken> {
|
||||
|
||||
private static final RealmUrlCheck NULL_INSTANCE = new RealmUrlCheck(null);
|
||||
|
||||
private final String realmUrl;
|
||||
|
||||
public RealmUrlCheck(String realmUrl) {
|
||||
this.realmUrl = realmUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(JsonWebToken t) throws VerificationException {
|
||||
if (this.realmUrl == null) {
|
||||
throw new VerificationException("Realm URL not set");
|
||||
}
|
||||
|
||||
if (! this.realmUrl.equals(t.getIssuer())) {
|
||||
throw new VerificationException("Invalid token issuer. Expected '" + this.realmUrl + "', but was '" + t.getIssuer() + "'");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
public static class TokenTypeCheck implements Predicate<JsonWebToken> {
|
||||
|
||||
private static final TokenTypeCheck INSTANCE_BEARER = new TokenTypeCheck(TokenUtil.TOKEN_TYPE_BEARER);
|
||||
|
||||
private final String tokenType;
|
||||
|
||||
public TokenTypeCheck(String tokenType) {
|
||||
this.tokenType = tokenType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(JsonWebToken t) throws VerificationException {
|
||||
if (! tokenType.equalsIgnoreCase(t.getType())) {
|
||||
throw new VerificationException("Token type is incorrect. Expected '" + tokenType + "' but was '" + t.getType() + "'");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
private String tokenString;
|
||||
private Class<? extends T> clazz;
|
||||
private PublicKey publicKey;
|
||||
private SecretKey secretKey;
|
||||
private String realmUrl;
|
||||
private String expectedTokenType = TokenUtil.TOKEN_TYPE_BEARER;
|
||||
private boolean checkTokenType = true;
|
||||
private boolean checkActive = true;
|
||||
private boolean checkRealmUrl = true;
|
||||
private final LinkedList<Predicate<? super T>> checks = new LinkedList<>();
|
||||
|
||||
private JWSInput jws;
|
||||
private AccessToken token;
|
||||
private T token;
|
||||
|
||||
protected TokenVerifier(String tokenString) {
|
||||
protected TokenVerifier(String tokenString, Class<T> clazz) {
|
||||
this.tokenString = tokenString;
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
public static TokenVerifier create(String tokenString) {
|
||||
return new TokenVerifier(tokenString);
|
||||
protected TokenVerifier(T token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public TokenVerifier publicKey(PublicKey publicKey) {
|
||||
/**
|
||||
* Creates a {@code TokenVerifier<AccessToken> instance. The method is here for backwards compatibility.
|
||||
* @param tokenString
|
||||
* @return
|
||||
* @deprecated use {@link #create(java.lang.String, java.lang.Class) } instead
|
||||
*/
|
||||
public static TokenVerifier<AccessToken> create(String tokenString) {
|
||||
return create(tokenString, AccessToken.class);
|
||||
}
|
||||
|
||||
public static <T extends JsonWebToken> TokenVerifier<T> create(String tokenString, Class<T> clazz) {
|
||||
return new TokenVerifier(tokenString, clazz)
|
||||
.check(RealmUrlCheck.NULL_INSTANCE)
|
||||
.check(SUBJECT_EXISTS_CHECK)
|
||||
.check(TokenTypeCheck.INSTANCE_BEARER)
|
||||
.check(IS_ACTIVE);
|
||||
}
|
||||
|
||||
public static <T extends JsonWebToken> TokenVerifier<T> from(T token) {
|
||||
return new TokenVerifier(token)
|
||||
.check(RealmUrlCheck.NULL_INSTANCE)
|
||||
.check(SUBJECT_EXISTS_CHECK)
|
||||
.check(TokenTypeCheck.INSTANCE_BEARER)
|
||||
.check(IS_ACTIVE);
|
||||
}
|
||||
|
||||
private void removeCheck(Class<? extends Predicate<?>> checkClass) {
|
||||
for (Iterator<Predicate<? super T>> it = checks.iterator(); it.hasNext();) {
|
||||
if (it.next().getClass() == checkClass) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeCheck(Predicate<? super T> check) {
|
||||
checks.remove(check);
|
||||
}
|
||||
|
||||
private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Class<? extends Predicate<?>> checkClass, boolean active, P predicate) {
|
||||
removeCheck(checkClass);
|
||||
if (active) {
|
||||
checks.add(predicate);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private <P extends Predicate<? super T>> TokenVerifier<T> replaceCheck(Predicate<? super T> check, boolean active, P predicate) {
|
||||
removeCheck(check);
|
||||
if (active) {
|
||||
checks.add(predicate);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all preset checks and will test the given checks in {@link #verify()} method.
|
||||
* @param checks
|
||||
* @return
|
||||
*/
|
||||
public TokenVerifier<T> checkOnly(Predicate<? super T>... checks) {
|
||||
this.checks.clear();
|
||||
if (checks != null) {
|
||||
this.checks.addAll(Arrays.asList(checks));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will test the given checks in {@link #verify()} method in addition to already set checks.
|
||||
* @param checks
|
||||
* @return
|
||||
*/
|
||||
public TokenVerifier<T> check(Predicate<? super T>... checks) {
|
||||
if (checks != null) {
|
||||
this.checks.addAll(Arrays.asList(checks));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenVerifier<T> publicKey(PublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenVerifier secretKey(SecretKey secretKey) {
|
||||
public TokenVerifier<T> secretKey(SecretKey secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenVerifier realmUrl(String realmUrl) {
|
||||
public TokenVerifier<T> realmUrl(String realmUrl) {
|
||||
this.realmUrl = realmUrl;
|
||||
return this;
|
||||
return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl));
|
||||
}
|
||||
|
||||
public TokenVerifier checkTokenType(boolean checkTokenType) {
|
||||
public TokenVerifier<T> checkTokenType(boolean checkTokenType) {
|
||||
this.checkTokenType = checkTokenType;
|
||||
return this;
|
||||
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
|
||||
}
|
||||
|
||||
public TokenVerifier checkActive(boolean checkActive) {
|
||||
this.checkActive = checkActive;
|
||||
return this;
|
||||
public TokenVerifier<T> tokenType(String tokenType) {
|
||||
this.expectedTokenType = tokenType;
|
||||
return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType));
|
||||
}
|
||||
|
||||
public TokenVerifier checkRealmUrl(boolean checkRealmUrl) {
|
||||
public TokenVerifier<T> checkActive(boolean checkActive) {
|
||||
return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE);
|
||||
}
|
||||
|
||||
public TokenVerifier<T> checkRealmUrl(boolean checkRealmUrl) {
|
||||
this.checkRealmUrl = checkRealmUrl;
|
||||
return this;
|
||||
return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl));
|
||||
}
|
||||
|
||||
public TokenVerifier parse() throws VerificationException {
|
||||
public TokenVerifier<T> parse() throws VerificationException {
|
||||
if (jws == null) {
|
||||
if (tokenString == null) {
|
||||
throw new VerificationException("Token not set");
|
||||
|
@ -100,7 +269,7 @@ public class TokenVerifier {
|
|||
|
||||
|
||||
try {
|
||||
token = jws.readJsonContent(AccessToken.class);
|
||||
token = jws.readJsonContent(clazz);
|
||||
} catch (JWSInputException e) {
|
||||
throw new VerificationException("Failed to read access token from JWT", e);
|
||||
}
|
||||
|
@ -108,8 +277,10 @@ public class TokenVerifier {
|
|||
return this;
|
||||
}
|
||||
|
||||
public AccessToken getToken() throws VerificationException {
|
||||
parse();
|
||||
public T getToken() throws VerificationException {
|
||||
if (token == null) {
|
||||
parse();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
|
@ -118,50 +289,43 @@ public class TokenVerifier {
|
|||
return jws.getHeader();
|
||||
}
|
||||
|
||||
public TokenVerifier verify() throws VerificationException {
|
||||
parse();
|
||||
|
||||
if (checkRealmUrl && realmUrl == null) {
|
||||
throw new VerificationException("Realm URL not set");
|
||||
}
|
||||
|
||||
public void verifySignature() throws VerificationException {
|
||||
AlgorithmType algorithmType = getHeader().getAlgorithm().getType();
|
||||
|
||||
if (AlgorithmType.RSA.equals(algorithmType)) {
|
||||
if (publicKey == null) {
|
||||
throw new VerificationException("Public key not set");
|
||||
}
|
||||
if (null == algorithmType) {
|
||||
throw new VerificationException("Unknown or unsupported token algorithm");
|
||||
} else switch (algorithmType) {
|
||||
case RSA:
|
||||
if (publicKey == null) {
|
||||
throw new VerificationException("Public key not set");
|
||||
}
|
||||
if (!RSAProvider.verify(jws, publicKey)) {
|
||||
throw new TokenSignatureInvalidException("Invalid token signature");
|
||||
} break;
|
||||
case HMAC:
|
||||
if (secretKey == null) {
|
||||
throw new VerificationException("Secret key not set");
|
||||
}
|
||||
if (!HMACProvider.verify(jws, secretKey)) {
|
||||
throw new TokenSignatureInvalidException("Invalid token signature");
|
||||
} break;
|
||||
default:
|
||||
throw new VerificationException("Unknown or unsupported token algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
if (!RSAProvider.verify(jws, publicKey)) {
|
||||
throw new VerificationException("Invalid token signature");
|
||||
}
|
||||
} else if (AlgorithmType.HMAC.equals(algorithmType)) {
|
||||
if (secretKey == null) {
|
||||
throw new VerificationException("Secret key not set");
|
||||
}
|
||||
|
||||
if (!HMACProvider.verify(jws, secretKey)) {
|
||||
throw new VerificationException("Invalid token signature");
|
||||
}
|
||||
} else {
|
||||
throw new VerificationException("Unknown or unsupported token algorith");
|
||||
public TokenVerifier<T> verify() throws VerificationException {
|
||||
if (getToken() == null) {
|
||||
parse();
|
||||
}
|
||||
if (jws != null) {
|
||||
verifySignature();
|
||||
}
|
||||
|
||||
String user = token.getSubject();
|
||||
if (user == null) {
|
||||
throw new VerificationException("Subject missing in token");
|
||||
}
|
||||
|
||||
if (checkRealmUrl && !realmUrl.equals(token.getIssuer())) {
|
||||
throw new VerificationException("Invalid token issuer. Expected '" + realmUrl + "', but was '" + token.getIssuer() + "'");
|
||||
}
|
||||
|
||||
if (checkTokenType && !TokenUtil.TOKEN_TYPE_BEARER.equalsIgnoreCase(token.getType())) {
|
||||
throw new VerificationException("Token type is incorrect. Expected '" + TokenUtil.TOKEN_TYPE_BEARER + "' but was '" + token.getType() + "'");
|
||||
}
|
||||
|
||||
if (checkActive && !token.isActive()) {
|
||||
throw new VerificationException("Token is not active");
|
||||
for (Predicate<? super T> check : checks) {
|
||||
if (! check.test(getToken())) {
|
||||
throw new VerificationException("JWT check failed for check " + check);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
|
|
|
@ -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.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.authentication.*;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.credential.*;
|
||||
import org.keycloak.email.EmailException;
|
||||
import org.keycloak.email.EmailTemplateProvider;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.models.utils.HmacOTP;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
|
||||
import java.util.*;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriBuilder;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
|
@ -53,9 +47,6 @@ import java.util.concurrent.TimeUnit;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory {
|
||||
public static final String RESET_CREDENTIAL_SECRET = "RESET_CREDENTIAL_SECRET";
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ResetCredentialEmail.class);
|
||||
|
||||
public static final String PROVIDER_ID = "reset-credential-email";
|
||||
|
||||
|
@ -85,15 +76,25 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
|||
return;
|
||||
}
|
||||
|
||||
// We send the secret in the email in a link as a query param. We don't need to sign it or anything because
|
||||
// it can only be guessed once, and it must match watch is stored in the client session.
|
||||
String secret = HmacOTP.generateSecret(10);
|
||||
context.getClientSession().setNote(RESET_CREDENTIAL_SECRET, secret);
|
||||
String link = UriBuilder.fromUri(context.getActionUrl()).queryParam(Constants.KEY, secret).build().toString();
|
||||
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
|
||||
int validityInSecs = context.getRealm().getAccessCodeLifespanUserAction();
|
||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||
|
||||
PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
|
||||
CredentialModel password = passwordProvider.getPassword(context.getRealm(), user);
|
||||
Long lastCreatedPassword = password == null ? null : password.getCreatedDate();
|
||||
|
||||
// We send the secret in the email in a link as a query param.
|
||||
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, null, lastCreatedPassword, context.getClientSession());
|
||||
KeycloakSession keycloakSession = context.getSession();
|
||||
String link = UriBuilder
|
||||
.fromUri(context.getActionUrl())
|
||||
.queryParam(Constants.KEY, token.serialize(keycloakSession, context.getRealm(), context.getUriInfo()))
|
||||
.build()
|
||||
.toString();
|
||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||
try {
|
||||
|
||||
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration);
|
||||
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
|
||||
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
||||
.user(user)
|
||||
.detail(Details.USERNAME, username)
|
||||
|
@ -114,19 +115,56 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
|||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
/*String secret = context.getClientSession().getNote(RESET_CREDENTIAL_SECRET);
|
||||
String key = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
|
||||
|
||||
// Can only guess once! We remove the note so another guess can't happen
|
||||
context.getClientSession().removeNote(RESET_CREDENTIAL_SECRET);
|
||||
if (secret == null || key == null || !secret.equals(key)) {
|
||||
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
||||
/*
|
||||
KeycloakSession keycloakSession = context.getSession();
|
||||
String actionTokenString = context.getUriInfo().getQueryParameters().getFirst(Constants.KEY);
|
||||
ResetCredentialsActionToken tokenFromMail = null;
|
||||
try {
|
||||
tokenFromMail = ResetCredentialsActionToken.deserialize(keycloakSession, context.getRealm(), context.getUriInfo(), actionTokenString);
|
||||
} catch (VerificationException ex) {
|
||||
context.getEvent().detail(Details.REASON, ex.getMessage()).error(Errors.INVALID_CODE);
|
||||
Response challenge = context.form()
|
||||
.setError(Messages.INVALID_ACCESS_CODE)
|
||||
.setError(Messages.INVALID_CODE)
|
||||
.createErrorPage();
|
||||
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
|
||||
}
|
||||
|
||||
String userId = tokenFromMail == null ? null : tokenFromMail.getUserId();
|
||||
|
||||
if (tokenFromMail == null) {
|
||||
context.getEvent()
|
||||
.error(Errors.INVALID_CODE);
|
||||
Response challenge = context.form()
|
||||
.setError(Messages.INVALID_CODE)
|
||||
.createErrorPage();
|
||||
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) context.getSession().getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID);
|
||||
CredentialModel password = passwordProvider.getPassword(context.getRealm(), context.getUser());
|
||||
|
||||
Long lastCreatedPasswordMail = tokenFromMail.getLastChangedPasswordTimestamp();
|
||||
Long lastCreatedPasswordFromStore = password == null ? null : password.getCreatedDate();
|
||||
|
||||
String clientSessionId = tokenFromMail.getClientSessionId();
|
||||
ClientSessionModel clientSession = clientSessionId == null ? null : keycloakSession.sessions().getClientSession(clientSessionId);
|
||||
|
||||
if (clientSession == null
|
||||
|| ! Objects.equals(lastCreatedPasswordMail, lastCreatedPasswordFromStore)
|
||||
|| ! Objects.equals(userId, context.getUser().getId())) {
|
||||
context.getEvent()
|
||||
.user(userId)
|
||||
.detail(Details.USERNAME, context.getUser().getUsername())
|
||||
.detail(Details.TOKEN_ID, tokenFromMail.getId())
|
||||
.error(Errors.EXPIRED_CODE);
|
||||
Response challenge = context.form()
|
||||
.setError(Messages.INVALID_CODE)
|
||||
.createErrorPage();
|
||||
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
|
||||
return;
|
||||
}
|
||||
|
||||
// We now know email is valid, so set it to valid.
|
||||
context.getUser().setEmailVerified(true);
|
||||
context.success();*/
|
||||
|
|
|
@ -154,6 +154,11 @@ public class RestartLoginCookie {
|
|||
// TODO:mposolda
|
||||
/*
|
||||
public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception {
|
||||
String[] parts = code.split("\\.");
|
||||
return restartSessionByClientSession(session, realm, parts[1]);
|
||||
}
|
||||
|
||||
public static ClientSessionModel restartSessionByClientSession(KeycloakSession session, RealmModel realm, String clientSessionId) throws Exception {
|
||||
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
|
||||
if (cook == null) {
|
||||
logger.debug("KC_RESTART cookie doesn't exist");
|
||||
|
@ -167,8 +172,6 @@ public class RestartLoginCookie {
|
|||
return null;
|
||||
}
|
||||
RestartLoginCookie cookie = input.readJsonContent(RestartLoginCookie.class);
|
||||
String[] parts = code.split("\\.");
|
||||
String clientSessionId = parts[1];
|
||||
if (!clientSessionId.equals(cookie.getClientSession())) {
|
||||
logger.debug("RestartLoginCookie clientSession does not match code's clientSession");
|
||||
return null;
|
||||
|
|
|
@ -127,7 +127,10 @@ public class AuthenticationManager {
|
|||
if (cookie == null) return;
|
||||
String tokenString = cookie.getValue();
|
||||
|
||||
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(false).checkTokenType(false);
|
||||
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class)
|
||||
.realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()))
|
||||
.checkActive(false)
|
||||
.checkTokenType(false);
|
||||
|
||||
String kid = verifier.getHeader().getKeyId();
|
||||
SecretKey secretKey = session.keys().getHmacSecretKey(realm, kid);
|
||||
|
@ -710,7 +713,7 @@ public class AuthenticationManager {
|
|||
public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType,
|
||||
boolean isCookie, String tokenString, HttpHeaders headers) {
|
||||
try {
|
||||
TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
|
||||
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType);
|
||||
String kid = verifier.getHeader().getKeyId();
|
||||
AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType();
|
||||
|
||||
|
|
|
@ -24,13 +24,13 @@ import org.keycloak.authentication.RequiredActionContext;
|
|||
import org.keycloak.authentication.RequiredActionContextResult;
|
||||
import org.keycloak.authentication.RequiredActionFactory;
|
||||
import org.keycloak.authentication.RequiredActionProvider;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.TokenVerifier.Predicate;
|
||||
import org.keycloak.authentication.ResetCredentialsActionToken;
|
||||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
|
||||
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
|
||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
|
||||
import org.keycloak.authentication.requiredactions.VerifyEmail;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
|
@ -48,7 +48,6 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserConsentModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.protocol.LoginProtocol;
|
||||
|
@ -57,11 +56,14 @@ import org.keycloak.protocol.RestartLoginCookie;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorPage;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.ClientSessionCode.ActionType;
|
||||
import org.keycloak.services.managers.ClientSessionCode.ParseResult;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.services.util.CookieHelper;
|
||||
|
@ -84,6 +86,7 @@ import javax.ws.rs.core.UriBuilder;
|
|||
import javax.ws.rs.core.UriInfo;
|
||||
import javax.ws.rs.ext.Providers;
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -166,24 +169,51 @@ public class LoginActionsService {
|
|||
}
|
||||
}
|
||||
|
||||
private <C extends CommonClientSessionModel> SessionCodeChecks<C> checksForCode(String code, Class<C> expectedClazz) {
|
||||
SessionCodeChecks<C> res = new SessionCodeChecks<>(code, expectedClazz);
|
||||
res.initialVerifyCode();
|
||||
return res;
|
||||
}
|
||||
|
||||
private class Checks {
|
||||
// TODO: Merge with Hynek's code. This may not be just loginSession
|
||||
ClientSessionCode<LoginSessionModel> clientCode;
|
||||
|
||||
|
||||
private class SessionCodeChecks<C extends CommonClientSessionModel> {
|
||||
ClientSessionCode<C> clientCode;
|
||||
Response response;
|
||||
ClientSessionCode.ParseResult result;
|
||||
ClientSessionCode.ParseResult<C> result;
|
||||
Class<C> expectedClazz;
|
||||
|
||||
boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) {
|
||||
if (!verifyCode(code)) {
|
||||
private final String code;
|
||||
|
||||
public SessionCodeChecks(String code, Class<C> expectedClazz) {
|
||||
this.code = code;
|
||||
this.expectedClazz = expectedClazz;
|
||||
}
|
||||
|
||||
public C getClientSession() {
|
||||
return clientCode == null ? null : clientCode.getClientSession();
|
||||
}
|
||||
|
||||
public boolean passed() {
|
||||
return response == null;
|
||||
}
|
||||
|
||||
public boolean failed() {
|
||||
return response != null;
|
||||
}
|
||||
|
||||
|
||||
boolean verifyCode(String requiredAction, ClientSessionCode.ActionType actionType) {
|
||||
if (failed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!clientCode.isValidAction(requiredAction)) {
|
||||
LoginSessionModel loginSession = clientCode.getClientSession();
|
||||
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(loginSession.getAction())) {
|
||||
C clientSession = getClientSession();
|
||||
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
|
||||
response = redirectToRequiredActions(code);
|
||||
return false;
|
||||
|
||||
} // TODO:mposolda
|
||||
} // TODO:mposolda
|
||||
/*else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
|
||||
response = session.getProvider(LoginFormsProvider.class)
|
||||
.setSuccess(Messages.ALREADY_LOGGED_IN)
|
||||
|
@ -191,9 +221,9 @@ public class LoginActionsService {
|
|||
return false;
|
||||
}*/
|
||||
}
|
||||
if (!isActionActive(actionType)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return isActionActive(actionType);
|
||||
}
|
||||
|
||||
private boolean isValidAction(String requiredAction) {
|
||||
if (!clientCode.isValidAction(requiredAction)) {
|
||||
|
@ -204,18 +234,19 @@ public class LoginActionsService {
|
|||
}
|
||||
|
||||
private void invalidAction() {
|
||||
event.client(clientCode.getClientSession().getClient());
|
||||
event.client(getClientSession().getClient());
|
||||
event.error(Errors.INVALID_CODE);
|
||||
response = ErrorPage.error(session, Messages.INVALID_CODE);
|
||||
}
|
||||
|
||||
private boolean isActionActive(ClientSessionCode.ActionType actionType) {
|
||||
if (!clientCode.isActionActive(actionType)) {
|
||||
event.client(clientCode.getClientSession().getClient());
|
||||
event.client(getClientSession().getClient());
|
||||
event.clone().error(Errors.EXPIRED_CODE);
|
||||
if (clientCode.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
|
||||
AuthenticationProcessor.resetFlow(clientCode.getClientSession());
|
||||
response = processAuthentication(null, clientCode.getClientSession(), Messages.LOGIN_TIMEOUT);
|
||||
if (getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
|
||||
LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
|
||||
AuthenticationProcessor.resetFlow(loginSession);
|
||||
response = processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT);
|
||||
return false;
|
||||
}
|
||||
response = ErrorPage.error(session, Messages.EXPIRED_CODE);
|
||||
|
@ -224,7 +255,7 @@ public class LoginActionsService {
|
|||
return true;
|
||||
}
|
||||
|
||||
public boolean verifyCode(String code) {
|
||||
private boolean initialVerifyCode() {
|
||||
if (!checkSsl()) {
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
|
||||
|
@ -235,14 +266,12 @@ public class LoginActionsService {
|
|||
response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO:mposolda it may not be just loginSessionModel
|
||||
result = ClientSessionCode.parseResult(code, session, realm, LoginSessionModel.class);
|
||||
result = ClientSessionCode.parseResult(code, session, realm, expectedClazz);
|
||||
clientCode = result.getCode();
|
||||
if (clientCode == null) {
|
||||
// TODO:mposolda
|
||||
/*
|
||||
if (result.isLoginSessionNotFound()) { // timeout
|
||||
if (result.isLoginSessionNotFound()) { // timeout or loginSession already logged
|
||||
// TODO:mposolda
|
||||
/*
|
||||
try {
|
||||
ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code);
|
||||
if (clientSession != null) {
|
||||
|
@ -252,13 +281,14 @@ public class LoginActionsService {
|
|||
}
|
||||
} catch (Exception e) {
|
||||
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
event.error(Errors.INVALID_CODE);
|
||||
response = ErrorPage.error(session, Messages.INVALID_CODE);*/
|
||||
response = ErrorPage.error(session, Messages.INVALID_CODE);
|
||||
return false;
|
||||
}
|
||||
LoginSessionModel clientSession = clientCode.getClientSession();
|
||||
|
||||
C clientSession = getClientSession();
|
||||
if (clientSession == null) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
response = ErrorPage.error(session, Messages.INVALID_CODE);
|
||||
|
@ -269,62 +299,48 @@ public class LoginActionsService {
|
|||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
|
||||
session.loginSessions().removeLoginSession(realm, clientSession);
|
||||
// TODO:mposolda
|
||||
//session.sessions().removeClientSession(realm, clientSession);
|
||||
return false;
|
||||
}
|
||||
if (!client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
|
||||
session.loginSessions().removeLoginSession(realm, clientSession);
|
||||
// TODO:mposolda
|
||||
//session.sessions().removeClientSession(realm, clientSession);
|
||||
return false;
|
||||
}
|
||||
session.getContext().setClient(client);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean verifyRequiredAction(String code, String executedAction) {
|
||||
// TODO:mposolda
|
||||
/*
|
||||
if (!verifyCode(code)) {
|
||||
public boolean verifyRequiredAction(String executedAction) {
|
||||
if (failed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false;
|
||||
if (!isActionActive(ClientSessionCode.ActionType.USER)) return false;
|
||||
|
||||
final ClientSessionModel clientSession = clientCode.getClientSession();
|
||||
final LoginSessionModel loginSession = (LoginSessionModel) getClientSession();
|
||||
|
||||
final UserSessionModel userSession = clientSession.getUserSession();
|
||||
if (userSession == null) {
|
||||
ServicesLogger.LOGGER.userSessionNull();
|
||||
event.error(Errors.USER_SESSION_NOT_FOUND);
|
||||
throw new WebApplicationException(ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE));
|
||||
}
|
||||
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
|
||||
event.error(Errors.INVALID_CODE);
|
||||
response = ErrorPage.error(session, Messages.SESSION_NOT_ACTIVE);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (executedAction == null && userSession != null) { // do next required action only if user is already authenticated
|
||||
initEvent(clientSession);
|
||||
if (executedAction == null) { // do next required action only if user is already authenticated
|
||||
initLoginEvent(loginSession);
|
||||
event.event(EventType.LOGIN);
|
||||
response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
|
||||
response = AuthenticationManager.nextActionAfterAuthentication(session, loginSession, clientConnection, request, uriInfo, event);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!executedAction.equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
|
||||
if (!executedAction.equals(loginSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
|
||||
logger.debug("required action doesn't match current required action");
|
||||
clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
|
||||
loginSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
|
||||
response = redirectToRequiredActions(code);
|
||||
return false;
|
||||
}*/
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* protocol independent login page entry point
|
||||
*
|
||||
|
@ -341,8 +357,8 @@ public class LoginActionsService {
|
|||
if (loginSession != null && code.equals(loginSession.getNote(LAST_PROCESSED_CODE))) {
|
||||
// Allow refresh of previous page
|
||||
} else {
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
|
@ -402,8 +418,8 @@ public class LoginActionsService {
|
|||
return authenticate(code, null);
|
||||
}
|
||||
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
return checks.response;
|
||||
}
|
||||
final ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
|
||||
|
@ -422,6 +438,163 @@ public class LoginActionsService {
|
|||
return null;
|
||||
}
|
||||
|
||||
private boolean isSslUsed(JsonWebToken t) throws VerificationException {
|
||||
if (! checkSsl()) {
|
||||
event.error(Errors.SSL_REQUIRED);
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.HTTPS_REQUIRED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isRealmEnabled(JsonWebToken t) throws VerificationException {
|
||||
if (! realm.isEnabled()) {
|
||||
event.error(Errors.REALM_DISABLED);
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.REALM_NOT_ENABLED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isResetCredentialsAllowed(ResetCredentialsActionToken t) throws VerificationException {
|
||||
if (!realm.isResetPasswordAllowed()) {
|
||||
event.client(t.getClientSession().getClient());
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean canResolveClientSession(ResetCredentialsActionToken t) throws VerificationException {
|
||||
// TODO:mposolda
|
||||
/*
|
||||
String clientSessionId = t == null ? null : t.getNote(ResetCredentialsActionToken.NOTE_CLIENT_SESSION_ID);
|
||||
|
||||
if (t == null || clientSessionId == null) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
|
||||
}
|
||||
|
||||
ClientSessionModel clientSession = session.sessions().getClientSession(clientSessionId);
|
||||
t.setClientSession(clientSession);
|
||||
|
||||
if (clientSession == null) { // timeout
|
||||
try {
|
||||
clientSession = RestartLoginCookie.restartSessionByClientSession(session, realm, clientSessionId);
|
||||
} catch (Exception e) {
|
||||
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
|
||||
}
|
||||
|
||||
if (clientSession != null) {
|
||||
event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE);
|
||||
throw new LoginActionsServiceException(processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor()));
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSession == null) {
|
||||
event.error(Errors.INVALID_CODE);
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.INVALID_CODE));
|
||||
}
|
||||
|
||||
event.detail(Details.CODE_ID, clientSession.getId());*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean canResolveClient(ResetCredentialsActionToken t) throws VerificationException {
|
||||
ClientModel client = t.getClientSession().getClient();
|
||||
if (client == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
session.sessions().removeClientSession(realm, t.getClientSession());
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
|
||||
}
|
||||
|
||||
if (! client.isEnabled()) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
session.sessions().removeClientSession(realm, t.getClientSession());
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
|
||||
}
|
||||
session.getContext().setClient(client);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private class IsValidAction implements Predicate<ResetCredentialsActionToken> {
|
||||
|
||||
private final String requiredAction;
|
||||
|
||||
public IsValidAction(String requiredAction) {
|
||||
this.requiredAction = requiredAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(ResetCredentialsActionToken t) throws VerificationException {
|
||||
ClientSessionModel clientSession = t.getClientSession();
|
||||
if (! Objects.equals(clientSession.getAction(), this.requiredAction)) {
|
||||
|
||||
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) {
|
||||
// TODO: Once login tokens would be implemented, this would have to be rewritten
|
||||
// String code = clientSession.getNote(ClientSessionCode.ACTIVE_CODE) + "." + clientSession.getId();
|
||||
String code = clientSession.getNote("active_code") + "." + clientSession.getId();
|
||||
throw new LoginActionsServiceException(redirectToRequiredActions(code));
|
||||
} else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
|
||||
throw new LoginActionsServiceException(
|
||||
session.getProvider(LoginFormsProvider.class)
|
||||
.setSuccess(Messages.ALREADY_LOGGED_IN)
|
||||
.createInfoPage());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class IsActiveAction implements Predicate<ResetCredentialsActionToken> {
|
||||
private final ClientSessionCode.ActionType actionType;
|
||||
|
||||
public IsActiveAction(ActionType actionType) {
|
||||
this.actionType = actionType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(ResetCredentialsActionToken t) throws VerificationException {
|
||||
int timestamp = t.getClientSession().getTimestamp();
|
||||
if (! isActionActive(actionType, timestamp)) {
|
||||
event.client(t.getClientSession().getClient());
|
||||
event.clone().error(Errors.EXPIRED_CODE);
|
||||
|
||||
if (t.getClientSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
|
||||
// TODO:mposolda incompatible types
|
||||
LoginSessionModel loginSession = (LoginSessionModel) t.getClientSession();
|
||||
|
||||
AuthenticationProcessor.resetFlow(loginSession);
|
||||
throw new LoginActionsServiceException(processAuthentication(null, loginSession, Messages.LOGIN_TIMEOUT));
|
||||
}
|
||||
|
||||
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.EXPIRED_CODE));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isActionActive(ActionType actionType, int timestamp) {
|
||||
int lifespan;
|
||||
switch (actionType) {
|
||||
case CLIENT:
|
||||
lifespan = realm.getAccessCodeLifespan();
|
||||
break;
|
||||
case LOGIN:
|
||||
lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction();
|
||||
break;
|
||||
case USER:
|
||||
lifespan = realm.getAccessCodeLifespanUserAction();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
return timestamp + lifespan > Time.currentTime();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for executing reset credentials flow. If code is null, a client session is created with the account
|
||||
* service as the client. Successful reset sends you to the account page. Note, account service must be enabled.
|
||||
|
@ -433,12 +606,10 @@ public class LoginActionsService {
|
|||
@Path(RESET_CREDENTIALS_PATH)
|
||||
@GET
|
||||
public Response resetCredentialsGET(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution) {
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("key") String key) {
|
||||
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
||||
//
|
||||
// TODO:mposolda
|
||||
/*
|
||||
if (code == null) {
|
||||
if (code == null && key == null) {
|
||||
if (!realm.isResetPasswordAllowed()) {
|
||||
event.event(EventType.RESET_PASSWORD);
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
|
@ -450,7 +621,7 @@ public class LoginActionsService {
|
|||
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
|
||||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
||||
//clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
|
||||
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
clientSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
|
||||
clientSession.setRedirectUri(redirectUri);
|
||||
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
|
||||
|
@ -459,32 +630,96 @@ public class LoginActionsService {
|
|||
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||
return processResetCredentials(null, clientSession, null);
|
||||
}
|
||||
|
||||
if (key != null) {
|
||||
try {
|
||||
ResetCredentialsActionToken token = ResetCredentialsActionToken.deserialize(
|
||||
session, realm, session.getContext().getUri(), key);
|
||||
return resetCredentials(code, token, execution);
|
||||
} catch (VerificationException ex) {
|
||||
event.event(EventType.RESET_PASSWORD)
|
||||
.detail(Details.REASON, ex.getMessage())
|
||||
.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
|
||||
}
|
||||
}
|
||||
|
||||
return resetCredentials(code, execution);
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
/**
|
||||
* @deprecated In favor of {@link #resetCredentials(String, org.keycloak.authentication.ResetCredentialsActionToken, java.lang.String)}
|
||||
* @param code
|
||||
* @param execution
|
||||
* @return
|
||||
*/
|
||||
protected Response resetCredentials(String code, String execution) {
|
||||
event.event(EventType.RESET_PASSWORD);
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
||||
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
||||
return checks.response;
|
||||
}
|
||||
final ClientSessionCode clientCode = checks.clientCode;
|
||||
final ClientSessionModel clientSession = clientCode.getClientSession();
|
||||
final LoginSessionModel clientSession = checks.getClientSession();
|
||||
|
||||
if (!realm.isResetPasswordAllowed()) {
|
||||
event.client(clientCode.getClientSession().getClient());
|
||||
event.client(clientSession.getClient());
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
|
||||
|
||||
}
|
||||
|
||||
// TODO:mposolda
|
||||
//return processResetCredentials(execution, clientSession, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Response resetCredentials(String code, ResetCredentialsActionToken token, String execution) {
|
||||
event.event(EventType.RESET_PASSWORD);
|
||||
|
||||
if (token == null) {
|
||||
// TODO: Use more appropriate code
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
try {
|
||||
TokenVerifier.from(token).checkOnly(
|
||||
// Start basic checks
|
||||
this::isRealmEnabled,
|
||||
this::isSslUsed,
|
||||
this::isResetCredentialsAllowed,
|
||||
this::canResolveClientSession,
|
||||
this::canResolveClient,
|
||||
// End basic checks
|
||||
|
||||
new IsValidAction(ClientSessionModel.Action.AUTHENTICATE.name()),
|
||||
new IsActiveAction(ActionType.USER)
|
||||
).verify();
|
||||
} catch (LoginActionsServiceException ex) {
|
||||
if (ex.getResponse() == null) {
|
||||
event.event(EventType.RESET_PASSWORD)
|
||||
.detail(Details.REASON, ex.getMessage())
|
||||
.error(Errors.INVALID_REQUEST);
|
||||
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
|
||||
} else {
|
||||
return ex.getResponse();
|
||||
}
|
||||
} catch (VerificationException ex) {
|
||||
event.event(EventType.RESET_PASSWORD)
|
||||
.detail(Details.REASON, ex.getMessage())
|
||||
.error(Errors.NOT_ALLOWED);
|
||||
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
final ClientSessionModel clientSession = token.getClientSession();
|
||||
|
||||
return processResetCredentials(execution, clientSession, null);
|
||||
}
|
||||
|
||||
protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) {
|
||||
// TODO:mposolda
|
||||
/*
|
||||
AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
|
||||
|
||||
@Override
|
||||
|
@ -507,7 +742,9 @@ public class LoginActionsService {
|
|||
};
|
||||
|
||||
return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor);
|
||||
}*/
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected Response processRegistration(String execution, LoginSessionModel loginSession, String errorMessage) {
|
||||
|
@ -531,8 +768,8 @@ public class LoginActionsService {
|
|||
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
return checks.response;
|
||||
}
|
||||
event.detail(Details.CODE_ID, code);
|
||||
|
@ -561,14 +798,14 @@ public class LoginActionsService {
|
|||
event.error(Errors.REGISTRATION_DISABLED);
|
||||
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
|
||||
}
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
SessionCodeChecks checks = checksForCode(code, LoginSessionModel.class);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
|
||||
ClientSessionCode<LoginSessionModel> clientCode = checks.clientCode;
|
||||
LoginSessionModel loginSession = clientCode.getClientSession();
|
||||
|
||||
return processRegistration(execution, loginSession, null);
|
||||
}
|
||||
|
||||
|
@ -607,13 +844,12 @@ public class LoginActionsService {
|
|||
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
||||
event.event(eventType);
|
||||
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
SessionCodeChecks checks = checksForCode(code);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
return checks.response;
|
||||
}
|
||||
event.detail(Details.CODE_ID, code);
|
||||
ClientSessionCode clientSessionCode = checks.clientCode;
|
||||
final ClientSessionModel clientSessionn = clientSessionCode.getClientSession();
|
||||
final ClientSessionModel clientSessionn = checks.getClientSession();
|
||||
|
||||
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
|
||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
|
||||
|
@ -681,10 +917,11 @@ public class LoginActionsService {
|
|||
public Response processConsent(final MultivaluedMap<String, String> formData) {
|
||||
event.event(EventType.LOGIN);
|
||||
String code = formData.getFirst("code");
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyRequiredAction(code, ClientSessionModel.Action.OAUTH_GRANT.name())) {
|
||||
SessionCodeChecks<LoginSessionModel> checks = checksForCode(code, LoginSessionModel.class);
|
||||
if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
ClientSessionCode<LoginSessionModel> accessCode = checks.clientCode;
|
||||
LoginSessionModel loginSession = accessCode.getClientSession();
|
||||
|
||||
|
@ -750,16 +987,15 @@ public class LoginActionsService {
|
|||
|
||||
clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
|
||||
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
|
||||
SessionCodeChecks checks = checksForCode(code);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
|
||||
if (checks.clientCode == null && checks.result.isClientSessionNotFound() || checks.result.isIllegalHash()) {
|
||||
return ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK);
|
||||
}
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
ClientSessionCode accessCode = checks.clientCode;
|
||||
clientSession = accessCode.getClientSession();
|
||||
clientSession = checks.getClientSession();
|
||||
if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) {
|
||||
ServicesLogger.LOGGER.reqdActionDoesNotMatch();
|
||||
event.error(Errors.INVALID_CODE);
|
||||
|
@ -789,12 +1025,12 @@ public class LoginActionsService {
|
|||
|
||||
return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
|
||||
} else {
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
|
||||
SessionCodeChecks checks = checksForCode(code);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
|
||||
return checks.response;
|
||||
}
|
||||
ClientSessionCode accessCode = checks.clientCode;
|
||||
ClientSessionModel clientSession = accessCode.getClientSession();
|
||||
ClientSessionModel clientSession = checks.getClientSession();
|
||||
UserSessionModel userSession = clientSession.getUserSession();
|
||||
initEvent(clientSession);
|
||||
|
||||
|
@ -824,11 +1060,11 @@ public class LoginActionsService {
|
|||
/*
|
||||
event.event(EventType.EXECUTE_ACTIONS);
|
||||
if (key != null) {
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
|
||||
SessionCodeChecks checks = checksForCode(key);
|
||||
if (!checks.verifyCode(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
|
||||
return checks.response;
|
||||
}
|
||||
ClientSessionModel clientSession = checks.clientCode.getClientSession();
|
||||
ClientSessionModel clientSession = checks.getClientSession();
|
||||
// verify user email as we know it is valid as this entry point would never have gotten here.
|
||||
clientSession.getUserSession().getUser().setEmailVerified(true);
|
||||
clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
|
||||
|
@ -936,12 +1172,11 @@ public class LoginActionsService {
|
|||
/*
|
||||
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
||||
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
|
||||
Checks checks = new Checks();
|
||||
if (!checks.verifyRequiredAction(code, action)) {
|
||||
SessionCodeChecks checks = checksForCode(code);
|
||||
if (!checks.verifyRequiredAction(action)) {
|
||||
return checks.response;
|
||||
}
|
||||
final ClientSessionCode clientCode = checks.clientCode;
|
||||
final ClientSessionModel clientSession = clientCode.getClientSession();
|
||||
final ClientSessionModel clientSession = checks.getClientSession();
|
||||
|
||||
final UserSessionModel userSession = clientSession.getUserSession();
|
||||
|
||||
|
|
|
@ -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.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
|
@ -74,6 +75,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
.build();
|
||||
|
||||
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
|
||||
expectedMessagesCount = 0;
|
||||
getCleanup().addUserId(userId);
|
||||
}
|
||||
|
||||
|
@ -104,6 +106,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
private int expectedMessagesCount;
|
||||
|
||||
@Test
|
||||
public void resetPasswordLink() throws IOException, MessagingException {
|
||||
String username = "login-test";
|
||||
|
@ -167,6 +171,24 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
resetPassword("login-test");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void resetPasswordTwice() throws IOException, MessagingException {
|
||||
String changePasswordUrl = resetPassword("login-test");
|
||||
driver.navigate().to(changePasswordUrl.trim());
|
||||
|
||||
errorPage.assertCurrent();
|
||||
assertEquals("An error occurred, please login again through your application.", errorPage.getError());
|
||||
|
||||
events.expect(EventType.RESET_PASSWORD)
|
||||
.client((String) null)
|
||||
.session((String) null)
|
||||
.user(userId)
|
||||
.detail(Details.USERNAME, "login-test")
|
||||
.error(Errors.EXPIRED_CODE)
|
||||
.assertEvent();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException {
|
||||
resetPassword(" login-test ");
|
||||
|
@ -174,15 +196,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
@Test
|
||||
public void resetPasswordCancelChangeUser() throws IOException, MessagingException {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword("test-user@localhost");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
initiateResetPasswordFromResetPasswordPage("test-user@localhost");
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).detail(Details.USERNAME, "test-user@localhost")
|
||||
.session((String) null)
|
||||
|
@ -206,16 +220,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
resetPassword("login@test.com");
|
||||
}
|
||||
|
||||
private void resetPassword(String username) throws IOException, MessagingException {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
private String resetPassword(String username) throws IOException, MessagingException {
|
||||
return resetPassword(username, "resetPassword");
|
||||
}
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword(username);
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
private String resetPassword(String username, String password) throws IOException, MessagingException {
|
||||
initiateResetPasswordFromResetPasswordPage(username);
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
|
||||
.user(userId)
|
||||
|
@ -224,9 +234,9 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
.session((String)null)
|
||||
.assertEvent();
|
||||
|
||||
assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
|
||||
|
||||
MimeMessage message = greenMail.getReceivedMessages()[0];
|
||||
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
|
||||
|
||||
String changePasswordUrl = getPasswordResetEmailLink(message);
|
||||
|
||||
|
@ -234,7 +244,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
updatePasswordPage.assertCurrent();
|
||||
|
||||
updatePasswordPage.changePassword("resetPassword", "resetPassword");
|
||||
updatePasswordPage.changePassword(password, password);
|
||||
|
||||
String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId();
|
||||
|
||||
|
@ -248,63 +258,27 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
loginPage.open();
|
||||
|
||||
loginPage.login("login-test", "resetPassword");
|
||||
loginPage.login("login-test", password);
|
||||
|
||||
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
|
||||
sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
|
||||
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
}
|
||||
|
||||
private void resetPassword(String username, String password) throws IOException, MessagingException {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword(username);
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
|
||||
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
|
||||
|
||||
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
|
||||
|
||||
String changePasswordUrl = getPasswordResetEmailLink(message);
|
||||
|
||||
driver.navigate().to(changePasswordUrl.trim());
|
||||
|
||||
updatePasswordPage.assertCurrent();
|
||||
|
||||
updatePasswordPage.changePassword(password, password);
|
||||
|
||||
String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId)
|
||||
.detail(Details.USERNAME, username).assertEvent().getSessionId();
|
||||
|
||||
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||
|
||||
events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent();
|
||||
|
||||
oauth.openLogout();
|
||||
|
||||
events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
|
||||
|
||||
return changePasswordUrl;
|
||||
}
|
||||
|
||||
private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword(username);
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
initiateResetPasswordFromResetPasswordPage(username);
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String)null)
|
||||
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
|
||||
|
||||
assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
|
||||
|
||||
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
|
||||
|
||||
String changePasswordUrl = getPasswordResetEmailLink(message);
|
||||
|
@ -320,17 +294,22 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
|
||||
public void initiateResetPasswordFromResetPasswordPage(String username) {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword("invalid");
|
||||
|
||||
resetPasswordPage.changePassword(username);
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
expectedMessagesCount++;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordWrongEmail() throws IOException, MessagingException, InterruptedException {
|
||||
initiateResetPasswordFromResetPasswordPage("invalid");
|
||||
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
|
||||
|
@ -359,15 +338,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
@Test
|
||||
public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException {
|
||||
try {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
initiateResetPasswordFromResetPasswordPage("login-test");
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
|
||||
.session((String)null)
|
||||
|
@ -403,15 +374,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
testRealm().update(realmRep);
|
||||
|
||||
try {
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
initiateResetPasswordFromResetPasswordPage("login-test");
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
|
||||
.session((String)null)
|
||||
|
@ -434,55 +397,50 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent();
|
||||
} finally {
|
||||
setTimeOffset(0);
|
||||
|
||||
realmRep.setAccessCodeLifespanUserAction(originalValue.get());
|
||||
testRealm().update(realmRep);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordDisabledUser() throws IOException, MessagingException, InterruptedException {
|
||||
UserRepresentation user = findUser("login-test");
|
||||
user.setEnabled(false);
|
||||
updateUser(user);
|
||||
try {
|
||||
user.setEnabled(false);
|
||||
updateUser(user);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
initiateResetPasswordFromResetPasswordPage("login-test");
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
|
||||
events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
|
||||
|
||||
user.setEnabled(true);
|
||||
updateUser(user);
|
||||
events.expectRequiredAction(EventType.RESET_PASSWORD).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("user_disabled").assertEvent();
|
||||
} finally {
|
||||
user.setEnabled(true);
|
||||
updateUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetPasswordNoEmail() throws IOException, MessagingException, InterruptedException {
|
||||
final String[] email = new String[1];
|
||||
final String email;
|
||||
|
||||
UserRepresentation user = findUser("login-test");
|
||||
email[0] = user.getEmail();
|
||||
user.setEmail("");
|
||||
updateUser(user);
|
||||
email = user.getEmail();
|
||||
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
try {
|
||||
user.setEmail("");
|
||||
updateUser(user);
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
initiateResetPasswordFromResetPasswordPage("login-test");
|
||||
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
|
||||
events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
|
||||
events.expectRequiredAction(EventType.RESET_PASSWORD_ERROR).session((String) null).user(userId).detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error("invalid_email").assertEvent();
|
||||
} finally {
|
||||
user.setEmail(email);
|
||||
updateUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -496,29 +454,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
RealmRepresentation realmRep = testRealm().toRepresentation();
|
||||
Map<String, String> oldSmtp = realmRep.getSmtpServer();
|
||||
|
||||
realmRep.setSmtpServer(smtpConfig);
|
||||
testRealm().update(realmRep);
|
||||
try {
|
||||
realmRep.setSmtpServer(smtpConfig);
|
||||
testRealm().update(realmRep);
|
||||
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
|
||||
errorPage.assertCurrent();
|
||||
errorPage.assertCurrent();
|
||||
|
||||
assertEquals("Failed to send email, please try again later.", errorPage.getError());
|
||||
assertEquals("Failed to send email, please try again later.", errorPage.getError());
|
||||
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
assertEquals(0, greenMail.getReceivedMessages().length);
|
||||
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
|
||||
.session((String)null)
|
||||
.detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
|
||||
|
||||
// Revert SMTP back
|
||||
realmRep.setSmtpServer(oldSmtp);
|
||||
testRealm().update(realmRep);
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD_ERROR).user(userId)
|
||||
.session((String)null)
|
||||
.detail(Details.USERNAME, "login-test").removeDetail(Details.CODE_ID).error(Errors.EMAIL_SEND_FAILED).assertEvent();
|
||||
} finally {
|
||||
// Revert SMTP back
|
||||
realmRep.setSmtpServer(oldSmtp);
|
||||
testRealm().update(realmRep);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPasswordPolicy(String policy) {
|
||||
|
@ -531,15 +491,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
|||
public void resetPasswordWithLengthPasswordPolicy() throws IOException, MessagingException {
|
||||
setPasswordPolicy("length");
|
||||
|
||||
loginPage.open();
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
||||
resetPasswordPage.changePassword("login-test");
|
||||
|
||||
loginPage.assertCurrent();
|
||||
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
|
||||
initiateResetPasswordFromResetPasswordPage("login-test");
|
||||
|
||||
assertEquals(1, greenMail.getReceivedMessages().length);
|
||||
|
||||
|
|
Loading…
Reference in a new issue