diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java index e6d658800d..38fa9d309b 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientBuilder.java @@ -170,8 +170,8 @@ public class HttpClientBuilder { return this; } - public HttpClientBuilder disableCookieCache() { - this.disableCookieCache = true; + public HttpClientBuilder disableCookieCache(boolean disable) { + this.disableCookieCache = disable; return this; } @@ -334,7 +334,7 @@ public class HttpClientBuilder { } public HttpClient build(AdapterHttpClientConfig adapterConfig) { - disableCookieCache(); // disable cookie cache as we don't want sticky sessions for load balancing + disableCookieCache(true); // disable cookie cache as we don't want sticky sessions for load balancing String truststorePath = adapterConfig.getTruststore(); if (truststorePath != null) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java index 0cdfab949c..f2555d4414 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java @@ -17,6 +17,8 @@ */ package org.keycloak.adapters.authorization; +import java.util.Set; + import org.jboss.logging.Logger; import org.keycloak.adapters.OIDCHttpFacade; import org.keycloak.adapters.spi.HttpFacade; @@ -26,8 +28,6 @@ import org.keycloak.authorization.client.resource.PermissionResource; import org.keycloak.authorization.client.resource.ProtectionResource; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig; -import java.util.Set; - /** * @author Pedro Igor */ @@ -52,7 +52,7 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer { private void challengeEntitlementAuthentication(OIDCHttpFacade facade) { HttpFacade.Response response = facade.getResponse(); AuthzClient authzClient = getAuthzClient(); - String clientId = authzClient.getConfiguration().getClientId(); + String clientId = authzClient.getConfiguration().getResource(); String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement"; response.setStatus(401); response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\""); @@ -65,7 +65,7 @@ public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer { HttpFacade.Response response = facade.getResponse(); AuthzClient authzClient = getAuthzClient(); String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient); - String clientId = authzClient.getConfiguration().getClientId(); + String clientId = authzClient.getConfiguration().getResource(); String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize"; response.setStatus(401); response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\""); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java index 316a39d41e..0dbddd4b47 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java @@ -127,7 +127,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { AccessToken token = httpFacade.getSecurityContext().getToken(); if (token.getAuthorization() == null) { - EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getClientId()); + EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getResource()); return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment); } else { EntitlementRequest request = new EntitlementRequest(); @@ -137,7 +137,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes())); LOGGER.debugf("Sending entitlements request: resource_set_id [%s], resource_set_name [%s], scopes [%s].", permissionRequest.getResourceSetId(), permissionRequest.getResourceSetName(), permissionRequest.getScopes()); request.addPermission(permissionRequest); - EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getClientId(), request); + EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getResource(), request); return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment); } } diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java index e28500beeb..e03158f99a 100755 --- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java +++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java @@ -168,7 +168,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte HttpSession httpSession = request.getSession(); httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount); httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext()); - if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId()); + if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getSessionState(), account.getPrincipal().getName(), httpSession.getId()); //String username = securityContext.getToken().getSubject(); //log.fine("userSessionManagement.login: " + username); } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java index 835c830b91..647891ff4a 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java @@ -17,44 +17,33 @@ */ package org.keycloak.authorization.client; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClients; -import org.keycloak.util.BasicAuthHelper; - import java.util.HashMap; import java.util.Map; +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.HttpClients; +import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.util.BasicAuthHelper; +import com.fasterxml.jackson.annotation.JsonIgnore; + /** * @author Pedro Igor */ -public class Configuration { +public class Configuration extends AdapterConfig { @JsonIgnore private HttpClient httpClient; - @JsonProperty("auth-server-url") - protected String authServerUrl; - - @JsonProperty("realm") - protected String realm; - - @JsonProperty("resource") - protected String clientId; - - @JsonProperty("credentials") - protected Map clientCredentials = new HashMap<>(); - public Configuration() { } public Configuration(String authServerUrl, String realm, String clientId, Map clientCredentials, HttpClient httpClient) { this.authServerUrl = authServerUrl; - this.realm = realm; - this.clientId = clientId; - this.clientCredentials = clientCredentials; + setAuthServerUrl(authServerUrl); + setRealm(realm); + setResource(clientId); + setCredentials(clientCredentials); this.httpClient = httpClient; } @@ -62,13 +51,13 @@ public class Configuration { private ClientAuthenticator clientAuthenticator = new ClientAuthenticator() { @Override public void configureClientCredentials(HashMap requestParams, HashMap requestHeaders) { - String secret = (String) clientCredentials.get("secret"); + String secret = (String) getCredentials().get("secret"); if (secret == null) { throw new RuntimeException("Client secret not provided."); } - requestHeaders.put("Authorization", BasicAuthHelper.createHeader(clientId, secret)); + requestHeaders.put("Authorization", BasicAuthHelper.createHeader(getResource(), secret)); } }; @@ -80,23 +69,7 @@ public class Configuration { return httpClient; } - public String getClientId() { - return clientId; - } - - public String getAuthServerUrl() { - return authServerUrl; - } - public ClientAuthenticator getClientAuthenticator() { return this.clientAuthenticator; } - - public Map getClientCredentials() { - return clientCredentials; - } - - public String getRealm() { - return realm; - } } diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java index db8fc5ae5b..0e3c08bbca 100755 --- a/core/src/main/java/org/keycloak/RSATokenVerifier.java +++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java @@ -29,10 +29,10 @@ import java.security.PublicKey; */ public class RSATokenVerifier { - private TokenVerifier tokenVerifier; + private final TokenVerifier tokenVerifier; private RSATokenVerifier(String tokenString) { - this.tokenVerifier = TokenVerifier.create(tokenString); + this.tokenVerifier = TokenVerifier.create(tokenString, AccessToken.class).withDefaultChecks(); } public static RSATokenVerifier create(String tokenString) { diff --git a/core/src/main/java/org/keycloak/TokenVerifier.java b/core/src/main/java/org/keycloak/TokenVerifier.java index 9c30bfdc69..0c6e2db1a8 100755 --- a/core/src/main/java/org/keycloak/TokenVerifier.java +++ b/core/src/main/java/org/keycloak/TokenVerifier.java @@ -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,280 @@ 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.*; +import java.util.logging.Level; +import java.util.logging.Logger; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class TokenVerifier { +public class TokenVerifier { - private final String tokenString; + private static final Logger LOG = Logger.getLogger(TokenVerifier.class.getName()); + + // This interface is here as JDK 7 is a requirement for this project. + // Once JDK 8 would become mandatory, java.util.function.Predicate would be used instead. + + /** + * Functional interface of checks that verify some part of a JWT. + * @param Type of the token handled by this predicate. + */ + // @FunctionalInterface + public static interface Predicate { + /** + * 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 SUBJECT_EXISTS_CHECK = new Predicate() { + @Override + public boolean test(JsonWebToken t) throws VerificationException { + String subject = t.getSubject(); + if (subject == null) { + throw new VerificationException("Subject missing in token"); + } + + return true; + } + }; + + /** + * Check for token being neither expired nor used before it gets valid. + * @see JsonWebToken#isActive() + */ + public static final Predicate IS_ACTIVE = new Predicate() { + @Override + public boolean test(JsonWebToken t) throws VerificationException { + if (! t.isActive()) { + throw new TokenNotActiveException(t, "Token is not active"); + } + + return true; + } + }; + + public static class RealmUrlCheck implements Predicate { + + 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 { + + 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 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> checks = new LinkedList<>(); private JWSInput jws; - private AccessToken token; + private T token; - protected TokenVerifier(String tokenString) { + protected TokenVerifier(String tokenString, Class 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 an instance of {@code TokenVerifier} from the given string on a JWT of the given class. + * The token verifier has no checks defined. Note that the checks are only tested when + * {@link #verify()} method is invoked. + * @param Type of the token + * @param tokenString String representation of JWT + * @param clazz Class of the token + * @return + */ + public static TokenVerifier create(String tokenString, Class clazz) { + return new TokenVerifier(tokenString, clazz); + } + + /** + * Creates an instance of {@code TokenVerifier} from the given string on a JWT of the given class. + * The token verifier has no checks defined. Note that the checks are only tested when + * {@link #verify()} method is invoked. + * @return + */ + public static TokenVerifier create(T token) { + return new TokenVerifier(token); + } + + /** + * Adds default checks to the token verification: + *
    + *
  • Realm URL (JWT issuer field: {@code iss}) has to be defined and match realm set via {@link #realmUrl(java.lang.String)} method
  • + *
  • Subject (JWT subject field: {@code sub}) has to be defined
  • + *
  • Token type (JWT type field: {@code typ}) has to be {@code Bearer}. The type can be set via {@link #tokenType(java.lang.String)} method
  • + *
  • Token has to be active, ie. both not expired and not used before its validity (JWT issuer fields: {@code exp} and {@code nbf})
  • + *
+ * @return This token verifier. + */ + public TokenVerifier withDefaultChecks() { + return withChecks( + RealmUrlCheck.NULL_INSTANCE, + SUBJECT_EXISTS_CHECK, + TokenTypeCheck.INSTANCE_BEARER, + IS_ACTIVE + ); + } + + private void removeCheck(Class> checkClass) { + for (Iterator> it = checks.iterator(); it.hasNext();) { + if (it.next().getClass() == checkClass) { + it.remove(); + } + } + } + + private void removeCheck(Predicate check) { + checks.remove(check); + } + + private

> TokenVerifier replaceCheck(Class> checkClass, boolean active, P predicate) { + removeCheck(checkClass); + if (active) { + checks.add(predicate); + } + return this; + } + + private

> TokenVerifier replaceCheck(Predicate check, boolean active, P predicate) { + removeCheck(check); + if (active) { + checks.add(predicate); + } + return this; + } + + /** + * Will test the given checks in {@link #verify()} method in addition to already set checks. + * @param checks + * @return + */ + public TokenVerifier withChecks(Predicate... checks) { + if (checks != null) { + this.checks.addAll(Arrays.asList(checks)); + } + return this; + } + + /** + * Sets the key for verification of RSA-based signature. + * @param publicKey + * @return + */ + public TokenVerifier publicKey(PublicKey publicKey) { this.publicKey = publicKey; return this; } - public TokenVerifier secretKey(SecretKey secretKey) { + /** + * Sets the key for verification of HMAC-based signature. + * @param secretKey + * @return + */ + public TokenVerifier secretKey(SecretKey secretKey) { this.secretKey = secretKey; return this; } - public TokenVerifier realmUrl(String realmUrl) { + /** + * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}. + * @return This token verifier + */ + public TokenVerifier realmUrl(String realmUrl) { this.realmUrl = realmUrl; - return this; + return replaceCheck(RealmUrlCheck.class, checkRealmUrl, new RealmUrlCheck(realmUrl)); } - public TokenVerifier checkTokenType(boolean checkTokenType) { + /** + * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}. + * @return This token verifier + */ + public TokenVerifier 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; + /** + * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}. + * @return This token verifier + */ + public TokenVerifier tokenType(String tokenType) { + this.expectedTokenType = tokenType; + return replaceCheck(TokenTypeCheck.class, this.checkTokenType, new TokenTypeCheck(expectedTokenType)); } - public TokenVerifier checkRealmUrl(boolean checkRealmUrl) { + /** + * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}. + * @return This token verifier + */ + public TokenVerifier checkActive(boolean checkActive) { + return replaceCheck(IS_ACTIVE, checkActive, IS_ACTIVE); + } + + /** + * @deprecated This method is here only for backward compatibility with previous version of {@code TokenVerifier}. + * @return This token verifier + */ + public TokenVerifier checkRealmUrl(boolean checkRealmUrl) { this.checkRealmUrl = checkRealmUrl; - return this; + return replaceCheck(RealmUrlCheck.class, this.checkRealmUrl, new RealmUrlCheck(realmUrl)); } - public TokenVerifier parse() throws VerificationException { + public TokenVerifier parse() throws VerificationException { if (jws == null) { if (tokenString == null) { throw new VerificationException("Token not set"); @@ -100,7 +314,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 +322,10 @@ public class TokenVerifier { return this; } - public AccessToken getToken() throws VerificationException { - parse(); + public T getToken() throws VerificationException { + if (token == null) { + parse(); + } return token; } @@ -118,53 +334,97 @@ 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(token, "Invalid token signature"); + } break; + case HMAC: + if (secretKey == null) { + throw new VerificationException("Secret key not set"); + } + if (!HMACProvider.verify(jws, secretKey)) { + throw new TokenSignatureInvalidException(token, "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 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 check : checks) { + if (! check.test(getToken())) { + throw new VerificationException("JWT check failed for check " + check); + } } return this; } + /** + * Creates an optional predicate from a predicate that will proceed with check but always pass. + * @param + * @param mandatoryPredicate + * @return + */ + public static Predicate optional(final Predicate mandatoryPredicate) { + return new Predicate() { + @Override + public boolean test(T t) throws VerificationException { + try { + if (! mandatoryPredicate.test(t)) { + LOG.finer("[optional] predicate failed: " + mandatoryPredicate); + } + + return true; + } catch (VerificationException ex) { + LOG.log(Level.FINER, "[optional] predicate " + mandatoryPredicate + " failed.", ex); + return true; + } + } + }; + } + + /** + * Creates a predicate that will proceed with checks of the given predicates + * and will pass if and only if at least one of the given predicates passes. + * @param + * @param predicates + * @return + */ + public static Predicate alternative(final Predicate... predicates) { + return new Predicate() { + @Override + public boolean test(T t) throws VerificationException { + for (Predicate predicate : predicates) { + try { + if (predicate.test(t)) { + return true; + } + + LOG.finer("[alternative] predicate failed: " + predicate); + } catch (VerificationException ex) { + LOG.log(Level.FINER, "[alternative] predicate " + predicate + " failed.", ex); + } + } + + return false; + } + }; + } } diff --git a/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java new file mode 100644 index 0000000000..4740567539 --- /dev/null +++ b/core/src/main/java/org/keycloak/exceptions/TokenNotActiveException.java @@ -0,0 +1,44 @@ +/* + * 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.representations.JsonWebToken; + +/** + * Exception thrown for cases when token is invalid due to time constraints (expired, or not yet valid). + * Cf. {@link JsonWebToken#isActive()}. + * @author hmlnarik + */ +public class TokenNotActiveException extends TokenVerificationException { + + public TokenNotActiveException(JsonWebToken token) { + super(token); + } + + public TokenNotActiveException(JsonWebToken token, String message) { + super(token, message); + } + + public TokenNotActiveException(JsonWebToken token, String message, Throwable cause) { + super(token, message, cause); + } + + public TokenNotActiveException(JsonWebToken token, Throwable cause) { + super(token, cause); + } + +} diff --git a/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java new file mode 100644 index 0000000000..4d389eb8d7 --- /dev/null +++ b/core/src/main/java/org/keycloak/exceptions/TokenSignatureInvalidException.java @@ -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.representations.JsonWebToken; + +/** + * Thrown when token signature is invalid. + * @author hmlnarik + */ +public class TokenSignatureInvalidException extends TokenVerificationException { + + public TokenSignatureInvalidException(JsonWebToken token) { + super(token); + } + + public TokenSignatureInvalidException(JsonWebToken token, String message) { + super(token, message); + } + + public TokenSignatureInvalidException(JsonWebToken token, String message, Throwable cause) { + super(token, message, cause); + } + + public TokenSignatureInvalidException(JsonWebToken token, Throwable cause) { + super(token, cause); + } + +} diff --git a/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java b/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java new file mode 100644 index 0000000000..4d6b7d0177 --- /dev/null +++ b/core/src/main/java/org/keycloak/exceptions/TokenVerificationException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.exceptions; + +import org.keycloak.common.VerificationException; +import org.keycloak.representations.JsonWebToken; + +/** + * Exception thrown on failed verification of a token. + * + * @author hmlnarik + */ +public class TokenVerificationException extends VerificationException { + + private final JsonWebToken token; + + public TokenVerificationException(JsonWebToken token) { + this.token = token; + } + + public TokenVerificationException(JsonWebToken token, String message) { + super(message); + this.token = token; + } + + public TokenVerificationException(JsonWebToken token, String message, Throwable cause) { + super(message, cause); + this.token = token; + } + + public TokenVerificationException(JsonWebToken token, Throwable cause) { + super(cause); + this.token = token; + } + + public JsonWebToken getToken() { + return token; + } + +} diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java index 4ef6831678..36778e102d 100755 --- a/core/src/main/java/org/keycloak/representations/AccessToken.java +++ b/core/src/main/java/org/keycloak/representations/AccessToken.java @@ -97,9 +97,6 @@ public class AccessToken extends IDToken { } } - @JsonProperty("client_session") - protected String clientSession; - @JsonProperty("trusted-certs") protected Set trustedCertificates; @@ -156,10 +153,6 @@ public class AccessToken extends IDToken { return resourceAccess.get(resource); } - public String getClientSession() { - return clientSession; - } - public Access addAccess(String service) { Access access = resourceAccess.get(service); if (access != null) return access; @@ -168,11 +161,6 @@ public class AccessToken extends IDToken { return access; } - public AccessToken clientSession(String session) { - this.clientSession = session; - return this; - } - @Override public AccessToken id(String id) { return (AccessToken) super.id(id); diff --git a/core/src/main/java/org/keycloak/representations/RefreshToken.java b/core/src/main/java/org/keycloak/representations/RefreshToken.java index 4b89cf6f70..3a7a95188d 100755 --- a/core/src/main/java/org/keycloak/representations/RefreshToken.java +++ b/core/src/main/java/org/keycloak/representations/RefreshToken.java @@ -40,7 +40,6 @@ public class RefreshToken extends AccessToken { */ public RefreshToken(AccessToken token) { this(); - this.clientSession = token.getClientSession(); this.issuer = token.issuer; this.subject = token.subject; this.issuedFor = token.issuedFor; diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index b14e55bda1..670e1d8bde 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -46,6 +46,8 @@ public class RealmRepresentation { protected Integer accessCodeLifespan; protected Integer accessCodeLifespanUserAction; protected Integer accessCodeLifespanLogin; + protected Integer actionTokenGeneratedByAdminLifespan; + protected Integer actionTokenGeneratedByUserLifespan; protected Boolean enabled; protected String sslRequired; @Deprecated @@ -338,6 +340,22 @@ public class RealmRepresentation { this.accessCodeLifespanLogin = accessCodeLifespanLogin; } + public Integer getActionTokenGeneratedByAdminLifespan() { + return actionTokenGeneratedByAdminLifespan; + } + + public void setActionTokenGeneratedByAdminLifespan(Integer actionTokenGeneratedByAdminLifespan) { + this.actionTokenGeneratedByAdminLifespan = actionTokenGeneratedByAdminLifespan; + } + + public Integer getActionTokenGeneratedByUserLifespan() { + return actionTokenGeneratedByUserLifespan; + } + + public void setActionTokenGeneratedByUserLifespan(Integer actionTokenGeneratedByUserLifespan) { + this.actionTokenGeneratedByUserLifespan = actionTokenGeneratedByUserLifespan; + } + public List getDefaultRoles() { return defaultRoles; } diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl index 882d1b18f5..d78ff753e7 100755 --- a/distribution/demo-dist/src/main/xslt/standalone.xsl +++ b/distribution/demo-dist/src/main/xslt/standalone.xsl @@ -89,9 +89,11 @@ + + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli index def555ec91..0f477458de 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-clustered.cli @@ -199,4 +199,30 @@ if ((result.default-provider == undefined) && (result.provider.default.enabled = echo end-if +# Migrate from 3.0.0 to 3.2.0 +if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/:read-resource + echo Adding distributed-cache=authenticationSessions to keycloak cache container... + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/:add(mode=SYNC,owners=1) + echo +end-if + +if (outcome == failed) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:read-resource + echo Adding local-cache=actionTokens to keycloak cache container... + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:add(indexing=NONE,start=LAZY) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) + echo +end-if + +if (outcome == success) of /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization/:read-resource + echo Replacing distributed-cache=authorization with local-cache=authorization + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization/:remove + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=authorization/:add + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=authorization/component=eviction/:write-attribute(name=strategy,value=LRU) + /profile=$clusteredProfile/subsystem=infinispan/cache-container=keycloak/local-cache=authorization/component=eviction/:write-attribute(name=max-entries,value=10000) + echo +end-if + echo *** End Migration of /profile=$clusteredProfile *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli index 4547b2ac8b..fc01c29cc1 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-domain-standalone.cli @@ -187,4 +187,22 @@ if ((result.default-provider == undefined) && (result.provider.default.enabled = echo end-if + +# Migrate from 3.0.0 to 3.2.0 +if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=authenticationSessions/:read-resource + echo Adding local-cache=authenticationSessions to keycloak cache container... + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=authenticationSessions/:add(indexing=NONE,start=LAZY) + echo +end-if + +if (outcome == failed) of /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:read-resource + echo Adding local-cache=actionTokens to keycloak cache container... + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:add(indexing=NONE,start=LAZY) + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) + /profile=$standaloneProfile/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) + echo +end-if + echo *** End Migration of /profile=$standaloneProfile *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli index 65b6ef96ec..4d5fac67db 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone-ha.cli @@ -203,4 +203,31 @@ if ((result.default-provider == undefined) && (result.provider.default.enabled = /subsystem=keycloak-server/spi=connectionsInfinispan/:write-attribute(name=default-provider,value=default) echo end-if + +# Migrate from 3.0.0 to 3.2.0 +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/:read-resource + echo Adding distributed-cache=authenticationSessions to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions/:add(mode=SYNC,owners=1) + echo +end-if + +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:read-resource + echo Adding local-cache=actionTokens to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:add(indexing=NONE,start=LAZY) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) + echo +end-if + +if (outcome == success) of /subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization/:read-resource + echo Replacing distributed-cache=authorization with local-cache=authorization + /subsystem=infinispan/cache-container=keycloak/distributed-cache=authorization/:remove + /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/:add + /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/component=eviction/:write-attribute(name=strategy,value=LRU) + /subsystem=infinispan/cache-container=keycloak/local-cache=authorization/component=eviction/:write-attribute(name=max-entries,value=10000) + echo +end-if + echo *** End Migration *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli index 3e0515deed..517759f3bb 100644 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/bin/migrate-standalone.cli @@ -195,4 +195,22 @@ if ((result.default-provider == undefined) && (result.provider.default.enabled = echo end-if + +# Migrate from 3.0.0 to 3.2.0 +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=authenticationSessions/:read-resource + echo Adding local-cache=authenticationSessions to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/local-cache=authenticationSessions/:add(indexing=NONE,start=LAZY) + echo +end-if + +if (outcome == failed) of /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:read-resource + echo Adding local-cache=actionTokens to keycloak cache container... + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/:add(indexing=NONE,start=LAZY) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=strategy,value=NONE) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=eviction/:write-attribute(name=max-entries,value=-1) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=interval,value=300000) + /subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/component=expiration/:write-attribute(name=max-idle,value=-1) + echo +end-if + echo *** End Migration *** \ No newline at end of file diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml index e7fdb8abed..4388f83dfc 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-infinispan/main/module.xml @@ -33,6 +33,7 @@ + diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-base.cli index f24502ae4e..5ce3122fa8 100644 --- a/distribution/server-overlay/src/main/cli/keycloak-install-base.cli +++ b/distribution/server-overlay/src/main/cli/keycloak-install-base.cli @@ -6,6 +6,7 @@ embed-server --server-config=standalone.xml /subsystem=infinispan/cache-container=keycloak/local-cache=users:add() /subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU) /subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=authenticationSessions:add() /subsystem=infinispan/cache-container=keycloak/local-cache=offlineSessions:add() /subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add() /subsystem=infinispan/cache-container=keycloak/local-cache=work:add() @@ -14,4 +15,7 @@ embed-server --server-config=standalone.xml /subsystem=infinispan/cache-container=keycloak/local-cache=keys:add() /subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU) /subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000) +/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/eviction=EVICTION:add(max-entries=-1,strategy=NONE) +/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/expiration=EXPIRATION:add(max-idle=-1,interval=300000) /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) diff --git a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli index ec2b56ff23..4710eb8a82 100644 --- a/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli +++ b/distribution/server-overlay/src/main/cli/keycloak-install-ha-base.cli @@ -7,6 +7,7 @@ embed-server --server-config=standalone-ha.xml /subsystem=infinispan/cache-container=keycloak/local-cache=users:add() /subsystem=infinispan/cache-container=keycloak/local-cache=users/eviction=EVICTION:add(max-entries=10000,strategy=LRU) /subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1") +/subsystem=infinispan/cache-container=keycloak/distributed-cache=authenticationSessions:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=offlineSessions:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1") /subsystem=infinispan/cache-container=keycloak/local-cache=authorization:add() @@ -15,4 +16,7 @@ embed-server --server-config=standalone-ha.xml /subsystem=infinispan/cache-container=keycloak/local-cache=keys:add() /subsystem=infinispan/cache-container=keycloak/local-cache=keys/eviction=EVICTION:add(max-entries=1000,strategy=LRU) /subsystem=infinispan/cache-container=keycloak/local-cache=keys/expiration=EXPIRATION:add(max-idle=3600000) +/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens:add() +/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/eviction=EVICTION:add(max-entries=-1,strategy=NONE) +/subsystem=infinispan/cache-container=keycloak/local-cache=actionTokens/expiration=EXPIRATION:add(max-idle=-1,interval=300000) /extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem) diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json index f6b9c90927..7983fa39f1 100644 --- a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json @@ -1,13 +1,10 @@ { "realm": "servlet-authz", - "auth-server-url" : "http://localhost:8080/auth", - "ssl-required" : "external", - "resource" : "servlet-authz-app", - "public-client" : false, + "auth-server-url": "http://localhost:8080/auth", + "ssl-required": "external", + "resource": "servlet-authz-app", "credentials": { "secret": "secret" }, - "policy-enforcer": { - "on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp" - } + "policy-enforcer": {} } \ No newline at end of file diff --git a/examples/kerberos/README.md b/examples/kerberos/README.md index 02bffddf99..2c1d335ace 100644 --- a/examples/kerberos/README.md +++ b/examples/kerberos/README.md @@ -47,7 +47,7 @@ is in your `/etc/hosts` before other records for the 127.0.0.1 host to avoid iss **5)** Configure Kerberos client (On linux it's in file `/etc/krb5.conf` ). You need to configure `KEYCLOAK.ORG` realm for host `localhost` and enable `forwardable` flag, which is needed for credential delegation example, as application needs to forward Kerberos ticket and authenticate with it against LDAP server. -See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration/src/test/resources/kerberos/test-krb5.conf) for inspiration. +See [this file](https://github.com/keycloak/keycloak/blob/master/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/test-krb5.conf) for inspiration. On OS X the file to edit (or create) is `/Library/Preferences/edu.mit.Kerberos` with the same syntax as `krb5.conf`. On Windows the file to edit (or create) is `c:\Windows\krb5.ini` with the same syntax as `krb5.conf`. diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java index 3fd7778c49..ef09dde939 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientPoliciesResource.java @@ -27,6 +27,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.jboss.resteasy.annotations.cache.NoCache; +import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; /** diff --git a/misc/Testsuite.md b/misc/Testsuite.md index 8403806470..cb77ad7a59 100644 --- a/misc/Testsuite.md +++ b/misc/Testsuite.md @@ -132,6 +132,12 @@ kinit hnelson@KEYCLOAK.ORG and provide password `secret` Now when you access `http://localhost:8081/auth/realms/master/account` you should be logged in automatically as user `hnelson` . + +Simple loadbalancer +------------------- + +You can run class `SimpleUndertowLoadBalancer` from IDE. By default, it executes the embedded undertow loadbalancer running on `http://localhost:8180`, which communicates with 2 backend Keycloak nodes +running on `http://localhost:8181` and `http://localhost:8182` . See javadoc for more details. Create many users or offline sessions diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 5309fc9e70..17ae1219fb 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -19,6 +19,8 @@ package org.keycloak.connections.infinispan; import java.util.concurrent.TimeUnit; +import org.infinispan.commons.util.FileLookup; +import org.infinispan.commons.util.FileLookupFactory; import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.Configuration; import org.infinispan.configuration.cache.ConfigurationBuilder; @@ -27,12 +29,13 @@ import org.infinispan.eviction.EvictionStrategy; import org.infinispan.eviction.EvictionType; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.EmbeddedCacheManager; -import org.infinispan.persistence.remote.configuration.ExhaustedAction; import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; import org.infinispan.transaction.LockingMode; import org.infinispan.transaction.TransactionMode; import org.infinispan.transaction.lookup.DummyTransactionManagerLookup; import org.jboss.logging.Logger; +import org.jgroups.JChannel; import org.keycloak.Config; import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory; import org.keycloak.models.KeycloakSession; @@ -118,7 +121,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, getRevisionCacheConfig(userRevisionsMaxEntries)); cacheManager.getCache(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true); cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, true); long authzRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME).getCacheConfiguration().eviction().maxEntries(); authzRevisionsMaxEntries = authzRevisionsMaxEntries > 0 @@ -147,7 +153,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true); if (clustered) { - gcb.transport().defaultTransport(); + String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); + configureTransport(gcb, nodeName); } gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains); @@ -190,6 +197,13 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration); cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, sessionCacheConfiguration); cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration); + cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfiguration); + + // Retrieve caches to enforce rebalance + cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, true); + cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true); ConfigurationBuilder replicationConfigBuilder = new ConfigurationBuilder(); if (clustered) { @@ -229,6 +243,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig()); cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true); + cacheManager.defineConfiguration(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, getActionTokenCacheConfig()); + cacheManager.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE, true); + long authzRevisionsMaxEntries = cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME).getCacheConfiguration().eviction().maxEntries(); authzRevisionsMaxEntries = authzRevisionsMaxEntries > 0 ? 2 * authzRevisionsMaxEntries @@ -286,4 +303,40 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon return cb.build(); } + private Configuration getActionTokenCacheConfig() { + ConfigurationBuilder cb = new ConfigurationBuilder(); + + cb.eviction() + .strategy(EvictionStrategy.NONE) + .type(EvictionType.COUNT) + .size(InfinispanConnectionProvider.ACTION_TOKEN_CACHE_DEFAULT_MAX); + cb.expiration() + .maxIdle(InfinispanConnectionProvider.ACTION_TOKEN_MAX_IDLE_SECONDS, TimeUnit.SECONDS) + .wakeUpInterval(InfinispanConnectionProvider.ACTION_TOKEN_WAKE_UP_INTERVAL_SECONDS, TimeUnit.SECONDS); + + return cb.build(); + } + + protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName) { + if (nodeName == null) { + gcb.transport().defaultTransport(); + } else { + FileLookup fileLookup = FileLookupFactory.newInstance(); + + try { + // Compatibility with Wildfly + JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader())); + channel.setName(nodeName); + JGroupsTransport transport = new JGroupsTransport(channel); + + gcb.transport().nodeName(nodeName); + gcb.transport().transport(transport); + + logger.infof("Configured jgroups transport with the channel name: %s", nodeName); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index bd57793dfb..8618a699ec 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -36,15 +36,24 @@ public interface InfinispanConnectionProvider extends Provider { String SESSION_CACHE_NAME = "sessions"; String OFFLINE_SESSION_CACHE_NAME = "offlineSessions"; String LOGIN_FAILURE_CACHE_NAME = "loginFailures"; + String AUTHENTICATION_SESSIONS_CACHE_NAME = "authenticationSessions"; String WORK_CACHE_NAME = "work"; String AUTHORIZATION_CACHE_NAME = "authorization"; String AUTHORIZATION_REVISIONS_CACHE_NAME = "authorizationRevisions"; int AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX = 20000; + String ACTION_TOKEN_CACHE = "actionTokens"; + int ACTION_TOKEN_CACHE_DEFAULT_MAX = -1; + int ACTION_TOKEN_MAX_IDLE_SECONDS = -1; + long ACTION_TOKEN_WAKE_UP_INTERVAL_SECONDS = 5 * 60 * 1000l; + String KEYS_CACHE_NAME = "keys"; int KEYS_CACHE_DEFAULT_MAX = 1000; int KEYS_CACHE_MAX_IDLE_SECONDS = 3600; + // System property used on Wildfly to identify distributedCache address and sticky session route + String JBOSS_NODE_NAME = "jboss.node.name"; + Cache getCache(String name); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java new file mode 100644 index 0000000000..37a1a218d5 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016 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.models.cache.infinispan; + +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; + +/** + * Event requesting adding of an invalidated action token. + */ +public class AddInvalidatedActionTokenEvent implements ClusterEvent { + + private final ActionTokenReducedKey key; + private final int expirationInSecs; + private final ActionTokenValueEntity tokenValue; + + public AddInvalidatedActionTokenEvent(ActionTokenReducedKey key, int expirationInSecs, ActionTokenValueEntity tokenValue) { + this.key = key; + this.expirationInSecs = expirationInSecs; + this.tokenValue = tokenValue; + } + + public ActionTokenReducedKey getKey() { + return key; + } + + public int getExpirationInSecs() { + return expirationInSecs; + } + + public ActionTokenValueEntity getTokenValue() { + return tokenValue; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 8350f0dc30..0bed8262a1 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -474,6 +474,30 @@ public class RealmAdapter implements CachedRealmModel { updated.setAccessCodeLifespanLogin(seconds); } + @Override + public int getActionTokenGeneratedByAdminLifespan() { + if (isUpdated()) return updated.getActionTokenGeneratedByAdminLifespan(); + return cached.getActionTokenGeneratedByAdminLifespan(); + } + + @Override + public void setActionTokenGeneratedByAdminLifespan(int seconds) { + getDelegateForUpdate(); + updated.setActionTokenGeneratedByAdminLifespan(seconds); + } + + @Override + public int getActionTokenGeneratedByUserLifespan() { + if (isUpdated()) return updated.getActionTokenGeneratedByUserLifespan(); + return cached.getActionTokenGeneratedByUserLifespan(); + } + + @Override + public void setActionTokenGeneratedByUserLifespan(int seconds) { + getDelegateForUpdate(); + updated.setActionTokenGeneratedByUserLifespan(seconds); + } + @Override public List getRequiredCredentials() { if (isUpdated()) return updated.getRequiredCredentials(); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java new file mode 100644 index 0000000000..0a4d858b52 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 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.models.cache.infinispan; + +import org.keycloak.cluster.ClusterEvent; + +/** + * Event requesting removal of the action tokens with the given user and action regardless of nonce. + */ +public class RemoveActionTokensSpecificEvent implements ClusterEvent { + + private final String userId; + private final String actionId; + + public RemoveActionTokensSpecificEvent(String userId, String actionId) { + this.userId = userId; + this.actionId = actionId; + } + + public String getUserId() { + return userId; + } + + public String getActionId() { + return actionId; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java index fef0486af1..3668d9740e 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java @@ -83,6 +83,8 @@ public class CachedRealm extends AbstractExtendableRevisioned { protected int accessCodeLifespan; protected int accessCodeLifespanUserAction; protected int accessCodeLifespanLogin; + protected int actionTokenGeneratedByAdminLifespan; + protected int actionTokenGeneratedByUserLifespan; protected int notBefore; protected PasswordPolicy passwordPolicy; protected OTPPolicy otpPolicy; @@ -175,6 +177,8 @@ public class CachedRealm extends AbstractExtendableRevisioned { accessCodeLifespan = model.getAccessCodeLifespan(); accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction(); accessCodeLifespanLogin = model.getAccessCodeLifespanLogin(); + actionTokenGeneratedByAdminLifespan = model.getActionTokenGeneratedByAdminLifespan(); + actionTokenGeneratedByUserLifespan = model.getActionTokenGeneratedByUserLifespan(); notBefore = model.getNotBefore(); passwordPolicy = model.getPasswordPolicy(); otpPolicy = model.getOTPPolicy(); @@ -399,6 +403,14 @@ public class CachedRealm extends AbstractExtendableRevisioned { return accessCodeLifespanLogin; } + public int getActionTokenGeneratedByAdminLifespan() { + return actionTokenGeneratedByAdminLifespan; + } + + public int getActionTokenGeneratedByUserLifespan() { + return actionTokenGeneratedByUserLifespan; + } + public List getRequiredCredentials() { return requiredCredentials; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java new file mode 100644 index 0000000000..d7bdcdfcce --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java @@ -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.models.cache.infinispan.events; + +import org.keycloak.cluster.ClusterEvent; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * + * @author hmlnarik + */ +public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent { + + private String authSessionId; + + private Map authNotesFragment; + + public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map authNotesFragment) { + AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent(); + event.authSessionId = authSessionId; + event.authNotesFragment = new LinkedHashMap<>(authNotesFragment); + return event; + } + + public String getAuthSessionId() { + return authSessionId; + } + + public Map getAuthNotesFragment() { + return authNotesFragment; + } + + @Override + public String toString() { + return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java new file mode 100644 index 0000000000..13352dfcab --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticatedClientSessionAdapter.java @@ -0,0 +1,197 @@ +/* + * Copyright 2016 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.models.sessions.infinispan; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.infinispan.Cache; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; + +/** + * @author Marek Posolda + */ +public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel { + + private final AuthenticatedClientSessionEntity entity; + private final ClientModel client; + private final InfinispanUserSessionProvider provider; + private final Cache cache; + private UserSessionAdapter userSession; + + public AuthenticatedClientSessionAdapter(AuthenticatedClientSessionEntity entity, ClientModel client, UserSessionAdapter userSession, InfinispanUserSessionProvider provider, Cache cache) { + this.provider = provider; + this.entity = entity; + this.client = client; + this.cache = cache; + this.userSession = userSession; + } + + private void update() { + provider.getTx().replace(cache, userSession.getEntity().getId(), userSession.getEntity()); + } + + + @Override + public void setUserSession(UserSessionModel userSession) { + String clientUUID = client.getId(); + UserSessionEntity sessionEntity = this.userSession.getEntity(); + + // Dettach userSession + if (userSession == null) { + if (sessionEntity.getAuthenticatedClientSessions() != null) { + sessionEntity.getAuthenticatedClientSessions().remove(clientUUID); + update(); + this.userSession = null; + } + } else { + this.userSession = (UserSessionAdapter) userSession; + + if (sessionEntity.getAuthenticatedClientSessions() == null) { + sessionEntity.setAuthenticatedClientSessions(new HashMap<>()); + } + sessionEntity.getAuthenticatedClientSessions().put(clientUUID, entity); + update(); + } + } + + @Override + public UserSessionModel getUserSession() { + return this.userSession; + } + + @Override + public String getRedirectUri() { + return entity.getRedirectUri(); + } + + @Override + public void setRedirectUri(String uri) { + entity.setRedirectUri(uri); + update(); + } + + @Override + public String getId() { + return null; + } + + @Override + public RealmModel getRealm() { + return userSession.getRealm(); + } + + @Override + public ClientModel getClient() { + return client; + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + update(); + } + + @Override + public String getAction() { + return entity.getAction(); + } + + @Override + public void setAction(String action) { + entity.setAction(action); + update(); + } + + @Override + public String getProtocol() { + return entity.getAuthMethod(); + } + + @Override + public void setProtocol(String method) { + entity.setAuthMethod(method); + update(); + } + + @Override + public Set getRoles() { + return entity.getRoles(); + } + + @Override + public void setRoles(Set roles) { + entity.setRoles(roles); + update(); + } + + @Override + public Set getProtocolMappers() { + return entity.getProtocolMappers(); + } + + @Override + public void setProtocolMappers(Set protocolMappers) { + entity.setProtocolMappers(protocolMappers); + update(); + } + + @Override + public String getNote(String name) { + return entity.getNotes()==null ? null : entity.getNotes().get(name); + } + + @Override + public void setNote(String name, String value) { + if (entity.getNotes() == null) { + entity.setNotes(new HashMap<>()); + } + entity.getNotes().put(name, value); + update(); + } + + @Override + public void removeNote(String name) { + if (entity.getNotes() != null) { + entity.getNotes().remove(name); + update(); + } + } + + @Override + public Map getNotes() { + if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap(); + Map copy = new HashMap<>(); + copy.putAll(entity.getNotes()); + return copy; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java old mode 100755 new mode 100644 similarity index 53% rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java index c67a576250..05a762b54f --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java @@ -17,44 +17,48 @@ package org.keycloak.models.sessions.infinispan; -import org.infinispan.Cache; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; - import java.util.Collections; -import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.infinispan.Cache; +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.sessions.AuthenticationSessionModel; + /** - * @author Stian Thorgersen + * NOTE: Calling setter doesn't automatically enlist for update + * + * @author Marek Posolda */ -public class ClientSessionAdapter implements ClientSessionModel { +public class AuthenticationSessionAdapter implements AuthenticationSessionModel { private KeycloakSession session; - private InfinispanUserSessionProvider provider; - private Cache cache; + private InfinispanAuthenticationSessionProvider provider; + private Cache cache; private RealmModel realm; - private ClientSessionEntity entity; - private boolean offline; + private AuthenticationSessionEntity entity; - public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache cache, RealmModel realm, - ClientSessionEntity entity, boolean offline) { + public AuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, Cache cache, RealmModel realm, + AuthenticationSessionEntity entity) { this.session = session; this.provider = provider; this.cache = cache; this.realm = realm; this.entity = entity; - this.offline = offline; } + void update() { + provider.tx.replace(cache, entity.getId(), entity); + } + + @Override public String getId() { return entity.getId(); @@ -67,36 +71,7 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public ClientModel getClient() { - return realm.getClientById(entity.getClient()); - } - - @Override - public UserSessionAdapter getUserSession() { - return entity.getUserSession() != null ? provider.getUserSession(realm, entity.getUserSession(), offline) : null; - } - - @Override - public void setUserSession(UserSessionModel userSession) { - if (userSession == null) { - if (entity.getUserSession() != null) { - provider.dettachSession(getUserSession(), this); - } - entity.setUserSession(null); - } else { - UserSessionAdapter userSessionAdapter = (UserSessionAdapter) userSession; - if (entity.getUserSession() != null) { - if (entity.getUserSession().equals(userSession.getId())) { - return; - } else { - provider.dettachSession(userSessionAdapter, this); - } - } else { - provider.attachSession(userSessionAdapter, this); - } - - entity.setUserSession(userSession.getId()); - } - update(); + return realm.getClientById(entity.getClientUuid()); } @Override @@ -157,52 +132,104 @@ public class ClientSessionAdapter implements ClientSessionModel { } @Override - public String getAuthMethod() { - return entity.getAuthMethod(); + public String getProtocol() { + return entity.getProtocol(); } @Override - public void setAuthMethod(String authMethod) { - entity.setAuthMethod(authMethod); + public void setProtocol(String protocol) { + entity.setProtocol(protocol); update(); } @Override - public String getNote(String name) { - return entity.getNotes() != null ? entity.getNotes().get(name) : null; + public String getClientNote(String name) { + return (entity.getClientNotes() != null && name != null) ? entity.getClientNotes().get(name) : null; } @Override - public void setNote(String name, String value) { - if (entity.getNotes() == null) { - entity.setNotes(new HashMap()); + public void setClientNote(String name, String value) { + if (entity.getClientNotes() == null) { + entity.setClientNotes(new ConcurrentHashMap<>()); + } + if (name != null) { + if (value == null) { + entity.getClientNotes().remove(name); + } else { + entity.getClientNotes().put(name, value); + } } - entity.getNotes().put(name, value); update(); } @Override - public void removeNote(String name) { - if (entity.getNotes() != null) { - entity.getNotes().remove(name); - update(); + public void removeClientNote(String name) { + if (entity.getClientNotes() != null && name != null) { + entity.getClientNotes().remove(name); } + update(); } @Override - public Map getNotes() { - if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap(); - Map copy = new HashMap<>(); - copy.putAll(entity.getNotes()); + public Map getClientNotes() { + if (entity.getClientNotes() == null || entity.getClientNotes().isEmpty()) return Collections.emptyMap(); + Map copy = new ConcurrentHashMap<>(); + copy.putAll(entity.getClientNotes()); return copy; } + @Override + public void clearClientNotes() { + entity.setClientNotes(new ConcurrentHashMap<>()); + update(); + } + + @Override + public String getAuthNote(String name) { + return (entity.getAuthNotes() != null && name != null) ? entity.getAuthNotes().get(name) : null; + } + + @Override + public void setAuthNote(String name, String value) { + if (entity.getAuthNotes() == null) { + entity.setAuthNotes(new ConcurrentHashMap<>()); + } + if (name != null) { + if (value == null) { + entity.getAuthNotes().remove(name); + } else { + entity.getAuthNotes().put(name, value); + } + } + update(); + } + + @Override + public void removeAuthNote(String name) { + if (entity.getAuthNotes() != null && name != null) { + entity.getAuthNotes().remove(name); + } + update(); + } + + @Override + public void clearAuthNotes() { + entity.setAuthNotes(new ConcurrentHashMap<>()); + update(); + } + @Override public void setUserSessionNote(String name, String value) { if (entity.getUserSessionNotes() == null) { - entity.setUserSessionNotes(new HashMap()); + entity.setUserSessionNotes(new ConcurrentHashMap<>()); + } + if (name != null) { + if (value == null) { + entity.getUserSessionNotes().remove(name); + } else { + entity.getUserSessionNotes().put(name, value); + } } - entity.getUserSessionNotes().put(name, value); update(); } @@ -212,14 +239,14 @@ public class ClientSessionAdapter implements ClientSessionModel { if (entity.getUserSessionNotes() == null) { return Collections.EMPTY_MAP; } - HashMap copy = new HashMap<>(); + ConcurrentHashMap copy = new ConcurrentHashMap<>(); copy.putAll(entity.getUserSessionNotes()); return copy; } @Override public void clearUserSessionNotes() { - entity.setUserSessionNotes(new HashMap()); + entity.setUserSessionNotes(new ConcurrentHashMap<>()); update(); } @@ -248,7 +275,6 @@ public class ClientSessionAdapter implements ClientSessionModel { @Override public void addRequiredAction(UserModel.RequiredAction action) { addRequiredAction(action.name()); - } @Override @@ -256,24 +282,22 @@ public class ClientSessionAdapter implements ClientSessionModel { removeRequiredAction(action.name()); } - void update() { - provider.getTx().replace(cache, entity.getId(), entity); - } @Override - public Map getExecutionStatus() { - return entity.getAuthenticatorStatus(); + public Map getExecutionStatus() { + + return entity.getExecutionStatus(); } @Override - public void setExecutionStatus(String authenticator, ExecutionStatus status) { - entity.getAuthenticatorStatus().put(authenticator, status); + public void setExecutionStatus(String authenticator, AuthenticationSessionModel.ExecutionStatus status) { + entity.getExecutionStatus().put(authenticator, status); update(); } @Override public void clearExecutionStatus() { - entity.getAuthenticatorStatus().clear(); + entity.getExecutionStatus().clear(); update(); } @@ -286,7 +310,22 @@ public class ClientSessionAdapter implements ClientSessionModel { if (user == null) entity.setAuthUserId(null); else entity.setAuthUserId(user.getId()); update(); - } + @Override + public void updateClient(ClientModel client) { + entity.setClientUuid(client.getId()); + update(); + } + + @Override + public void restartSession(RealmModel realm, ClientModel client) { + String id = entity.getId(); + entity = new AuthenticationSessionEntity(); + entity.setId(id); + entity.setRealm(realm.getId()); + entity.setClientUuid(client.getId()); + entity.setTimestamp(Time.currentTime()); + update(); + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java index e55cf3181c..19cb7c7560 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/Consumers.java @@ -19,11 +19,9 @@ package org.keycloak.models.sessions.infinispan; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -41,21 +39,6 @@ public class Consumers { return new UserSessionModelsConsumer(provider, realm, offline); } - public static class UserSessionIdAndTimestampConsumer implements Consumer> { - - private Map sessions = new HashMap<>(); - - @Override - public void accept(Map.Entry entry) { - SessionEntity e = entry.getValue(); - if (e instanceof ClientSessionEntity) { - ClientSessionEntity ce = (ClientSessionEntity) e; - sessions.put(ce.getUserSession(), ce.getTimestamp()); - } - } - - } - public static class UserSessionModelsConsumer implements Consumer> { private InfinispanUserSessionProvider provider; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java new file mode 100644 index 0000000000..127879a4d1 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java @@ -0,0 +1,98 @@ +/* + * 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.models.sessions.infinispan; + +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.models.*; + +import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent; +import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; +import java.util.*; +import org.infinispan.Cache; + +/** + * + * @author hmlnarik + */ +public class InfinispanActionTokenStoreProvider implements ActionTokenStoreProvider { + + private final Cache actionKeyCache; + private final InfinispanKeycloakTransaction tx; + private final KeycloakSession session; + + public InfinispanActionTokenStoreProvider(KeycloakSession session, Cache actionKeyCache) { + this.session = session; + this.actionKeyCache = actionKeyCache; + this.tx = new InfinispanKeycloakTransaction(); + + session.getTransactionManager().enlistAfterCompletion(tx); + } + + @Override + public void close() { + } + + @Override + public void put(ActionTokenKeyModel key, Map notes) { + if (key == null || key.getUserId() == null || key.getActionId() == null) { + return; + } + + ActionTokenReducedKey tokenKey = new ActionTokenReducedKey(key.getUserId(), key.getActionId(), key.getActionVerificationNonce()); + ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes); + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new AddInvalidatedActionTokenEvent(tokenKey, key.getExpiration(), tokenValue), false); + } + + @Override + public ActionTokenValueModel get(ActionTokenKeyModel actionTokenKey) { + if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) { + return null; + } + + ActionTokenReducedKey key = new ActionTokenReducedKey(actionTokenKey.getUserId(), actionTokenKey.getActionId(), actionTokenKey.getActionVerificationNonce()); + return this.actionKeyCache.getAdvancedCache().get(key); + } + + @Override + public ActionTokenValueModel remove(ActionTokenKeyModel actionTokenKey) { + if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null) { + return null; + } + + ActionTokenReducedKey key = new ActionTokenReducedKey(actionTokenKey.getUserId(), actionTokenKey.getActionId(), actionTokenKey.getActionVerificationNonce()); + ActionTokenValueEntity value = this.actionKeyCache.get(key); + + if (value != null) { + this.tx.remove(actionKeyCache, key); + } + + return value; + } + + public void removeAll(String userId, String actionId) { + if (userId == null || actionId == null) { + return; + } + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + this.tx.notify(cluster, InfinispanActionTokenStoreProviderFactory.ACTION_TOKEN_EVENTS, new RemoveActionTokensSpecificEvent(userId, actionId), false); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java new file mode 100644 index 0000000000..a8c5e3899e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java @@ -0,0 +1,100 @@ +/* + * 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.models.sessions.infinispan; + +import org.keycloak.Config; +import org.keycloak.Config.Scope; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.common.util.Time; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.*; + +import org.keycloak.models.cache.infinispan.AddInvalidatedActionTokenEvent; +import org.keycloak.models.cache.infinispan.RemoveActionTokensSpecificEvent; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity; +import org.keycloak.models.sessions.infinispan.entities.ActionTokenReducedKey; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.infinispan.Cache; +import org.infinispan.context.Flag; + +/** + * + * @author hmlnarik + */ +public class InfinispanActionTokenStoreProviderFactory implements ActionTokenStoreProviderFactory { + + public static final String ACTION_TOKEN_EVENTS = "ACTION_TOKEN_EVENTS"; + + /** + * If expiration is set to this value, no expiration is set on the corresponding cache entry (hence cache default is honored) + */ + private static final int DEFAULT_CACHE_EXPIRATION = 0; + + private Config.Scope config; + + @Override + public ActionTokenStoreProvider create(KeycloakSession session) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + Cache actionTokenCache = connections.getCache(InfinispanConnectionProvider.ACTION_TOKEN_CACHE); + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + + cluster.registerListener(ACTION_TOKEN_EVENTS, event -> { + if (event instanceof RemoveActionTokensSpecificEvent) { + RemoveActionTokensSpecificEvent e = (RemoveActionTokensSpecificEvent) event; + + actionTokenCache + .getAdvancedCache() + .withFlags(Flag.CACHE_MODE_LOCAL, Flag.SKIP_CACHE_LOAD) + .keySet() + .stream() + .filter(k -> Objects.equals(k.getUserId(), e.getUserId()) && Objects.equals(k.getActionId(), e.getActionId())) + .forEach(actionTokenCache::remove); + } else if (event instanceof AddInvalidatedActionTokenEvent) { + AddInvalidatedActionTokenEvent e = (AddInvalidatedActionTokenEvent) event; + + if (e.getExpirationInSecs() == DEFAULT_CACHE_EXPIRATION) { + actionTokenCache.put(e.getKey(), e.getTokenValue()); + } else { + actionTokenCache.put(e.getKey(), e.getTokenValue(), e.getExpirationInSecs() - Time.currentTime(), TimeUnit.SECONDS); + } + } + }); + + return new InfinispanActionTokenStoreProvider(session, actionTokenCache); + } + + @Override + public void init(Scope config) { + this.config = config; + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "infinispan"; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java new file mode 100644 index 0000000000..5991f98944 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright 2016 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.models.sessions.infinispan; + +import org.keycloak.cluster.ClusterProvider; +import java.util.Iterator; +import java.util.Map; + +import org.infinispan.Cache; +import org.infinispan.context.Flag; +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; +import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.models.utils.RealmInfoUtil; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.AuthenticationSessionProvider; + +/** + * @author Marek Posolda + */ +public class InfinispanAuthenticationSessionProvider implements AuthenticationSessionProvider { + + private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProvider.class); + + private final KeycloakSession session; + private final Cache cache; + protected final InfinispanKeycloakTransaction tx; + + public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache cache) { + this.session = session; + this.cache = cache; + + this.tx = new InfinispanKeycloakTransaction(); + session.getTransactionManager().enlistAfterCompletion(tx); + } + + @Override + public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client) { + String id = KeycloakModelUtils.generateId(); + return createAuthenticationSession(id, realm, client); + } + + @Override + public AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client) { + AuthenticationSessionEntity entity = new AuthenticationSessionEntity(); + entity.setId(id); + entity.setRealm(realm.getId()); + entity.setTimestamp(Time.currentTime()); + entity.setClientUuid(client.getId()); + + tx.put(cache, id, entity); + + AuthenticationSessionAdapter wrap = wrap(realm, entity); + return wrap; + } + + private AuthenticationSessionAdapter wrap(RealmModel realm, AuthenticationSessionEntity entity) { + return entity==null ? null : new AuthenticationSessionAdapter(session, this, cache, realm, entity); + } + + @Override + public AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId) { + AuthenticationSessionEntity entity = getAuthenticationSessionEntity(realm, authenticationSessionId); + return wrap(realm, entity); + } + + private AuthenticationSessionEntity getAuthenticationSessionEntity(RealmModel realm, String authSessionId) { + // Chance created in this transaction + AuthenticationSessionEntity entity = tx.get(cache, authSessionId); + + if (entity == null) { + entity = cache.get(authSessionId); + } + + return entity; + } + + @Override + public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession) { + tx.remove(cache, authenticationSession.getId()); + } + + @Override + public void removeExpired(RealmModel realm) { + log.debugf("Removing expired sessions"); + + int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); + + + // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) + Iterator> itr = cache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) + .entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)).iterator(); + + int counter = 0; + while (itr.hasNext()) { + counter++; + AuthenticationSessionEntity entity = itr.next().getValue(); + tx.remove(cache, entity.getId()); + } + + log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName()); + } + + // TODO: Should likely listen to "RealmRemovedEvent" received from cluster and clean just local sessions + @Override + public void onRealmRemoved(RealmModel realm) { + Iterator> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId())).iterator(); + while (itr.hasNext()) { + cache.remove(itr.next().getKey()); + } + } + + // TODO: Should likely listen to "ClientRemovedEvent" received from cluster and clean just local sessions + @Override + public void onClientRemoved(RealmModel realm, ClientModel client) { + Iterator> itr = cache.entrySet().stream().filter(AuthenticationSessionPredicate.create(realm.getId()).client(client.getId())).iterator(); + while (itr.hasNext()) { + cache.remove(itr.next().getKey()); + } + } + + @Override + public void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment) { + if (authSessionId == null) { + return; + } + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + cluster.notify( + InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS, + AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment), + true + ); + } + + @Override + public void close() { + + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java new file mode 100644 index 0000000000..83e970ddaa --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java @@ -0,0 +1,113 @@ +/* + * Copyright 2016 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.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.keycloak.Config; +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.cluster.ClusterProvider; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; +import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.sessions.AuthenticationSessionProvider; +import org.keycloak.sessions.AuthenticationSessionProviderFactory; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import org.jboss.logging.Logger; + +/** + * @author Marek Posolda + */ +public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory { + + private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class); + + private volatile Cache authSessionsCache; + + public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS"; + + @Override + public void init(Config.Scope config) { + + } + + @Override + public AuthenticationSessionProvider create(KeycloakSession session) { + lazyInit(session); + return new InfinispanAuthenticationSessionProvider(session, authSessionsCache); + } + + private void updateAuthNotes(ClusterEvent clEvent) { + if (! (clEvent instanceof AuthenticationSessionAuthNoteUpdateEvent)) { + return; + } + + AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent; + AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId()); + updateAuthSession(authSession, event.getAuthNotesFragment()); + } + + private static void updateAuthSession(AuthenticationSessionEntity authSession, Map authNotesFragment) { + if (authSession != null) { + if (authSession.getAuthNotes() == null) { + authSession.setAuthNotes(new ConcurrentHashMap<>()); + } + + for (Entry me : authNotesFragment.entrySet()) { + String value = me.getValue(); + if (value == null) { + authSession.getAuthNotes().remove(me.getKey()); + } else { + authSession.getAuthNotes().put(me.getKey(), value); + } + } + } + } + + private void lazyInit(KeycloakSession session) { + if (authSessionsCache == null) { + synchronized (this) { + if (authSessionsCache == null) { + InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class); + authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME); + + ClusterProvider cluster = session.getProvider(ClusterProvider.class); + cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes); + + log.debug("Registered cluster listeners"); + } + } + } + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return "infinispan"; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java new file mode 100644 index 0000000000..5471184da9 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java @@ -0,0 +1,218 @@ +/* + * 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.models.sessions.infinispan; + +import org.keycloak.cluster.ClusterEvent; +import org.keycloak.cluster.ClusterProvider; +import org.infinispan.context.Flag; +import org.keycloak.models.KeycloakTransaction; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.infinispan.Cache; +import org.jboss.logging.Logger; + +/** + * @author Stian Thorgersen + */ +public class InfinispanKeycloakTransaction implements KeycloakTransaction { + + private final static Logger log = Logger.getLogger(InfinispanKeycloakTransaction.class); + + public enum CacheOperation { + ADD, ADD_WITH_LIFESPAN, REMOVE, REPLACE, ADD_IF_ABSENT // ADD_IF_ABSENT throws an exception if there is existing value + } + + private boolean active; + private boolean rollback; + private final Map tasks = new LinkedHashMap<>(); + + @Override + public void begin() { + active = true; + } + + @Override + public void commit() { + if (rollback) { + throw new RuntimeException("Rollback only!"); + } + + tasks.values().forEach(CacheTask::execute); + } + + @Override + public void rollback() { + tasks.clear(); + } + + @Override + public void setRollbackOnly() { + rollback = true; + } + + @Override + public boolean getRollbackOnly() { + return rollback; + } + + @Override + public boolean isActive() { + return active; + } + + public void put(Cache cache, K key, V value) { + log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD, key); + + Object taskKey = getTaskKey(cache, key); + if (tasks.containsKey(taskKey)) { + throw new IllegalStateException("Can't add session: task in progress for session"); + } else { + tasks.put(taskKey, new CacheTaskWithValue(value) { + @Override + public void execute() { + decorateCache(cache).put(key, value); + } + }); + } + } + + public void put(Cache cache, K key, V value, long lifespan, TimeUnit lifespanUnit) { + log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD_WITH_LIFESPAN, key); + + Object taskKey = getTaskKey(cache, key); + if (tasks.containsKey(taskKey)) { + throw new IllegalStateException("Can't add session: task in progress for session"); + } else { + tasks.put(taskKey, new CacheTaskWithValue(value) { + @Override + public void execute() { + decorateCache(cache).put(key, value, lifespan, lifespanUnit); + } + }); + } + } + + public void putIfAbsent(Cache cache, K key, V value) { + log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD_IF_ABSENT, key); + + Object taskKey = getTaskKey(cache, key); + if (tasks.containsKey(taskKey)) { + throw new IllegalStateException("Can't add session: task in progress for session"); + } else { + tasks.put(taskKey, new CacheTaskWithValue(value) { + @Override + public void execute() { + V existing = cache.putIfAbsent(key, value); + if (existing != null) { + throw new IllegalStateException("There is already existing value in cache for key " + key); + } + } + }); + } + } + + public void replace(Cache cache, K key, V value) { + log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key); + + Object taskKey = getTaskKey(cache, key); + CacheTask current = tasks.get(taskKey); + if (current != null) { + if (current instanceof CacheTaskWithValue) { + ((CacheTaskWithValue) current).setValue(value); + } + } else { + tasks.put(taskKey, new CacheTaskWithValue(value) { + @Override + public void execute() { + decorateCache(cache).replace(key, value); + } + }); + } + } + + public void notify(ClusterProvider clusterProvider, String taskKey, ClusterEvent event, boolean ignoreSender) { + log.tracev("Adding cache operation SEND_EVENT: {0}", event); + + String theTaskKey = taskKey; + int i = 1; + while (tasks.containsKey(theTaskKey)) { + theTaskKey = taskKey + "-" + (i++); + } + + tasks.put(taskKey, () -> clusterProvider.notify(taskKey, event, ignoreSender)); + } + + public void remove(Cache cache, K key) { + log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key); + + Object taskKey = getTaskKey(cache, key); + tasks.put(taskKey, () -> decorateCache(cache).remove(key)); + } + + // This is for possibility to lookup for session by id, which was created in this transaction + public V get(Cache cache, K key) { + Object taskKey = getTaskKey(cache, key); + CacheTask current = tasks.get(taskKey); + if (current != null) { + if (current instanceof CacheTaskWithValue) { + return ((CacheTaskWithValue) current).getValue(); + } + return null; + } + + // Should we have per-transaction cache for lookups? + return cache.get(key); + } + + private static Object getTaskKey(Cache cache, K key) { + if (key instanceof String) { + return new StringBuilder(cache.getName()) + .append("::") + .append(key).toString(); + } else { + return key; + } + } + + public interface CacheTask { + void execute(); + } + + public abstract class CacheTaskWithValue implements CacheTask { + protected V value; + + public CacheTaskWithValue(V value) { + this.value = value; + } + + public V getValue() { + return value; + } + + public void setValue(V value) { + this.value = value; + } + } + + // Ignore return values. Should have better performance within cluster / cross-dc env + private static Cache decorateCache(Cache cache) { + return cache.getAdvancedCache() + .withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_REMOTE_LOOKUP); + } +} \ No newline at end of file diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java new file mode 100644 index 0000000000..0aca09f81c --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016 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.models.sessions.infinispan; + +import org.infinispan.Cache; +import org.infinispan.distribution.DistributionManager; +import org.infinispan.remoting.transport.Address; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.sessions.StickySessionEncoderProvider; + +/** + * @author Marek Posolda + */ +public class InfinispanStickySessionEncoderProvider implements StickySessionEncoderProvider { + + private final KeycloakSession session; + private final String myNodeName; + + public InfinispanStickySessionEncoderProvider(KeycloakSession session, String myNodeName) { + this.session = session; + this.myNodeName = myNodeName; + } + + @Override + public String encodeSessionId(String sessionId) { + String nodeName = getNodeName(sessionId); + if (nodeName != null) { + return sessionId + '.' + nodeName; + } else { + return sessionId; + } + } + + @Override + public String decodeSessionId(String encodedSessionId) { + int index = encodedSessionId.indexOf('.'); + return index == -1 ? encodedSessionId : encodedSessionId.substring(0, index); + } + + @Override + public void close() { + + } + + + private String getNodeName(String sessionId) { + InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class); + Cache cache = ispnProvider.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME); + DistributionManager distManager = cache.getAdvancedCache().getDistributionManager(); + + if (distManager != null) { + // Sticky session to the node, who owns this authenticationSession + Address address = distManager.getPrimaryLocation(sessionId); + return address.toString(); + } else { + // Fallback to jbossNodeName if authSession cache is local + return myNodeName; + } + } + + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java new file mode 100644 index 0000000000..b8e6a7131d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanStickySessionEncoderProviderFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 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.models.sessions.infinispan; + +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.sessions.StickySessionEncoderProvider; +import org.keycloak.sessions.StickySessionEncoderProviderFactory; + +/** + * @author Marek Posolda + */ +public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory { + + private String myNodeName; + + @Override + public StickySessionEncoderProvider create(KeycloakSession session) { + return new InfinispanStickySessionEncoderProvider(session, myNodeName); + } + + @Override + public void init(Config.Scope config) { + myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "infinispan"; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index a7c8c31ec4..0e50a73d3d 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -23,10 +23,9 @@ import org.infinispan.context.Flag; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; import org.keycloak.models.ClientInitialAccessModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; @@ -34,31 +33,29 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import org.keycloak.models.sessions.infinispan.stream.ClientInitialAccessPredicate; -import org.keycloak.models.sessions.infinispan.stream.ClientSessionPredicate; import org.keycloak.models.sessions.infinispan.stream.Comparators; import org.keycloak.models.sessions.infinispan.stream.Mappers; import org.keycloak.models.sessions.infinispan.stream.SessionPredicate; import org.keycloak.models.sessions.infinispan.stream.UserLoginFailurePredicate; import org.keycloak.models.sessions.infinispan.stream.UserSessionPredicate; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.models.utils.RealmInfoUtil; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -90,28 +87,27 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) { - String id = KeycloakModelUtils.generateId(); + public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); - ClientSessionEntity entity = new ClientSessionEntity(); - entity.setId(id); - entity.setRealm(realm.getId()); - entity.setTimestamp(Time.currentTime()); - entity.setClient(client.getId()); - - - tx.put(sessionCache, id, entity); - - ClientSessionAdapter wrap = wrap(realm, entity, false); - return wrap; + AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(entity, client, (UserSessionAdapter) userSession, this, sessionCache); + adapter.setUserSession(userSession); + return adapter; } @Override - public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { - String id = KeycloakModelUtils.generateId(); - + public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { UserSessionEntity entity = new UserSessionEntity(); entity.setId(id); + + updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); + + tx.putIfAbsent(sessionCache, id, entity); + + return wrap(realm, entity, false); + } + + void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { entity.setRealm(realm.getId()); entity.setUser(user.getId()); entity.setLoginUsername(loginUsername); @@ -126,42 +122,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setStarted(currentTime); entity.setLastSessionRefresh(currentTime); - tx.put(sessionCache, id, entity); - return wrap(realm, entity, false); - } - - @Override - public ClientSessionModel getClientSession(RealmModel realm, String id) { - return getClientSession(realm, id, false); - } - - protected ClientSessionModel getClientSession(RealmModel realm, String id, boolean offline) { - Cache cache = getCache(offline); - ClientSessionEntity entity = (ClientSessionEntity) cache.get(id); - - // Chance created in this transaction - if (entity == null) { - entity = (ClientSessionEntity) tx.get(cache, id); - } - - return wrap(realm, entity, offline); - } - - @Override - public ClientSessionModel getClientSession(String id) { - ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id); - - // Chance created in this transaction - if (entity == null) { - entity = (ClientSessionEntity) tx.get(sessionCache, id); - } - - if (entity != null) { - RealmModel realm = session.realms().getRealm(entity.getRealm()); - return wrap(realm, entity, false); - } - return null; } @Override @@ -171,11 +132,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { Cache cache = getCache(offline); - UserSessionEntity entity = (UserSessionEntity) cache.get(id); + UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction - // Chance created in this transaction if (entity == null) { - entity = (UserSessionEntity) tx.get(cache, id); + entity = (UserSessionEntity) cache.get(id); } return wrap(realm, entity, offline); @@ -221,37 +181,50 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { protected List getUserSessions(final RealmModel realm, ClientModel client, int firstResult, int maxResults, final boolean offline) { final Cache cache = getCache(offline); - Iterator itr = cache.entrySet().stream() - .filter(ClientSessionPredicate.create(realm.getId()).client(client.getId()).requireUserSession()) - .map(Mappers.clientSessionToUserSessionTimestamp()) - .iterator(); + Stream stream = cache.entrySet().stream() + .filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) + .map(Mappers.userSessionEntity()) + .sorted(Comparators.userSessionLastSessionRefresh()); - Map m = new HashMap<>(); - while(itr.hasNext()) { - UserSessionTimestamp next = itr.next(); - if (!m.containsKey(next.getUserSessionId()) || m.get(next.getUserSessionId()).getClientSessionTimestamp() < next.getClientSessionTimestamp()) { - m.put(next.getUserSessionId(), next); - } + // Doesn't work due to ISPN-6575 . TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 +// if (firstResult > 0) { +// stream = stream.skip(firstResult); +// } +// +// if (maxResults > 0) { +// stream = stream.limit(maxResults); +// } +// +// List entities = stream.collect(Collectors.toList()); + + + // Workaround for ISPN-6575 TODO Fix once infinispan upgraded to 8.2.2.Final or 9.0 and replace with the more effective code above + if (firstResult < 0) { + firstResult = 0; + } + if (maxResults < 0) { + maxResults = Integer.MAX_VALUE; } - Stream stream = new LinkedList<>(m.values()).stream().sorted(Comparators.userSessionTimestamp()); + int count = firstResult + maxResults; + if (count > 0) { + stream = stream.limit(count); + } + List entities = stream.collect(Collectors.toList()); - if (firstResult > 0) { - stream = stream.skip(firstResult); + if (firstResult > entities.size()) { + return Collections.emptyList(); } - if (maxResults > 0) { - stream = stream.limit(maxResults); - } + maxResults = Math.min(maxResults, entities.size() - firstResult); + entities = entities.subList(firstResult, firstResult + maxResults); + final List sessions = new LinkedList<>(); - stream.forEach(new Consumer() { + entities.stream().forEach(new Consumer() { @Override - public void accept(UserSessionTimestamp userSessionTimestamp) { - SessionEntity entity = cache.get(userSessionTimestamp.getUserSessionId()); - if (entity != null) { - sessions.add(wrap(realm, (UserSessionEntity) entity, offline)); - } + public void accept(UserSessionEntity userSessionEntity) { + sessions.add(wrap(realm, userSessionEntity, offline)); } }); @@ -264,7 +237,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) { - return getCache(offline).entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).client(client.getId()).requireUserSession()).map(Mappers.clientSessionToUserSessionId()).distinct().count(); + return getCache(offline).entrySet().stream() + .filter(UserSessionPredicate.create(realm.getId()).client(client.getId())) + .count(); } @Override @@ -294,9 +269,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void removeExpired(RealmModel realm) { log.debugf("Removing expired sessions"); removeExpiredUserSessions(realm); - removeExpiredClientSessions(realm); removeExpiredOfflineUserSessions(realm); - removeExpiredOfflineClientSessions(realm); removeExpiredClientInitialAccess(realm); } @@ -313,33 +286,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { counter++; UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); tx.remove(sessionCache, entity.getId()); - - if (entity.getClientSessions() != null) { - for (String clientSessionId : entity.getClientSessions()) { - tx.remove(sessionCache, clientSessionId); - } - } } log.debugf("Removed %d expired user sessions for realm '%s'", counter, realm.getName()); } - private void removeExpiredClientSessions(RealmModel realm) { - int expiredDettachedClientSession = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm); - - // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) - Iterator> itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) - .entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredDettachedClientSession).requireNullUserSession()).iterator(); - - int counter = 0; - while (itr.hasNext()) { - counter++; - tx.remove(sessionCache, itr.next().getKey()); - } - - log.debugf("Removed %d expired client sessions for realm '%s'", counter, realm.getName()); - } - private void removeExpiredOfflineUserSessions(RealmModel realm) { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); @@ -357,33 +308,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { persister.removeUserSession(entity.getId(), true); - for (String clientSessionId : entity.getClientSessions()) { - tx.remove(offlineSessionCache, clientSessionId); + for (String clientUUID : entity.getAuthenticatedClientSessions().keySet()) { + persister.removeClientSession(entity.getId(), clientUUID, true); } } log.debugf("Removed %d expired offline user sessions for realm '%s'", counter, realm.getName()); } - private void removeExpiredOfflineClientSessions(RealmModel realm) { - UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - int expiredOffline = Time.currentTime() - realm.getOfflineSessionIdleTimeout(); - - // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) - Iterator itr = offlineSessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) - .entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).expiredRefresh(expiredOffline)).map(Mappers.sessionId()).iterator(); - - int counter = 0; - while (itr.hasNext()) { - counter++; - String sessionId = itr.next(); - tx.remove(offlineSessionCache, sessionId); - persister.removeClientSession(sessionId, true); - } - - log.debugf("Removed %d expired offline client sessions for realm '%s'", counter, realm.getName()); - } - private void removeExpiredClientInitialAccess(RealmModel realm) { Iterator itr = sessionCache.getAdvancedCache().withFlags(Flag.CACHE_MODE_LOCAL) .entrySet().stream().filter(ClientInitialAccessPredicate.create(realm.getId()).expired(Time.currentTime())).map(Mappers.sessionId()).iterator(); @@ -445,21 +377,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - onClientRemoved(realm, client, true); - onClientRemoved(realm, client, false); - } - - private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) { - Cache cache = getCache(offline); - - Iterator> itr = cache.entrySet().stream().filter(ClientSessionPredicate.create(realm.getId()).client(client.getId())).iterator(); - while (itr.hasNext()) { - ClientSessionEntity entity = (ClientSessionEntity) itr.next().getValue(); - ClientSessionAdapter adapter = wrap(realm, entity, offline); - adapter.setUserSession(null); - - tx.remove(cache, entity.getId()); - } + // Nothing for now. userSession.getAuthenticatedClientSessions() will check lazily if particular client exists and update userSession on-the-fly. } @@ -475,55 +393,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { public void close() { } - void attachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) { - UserSessionEntity entity = userSession.getEntity(); - String clientSessionId = clientSession.getId(); - if (!entity.getClientSessions().contains(clientSessionId)) { - entity.getClientSessions().add(clientSessionId); - userSession.update(); - } - } - - @Override - public void removeClientSession(RealmModel realm, ClientSessionModel clientSession) { - removeClientSession(realm, clientSession, false); - } - - protected void removeClientSession(RealmModel realm, ClientSessionModel clientSession, boolean offline) { - Cache cache = getCache(offline); - - UserSessionModel userSession = clientSession.getUserSession(); - if (userSession != null) { - UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity(); - if (entity.getClientSessions() != null) { - entity.getClientSessions().remove(clientSession.getId()); - - } - tx.replace(cache, entity.getId(), entity); - } - tx.remove(cache, clientSession.getId()); - } - - - void dettachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) { - UserSessionEntity entity = userSession.getEntity(); - String clientSessionId = clientSession.getId(); - if (entity.getClientSessions() != null && entity.getClientSessions().contains(clientSessionId)) { - entity.getClientSessions().remove(clientSessionId); - userSession.update(); - } - } - protected void removeUserSession(RealmModel realm, UserSessionEntity sessionEntity, boolean offline) { Cache cache = getCache(offline); tx.remove(cache, sessionEntity.getId()); - - if (sessionEntity.getClientSessions() != null) { - for (String clientSessionId : sessionEntity.getClientSessions()) { - tx.remove(cache, clientSessionId); - } - } } InfinispanKeycloakTransaction getTx() { @@ -551,11 +424,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return models; } - ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) { - Cache cache = getCache(offline); - return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null; - } - ClientInitialAccessAdapter wrap(RealmModel realm, ClientInitialAccessEntity entity) { Cache cache = getCache(false); return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null; @@ -565,14 +433,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null; } - List wrapClientSessions(RealmModel realm, Collection entities, boolean offline) { - List models = new LinkedList<>(); - for (ClientSessionEntity e : entities) { - models.add(wrap(realm, e, offline)); - } - return models; - } - UserSessionEntity getUserSessionEntity(UserSessionModel userSession, boolean offline) { if (userSession instanceof UserSessionAdapter) { return ((UserSessionAdapter) userSession).getEntity(); @@ -585,7 +445,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { - UserSessionAdapter offlineUserSession = importUserSession(userSession, true); + UserSessionAdapter offlineUserSession = importUserSession(userSession, true, false); // started and lastSessionRefresh set to current time int currentTime = Time.currentTime(); @@ -596,7 +456,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) { + public UserSessionAdapter getOfflineUserSession(RealmModel realm, String userSessionId) { return getUserSession(realm, userSessionId, true); } @@ -608,9 +468,14 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } } + + @Override - public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) { - ClientSessionAdapter offlineClientSession = importClientSession(clientSession, true); + public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) { + UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : + getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); + + AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession); // update timestamp to current time offlineClientSession.setTimestamp(Time.currentTime()); @@ -619,38 +484,17 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId) { - return getClientSession(realm, clientSessionId, true); - } - - @Override - public List getOfflineClientSessions(RealmModel realm, UserModel user) { + public List getOfflineUserSessions(RealmModel realm, UserModel user) { Iterator> itr = offlineSessionCache.entrySet().stream().filter(UserSessionPredicate.create(realm.getId()).user(user.getId())).iterator(); - List clientSessions = new LinkedList<>(); + List userSessions = new LinkedList<>(); while(itr.hasNext()) { UserSessionEntity entity = (UserSessionEntity) itr.next().getValue(); - Set currClientSessions = entity.getClientSessions(); - - if (currClientSessions == null) { - continue; - } - - for (String clientSessionId : currClientSessions) { - ClientSessionEntity cls = (ClientSessionEntity) offlineSessionCache.get(clientSessionId); - if (cls != null) { - clientSessions.add(wrap(realm, cls, true)); - } - } + UserSessionModel userSession = wrap(realm, entity, true); + userSessions.add(userSession); } - return clientSessions; - } - - @Override - public void removeOfflineClientSession(RealmModel realm, String clientSessionId) { - ClientSessionModel clientSession = getOfflineClientSession(realm, clientSessionId); - removeClientSession(realm, clientSession, true); + return userSessions; } @Override @@ -664,7 +508,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { } @Override - public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) { + public UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline, boolean importAuthenticatedClientSessions) { UserSessionEntity entity = new UserSessionEntity(); entity.setId(userSession.getId()); entity.setRealm(userSession.getRealm().getId()); @@ -682,34 +526,45 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { entity.setStarted(userSession.getStarted()); entity.setLastSessionRefresh(userSession.getLastSessionRefresh()); + Cache cache = getCache(offline); tx.put(cache, userSession.getId(), entity); - return wrap(userSession.getRealm(), entity, offline); + UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline); + + // Handle client sessions + if (importAuthenticatedClientSessions) { + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { + importClientSession(importedSession, clientSession); + } + } + + return importedSession; } - @Override - public ClientSessionAdapter importClientSession(ClientSessionModel clientSession, boolean offline) { - ClientSessionEntity entity = new ClientSessionEntity(); - entity.setId(clientSession.getId()); - entity.setRealm(clientSession.getRealm().getId()); + + private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter importedUserSession, AuthenticatedClientSessionModel clientSession) { + AuthenticatedClientSessionEntity entity = new AuthenticatedClientSessionEntity(); entity.setAction(clientSession.getAction()); - entity.setAuthenticatorStatus(clientSession.getExecutionStatus()); - entity.setAuthMethod(clientSession.getAuthMethod()); - if (clientSession.getAuthenticatedUser() != null) { - entity.setAuthUserId(clientSession.getAuthenticatedUser().getId()); - } - entity.setClient(clientSession.getClient().getId()); + entity.setAuthMethod(clientSession.getProtocol()); + entity.setNotes(clientSession.getNotes()); entity.setProtocolMappers(clientSession.getProtocolMappers()); entity.setRedirectUri(clientSession.getRedirectUri()); entity.setRoles(clientSession.getRoles()); entity.setTimestamp(clientSession.getTimestamp()); - entity.setUserSessionNotes(clientSession.getUserSessionNotes()); - Cache cache = getCache(offline); - tx.put(cache, clientSession.getId(), entity); - return wrap(clientSession.getRealm(), entity, offline); + Map clientSessions = importedUserSession.getEntity().getAuthenticatedClientSessions(); + if (clientSessions == null) { + clientSessions = new HashMap<>(); + importedUserSession.getEntity().setAuthenticatedClientSessions(clientSessions); + } + + clientSessions.put(clientSession.getClient().getId(), entity); + + importedUserSession.update(); + + return new AuthenticatedClientSessionAdapter(entity, clientSession.getClient(), importedUserSession, this, importedUserSession.getCache()); } @Override @@ -732,11 +587,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { @Override public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) { Cache cache = getCache(false); - ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id); + ClientInitialAccessEntity entity = (ClientInitialAccessEntity) tx.get(cache, id); // Chance created in this transaction - // If created in this transaction if (entity == null) { - entity = (ClientInitialAccessEntity) tx.get(cache, id); + entity = (ClientInitialAccessEntity) cache.get(id); } return wrap(realm, entity); @@ -757,145 +611,4 @@ public class InfinispanUserSessionProvider implements UserSessionProvider { return list; } - - class InfinispanKeycloakTransaction implements KeycloakTransaction { - - private boolean active; - private boolean rollback; - private Map tasks = new HashMap<>(); - - @Override - public void begin() { - active = true; - } - - @Override - public void commit() { - if (rollback) { - throw new RuntimeException("Rollback only!"); - } - - for (CacheTask task : tasks.values()) { - task.execute(); - } - } - - @Override - public void rollback() { - tasks.clear(); - } - - @Override - public void setRollbackOnly() { - rollback = true; - } - - @Override - public boolean getRollbackOnly() { - return rollback; - } - - @Override - public boolean isActive() { - return active; - } - - public void put(Cache cache, Object key, Object value) { - log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD, key); - - Object taskKey = getTaskKey(cache, key); - if (tasks.containsKey(taskKey)) { - throw new IllegalStateException("Can't add session: task in progress for session"); - } else { - tasks.put(taskKey, new CacheTask(cache, CacheOperation.ADD, key, value)); - } - } - - public void replace(Cache cache, Object key, Object value) { - log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key); - - Object taskKey = getTaskKey(cache, key); - CacheTask current = tasks.get(taskKey); - if (current != null) { - switch (current.operation) { - case ADD: - case REPLACE: - current.value = value; - return; - case REMOVE: - return; - } - } else { - tasks.put(taskKey, new CacheTask(cache, CacheOperation.REPLACE, key, value)); - } - } - - public void remove(Cache cache, Object key) { - log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key); - - Object taskKey = getTaskKey(cache, key); - tasks.put(taskKey, new CacheTask(cache, CacheOperation.REMOVE, key, null)); - } - - // This is for possibility to lookup for session by id, which was created in this transaction - public Object get(Cache cache, Object key) { - Object taskKey = getTaskKey(cache, key); - CacheTask current = tasks.get(taskKey); - if (current != null) { - switch (current.operation) { - case ADD: - case REPLACE: - return current.value; } - } - - return null; - } - - private Object getTaskKey(Cache cache, Object key) { - if (key instanceof String) { - return new StringBuilder(cache.getName()) - .append("::") - .append(key.toString()).toString(); - } else { - // loginFailure cache - return key; - } - } - - public class CacheTask { - private Cache cache; - private CacheOperation operation; - private Object key; - private Object value; - - public CacheTask(Cache cache, CacheOperation operation, Object key, Object value) { - this.cache = cache; - this.operation = operation; - this.key = key; - this.value = value; - } - - public void execute() { - log.tracev("Executing cache operation: {0} on {1}", operation, key); - - switch (operation) { - case ADD: - cache.put(key, value); - break; - case REMOVE: - cache.remove(key); - break; - case REPLACE: - cache.replace(key, value); - break; - } - } - } - - } - - public enum CacheOperation { - ADD, REMOVE, REPLACE - } - } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java index bf1e6fd391..8ab15f7a0e 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java @@ -18,11 +18,13 @@ package org.keycloak.models.sessions.infinispan; import org.infinispan.Cache; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.sessions.infinispan.entities.AuthenticatedClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; @@ -60,6 +62,36 @@ public class UserSessionAdapter implements UserSessionModel { this.offline = offline; } + @Override + public Map getAuthenticatedClientSessions() { + Map clientSessionEntities = entity.getAuthenticatedClientSessions(); + Map result = new HashMap<>(); + + List removedClientUUIDS = new LinkedList<>(); + + if (clientSessionEntities != null) { + clientSessionEntities.forEach((String key, AuthenticatedClientSessionEntity value) -> { + // Check if client still exists + ClientModel client = realm.getClientById(key); + if (client != null) { + result.put(key, new AuthenticatedClientSessionAdapter(value, client, this, provider, cache)); + } else { + removedClientUUIDS.add(key); + } + }); + } + + // Update user session + if (!removedClientUUIDS.isEmpty()) { + for (String clientUUID : removedClientUUIDS) { + entity.getAuthenticatedClientSessions().remove(clientUUID); + } + update(); + } + + return Collections.unmodifiableMap(result); + } + public String getId() { return entity.getId(); } @@ -82,6 +114,12 @@ public class UserSessionAdapter implements UserSessionModel { return session.users().getUserById(entity.getUser(), realm); } + @Override + public void setUser(UserModel user) { + entity.setUser(user.getId()); + update(); + } + @Override public String getLoginUsername() { return entity.getLoginUsername(); @@ -159,19 +197,14 @@ public class UserSessionAdapter implements UserSessionModel { } @Override - public List getClientSessions() { - if (entity.getClientSessions() != null) { - List clientSessions = new LinkedList<>(); - for (String c : entity.getClientSessions()) { - ClientSessionModel clientSession = provider.getClientSession(realm, c, offline); - if (clientSession != null) { - clientSessions.add(clientSession); - } - } - return clientSessions; - } else { - return Collections.emptyList(); - } + public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + provider.updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); + + entity.setState(null); + entity.setNotes(null); + entity.setAuthenticatedClientSessions(null); + + update(); } @Override @@ -196,4 +229,7 @@ public class UserSessionAdapter implements UserSessionModel { provider.getTx().replace(cache, entity.getId(), entity); } + Cache getCache() { + return cache; + } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java new file mode 100644 index 0000000000..173c43497d --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java @@ -0,0 +1,104 @@ +/* + * 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.models.sessions.infinispan.entities; + +import java.io.*; +import java.util.Objects; +import java.util.UUID; +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.SerializeWith; + +/** + * + * @author hmlnarik + */ +@SerializeWith(value = ActionTokenReducedKey.ExternalizerImpl.class) +public class ActionTokenReducedKey implements Serializable { + + private final String userId; + private final String actionId; + + /** + * Nonce that must match. + */ + private final UUID actionVerificationNonce; + + public ActionTokenReducedKey(String userId, String actionId, UUID actionVerificationNonce) { + this.userId = userId; + this.actionId = actionId; + this.actionVerificationNonce = actionVerificationNonce; + } + + public String getUserId() { + return userId; + } + + public String getActionId() { + return actionId; + } + + public UUID getActionVerificationNonce() { + return actionVerificationNonce; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 71 * hash + Objects.hashCode(this.userId); + hash = 71 * hash + Objects.hashCode(this.actionId); + hash = 71 * hash + Objects.hashCode(this.actionVerificationNonce); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ActionTokenReducedKey other = (ActionTokenReducedKey) obj; + return Objects.equals(this.userId, other.getUserId()) + && Objects.equals(this.actionId, other.getActionId()) + && Objects.equals(this.actionVerificationNonce, other.getActionVerificationNonce()); + } + + public static class ExternalizerImpl implements Externalizer { + + @Override + public void writeObject(ObjectOutput output, ActionTokenReducedKey t) throws IOException { + output.writeUTF(t.userId); + output.writeUTF(t.actionId); + output.writeLong(t.actionVerificationNonce.getMostSignificantBits()); + output.writeLong(t.actionVerificationNonce.getLeastSignificantBits()); + } + + @Override + public ActionTokenReducedKey readObject(ObjectInput input) throws IOException, ClassNotFoundException { + return new ActionTokenReducedKey( + input.readUTF(), + input.readUTF(), + new UUID(input.readLong(), input.readLong()) + ); + } + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java new file mode 100644 index 0000000000..7c0f663da6 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java @@ -0,0 +1,76 @@ +/* + * 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.models.sessions.infinispan.entities; + +import org.keycloak.models.ActionTokenValueModel; + +import java.io.*; +import java.util.*; +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.SerializeWith; + +/** + * @author hmlnarik + */ +@SerializeWith(ActionTokenValueEntity.ExternalizerImpl.class) +public class ActionTokenValueEntity implements ActionTokenValueModel { + + private final Map notes; + + public ActionTokenValueEntity(Map notes) { + this.notes = notes == null ? Collections.EMPTY_MAP : new HashMap<>(notes); + } + + @Override + public Map getNotes() { + return Collections.unmodifiableMap(notes); + } + + @Override + public String getNote(String name) { + return notes.get(name); + } + + public static class ExternalizerImpl implements Externalizer { + + private static final int VERSION_1 = 1; + + @Override + public void writeObject(ObjectOutput output, ActionTokenValueEntity t) throws IOException { + output.writeByte(VERSION_1); + + output.writeBoolean(! t.notes.isEmpty()); + if (! t.notes.isEmpty()) { + output.writeObject(t.notes); + } + } + + @Override + public ActionTokenValueEntity readObject(ObjectInput input) throws IOException, ClassNotFoundException { + byte version = input.readByte(); + + if (version != VERSION_1) { + throw new IOException("Invalid version: " + version); + } + boolean notesEmpty = input.readBoolean(); + + Map notes = notesEmpty ? Collections.EMPTY_MAP : (Map) input.readObject(); + + return new ActionTokenValueEntity(notes); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java new file mode 100644 index 0000000000..3641d5f171 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticatedClientSessionEntity.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 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.models.sessions.infinispan.entities; + +import java.io.Serializable; +import java.util.Map; +import java.util.Set; + +/** + * @author Marek Posolda + */ +public class AuthenticatedClientSessionEntity implements Serializable { + + private String authMethod; + private String redirectUri; + private int timestamp; + private String action; + + private Set roles; + private Set protocolMappers; + private Map notes; + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public String getRedirectUri() { + return redirectUri; + } + + public void setRedirectUri(String redirectUri) { + this.redirectUri = redirectUri; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public Set getProtocolMappers() { + return protocolMappers; + } + + public void setProtocolMappers(Set protocolMappers) { + this.protocolMappers = protocolMappers; + } + + public Map getNotes() { + return notes; + } + + public void setNotes(Map notes) { + this.notes = notes; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java old mode 100755 new mode 100644 similarity index 61% rename from model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java rename to model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java index 0cbdc79d9b..0b992254b2 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java @@ -17,61 +17,49 @@ package org.keycloak.models.sessions.infinispan.entities; -import org.keycloak.models.ClientSessionModel; - import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import org.keycloak.sessions.AuthenticationSessionModel; + /** - * @author Stian Thorgersen + * @author Marek Posolda */ -public class ClientSessionEntity extends SessionEntity { +public class AuthenticationSessionEntity extends SessionEntity { - private String client; - - private String userSession; - - private String authMethod; + private String clientUuid; + private String authUserId; private String redirectUri; - private int timestamp; - private String action; - private Set roles; private Set protocolMappers; - private Map notes; + + private Map executionStatus = new HashMap<>();; + private String protocol; + + private Map clientNotes; + private Map authNotes; + private Set requiredActions = new HashSet<>(); private Map userSessionNotes; - private Map authenticatorStatus = new HashMap<>(); - private String authUserId; - private Set requiredActions = new HashSet<>(); - - public String getClient() { - return client; + public String getClientUuid() { + return clientUuid; } - public void setClient(String client) { - this.client = client; + public void setClientUuid(String clientUuid) { + this.clientUuid = clientUuid; } - public String getUserSession() { - return userSession; + public String getAuthUserId() { + return authUserId; } - public void setUserSession(String userSession) { - this.userSession = userSession; - } - - public String getAuthMethod() { - return authMethod; - } - - public void setAuthMethod(String authMethod) { - this.authMethod = authMethod; + public void setAuthUserId(String authUserId) { + this.authUserId = authUserId; } public String getRedirectUri() { @@ -114,28 +102,36 @@ public class ClientSessionEntity extends SessionEntity { this.protocolMappers = protocolMappers; } - public Map getNotes() { - return notes; + public Map getExecutionStatus() { + return executionStatus; } - public void setNotes(Map notes) { - this.notes = notes; + public void setExecutionStatus(Map executionStatus) { + this.executionStatus = executionStatus; } - public Map getAuthenticatorStatus() { - return authenticatorStatus; + public String getProtocol() { + return protocol; } - public void setAuthenticatorStatus(Map authenticatorStatus) { - this.authenticatorStatus = authenticatorStatus; + public void setProtocol(String protocol) { + this.protocol = protocol; } - public String getAuthUserId() { - return authUserId; + public Map getClientNotes() { + return clientNotes; } - public void setAuthUserId(String authUserId) { - this.authUserId = authUserId; + public void setClientNotes(Map clientNotes) { + this.clientNotes = clientNotes; + } + + public Set getRequiredActions() { + return requiredActions; + } + + public void setRequiredActions(Set requiredActions) { + this.requiredActions = requiredActions; } public Map getUserSessionNotes() { @@ -146,7 +142,11 @@ public class ClientSessionEntity extends SessionEntity { this.userSessionNotes = userSessionNotes; } - public Set getRequiredActions() { - return requiredActions; + public Map getAuthNotes() { + return authNotes; + } + + public void setAuthNotes(Map authNotes) { + this.authNotes = authNotes; } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java index 538babfa4d..54d182f0d8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/UserSessionEntity.java @@ -46,12 +46,12 @@ public class UserSessionEntity extends SessionEntity { private int lastSessionRefresh; - private Set clientSessions = new CopyOnWriteArraySet<>(); - private UserSessionModel.State state; private Map notes = new ConcurrentHashMap<>(); + private Map authenticatedClientSessions; + public String getUser() { return user; } @@ -108,10 +108,6 @@ public class UserSessionEntity extends SessionEntity { this.lastSessionRefresh = lastSessionRefresh; } - public Set getClientSessions() { - return clientSessions; - } - public Map getNotes() { return notes; } @@ -120,6 +116,14 @@ public class UserSessionEntity extends SessionEntity { this.notes = notes; } + public Map getAuthenticatedClientSessions() { + return authenticatedClientSessions; + } + + public void setAuthenticatedClientSessions(Map authenticatedClientSessions) { + this.authenticatedClientSessions = authenticatedClientSessions; + } + public UserSessionModel.State getState() { return state; } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java index 83a3885172..2b6fb71752 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java @@ -19,7 +19,6 @@ package org.keycloak.models.sessions.infinispan.initializer; import org.jboss.logging.Logger; import org.keycloak.cluster.ClusterProvider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; @@ -64,12 +63,7 @@ public class OfflineUserSessionLoader implements SessionLoader { for (UserSessionModel persistentSession : sessions) { // Save to memory/infinispan - UserSessionModel offlineUserSession = session.sessions().importUserSession(persistentSession, true); - - for (ClientSessionModel persistentClientSession : persistentSession.getClientSessions()) { - ClientSessionModel offlineClientSession = session.sessions().importClientSession(persistentClientSession, true); - offlineClientSession.setUserSession(offlineUserSession); - } + UserSessionModel offlineUserSession = session.sessions().importUserSession(persistentSession, true, true); } return true; diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java deleted file mode 100644 index 351921591c..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2016 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.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Collector; -import org.infinispan.distexec.mapreduce.Mapper; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; - -import java.io.Serializable; - -/** - * @author Stian Thorgersen - */ -public class ClientSessionMapper implements Mapper, Serializable { - - public ClientSessionMapper(String realm) { - this.realm = realm; - } - - private enum EmitValue { - KEY, ENTITY, USER_SESSION_AND_TIMESTAMP - } - - private String realm; - - private EmitValue emit = EmitValue.ENTITY; - - private String client; - - private String userSession; - - private Long expiredRefresh; - - private Boolean requireNullUserSession = false; - - public static ClientSessionMapper create(String realm) { - return new ClientSessionMapper(realm); - } - - public ClientSessionMapper emitKey() { - emit = EmitValue.KEY; - return this; - } - - public ClientSessionMapper emitUserSessionAndTimestamp() { - emit = EmitValue.USER_SESSION_AND_TIMESTAMP; - return this; - } - - public ClientSessionMapper client(String client) { - this.client = client; - return this; - } - - public ClientSessionMapper userSession(String userSession) { - this.userSession = userSession; - return this; - } - - public ClientSessionMapper expiredRefresh(long expiredRefresh) { - this.expiredRefresh = expiredRefresh; - return this; - } - - public ClientSessionMapper requireNullUserSession(boolean requireNullUserSession) { - this.requireNullUserSession = requireNullUserSession; - return this; - } - - @Override - public void map(String key, SessionEntity e, Collector collector) { - if (!realm.equals(e.getRealm())) { - return; - } - - if (!(e instanceof ClientSessionEntity)) { - return; - } - - ClientSessionEntity entity = (ClientSessionEntity) e; - - if (client != null && !entity.getClient().equals(client)) { - return; - } - - if (userSession != null && !userSession.equals(entity.getUserSession())) { - return; - } - - if (requireNullUserSession && entity.getUserSession() != null) { - return; - } - - if (expiredRefresh != null && entity.getTimestamp() > expiredRefresh) { - return; - } - - switch (emit) { - case KEY: - collector.emit(key, key); - break; - case ENTITY: - collector.emit(key, entity); - break; - case USER_SESSION_AND_TIMESTAMP: - if (entity.getUserSession() != null) { - collector.emit(entity.getUserSession(), entity.getTimestamp()); - } - break; - } - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java deleted file mode 100644 index 971f89af19..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2016 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.models.sessions.infinispan.mapreduce; - -import org.infinispan.distexec.mapreduce.Collector; -import org.infinispan.distexec.mapreduce.Mapper; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; - -import java.io.Serializable; -import java.util.Collection; - -/** - * Return all clientSessions attached to any from input list of userSessions - * - * @author Marek Posolda - */ -public class ClientSessionsOfUserSessionMapper implements Mapper, Serializable { - - private String realm; - private Collection userSessions; - - private EmitValue emit = EmitValue.ENTITY; - - private enum EmitValue { - KEY, ENTITY - } - - public ClientSessionsOfUserSessionMapper(String realm, Collection userSessions) { - this.realm = realm; - this.userSessions = userSessions; - } - - public ClientSessionsOfUserSessionMapper emitKey() { - emit = EmitValue.KEY; - return this; - } - - @Override - public void map(String key, SessionEntity e, Collector collector) { - if (!realm.equals(e.getRealm())) { - return; - } - - if (!(e instanceof ClientSessionEntity)) { - return; - } - - ClientSessionEntity entity = (ClientSessionEntity) e; - - if (userSessions.contains(entity.getUserSession())) { - switch (emit) { - case KEY: - collector.emit(entity.getId(), entity.getId()); - break; - case ENTITY: - collector.emit(entity.getId(), entity); - break; - } - } - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java new file mode 100644 index 0000000000..c471793842 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016 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.models.sessions.infinispan.stream; + +import java.io.Serializable; +import java.util.Map; +import java.util.function.Predicate; + +import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; + +/** + * @author Marek Posolda + */ +public class AuthenticationSessionPredicate implements Predicate>, Serializable { + + private String realm; + + private String client; + + private String user; + + private Integer expired; + + //private String brokerSessionId; + //private String brokerUserId; + + private AuthenticationSessionPredicate(String realm) { + this.realm = realm; + } + + public static AuthenticationSessionPredicate create(String realm) { + return new AuthenticationSessionPredicate(realm); + } + + public AuthenticationSessionPredicate user(String user) { + this.user = user; + return this; + } + + public AuthenticationSessionPredicate client(String client) { + this.client = client; + return this; + } + + public AuthenticationSessionPredicate expired(Integer expired) { + this.expired = expired; + return this; + } + +// public UserSessionPredicate brokerSessionId(String id) { +// this.brokerSessionId = id; +// return this; +// } + +// public UserSessionPredicate brokerUserId(String id) { +// this.brokerUserId = id; +// return this; +// } + + @Override + public boolean test(Map.Entry entry) { + AuthenticationSessionEntity entity = entry.getValue(); + + if (!realm.equals(entity.getRealm())) { + return false; + } + + if (user != null && !entity.getAuthUserId().equals(user)) { + return false; + } + + if (client != null && !entity.getClientUuid().equals(client)) { + return false; + } + +// if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) { +// return false; +// } +// +// if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) { +// return false; +// } + + if (expired != null && entity.getTimestamp() > expired) { + return false; + } + + return true; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java deleted file mode 100644 index 0f3ce5aebf..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/ClientSessionPredicate.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2016 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.models.sessions.infinispan.stream; - -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; -import org.keycloak.models.sessions.infinispan.entities.SessionEntity; - -import java.io.Serializable; -import java.util.Map; -import java.util.function.Predicate; - -/** - * @author Stian Thorgersen - */ -public class ClientSessionPredicate implements Predicate>, Serializable { - - private String realm; - - private String client; - - private String userSession; - - private Long expiredRefresh; - - private Boolean requireUserSession = false; - - private Boolean requireNullUserSession = false; - - private ClientSessionPredicate(String realm) { - this.realm = realm; - } - - public static ClientSessionPredicate create(String realm) { - return new ClientSessionPredicate(realm); - } - - public ClientSessionPredicate client(String client) { - this.client = client; - return this; - } - - public ClientSessionPredicate userSession(String userSession) { - this.userSession = userSession; - return this; - } - - public ClientSessionPredicate expiredRefresh(long expiredRefresh) { - this.expiredRefresh = expiredRefresh; - return this; - } - - public ClientSessionPredicate requireUserSession() { - requireUserSession = true; - return this; - } - - public ClientSessionPredicate requireNullUserSession() { - requireNullUserSession = true; - return this; - } - - @Override - public boolean test(Map.Entry entry) { - SessionEntity e = entry.getValue(); - - if (!realm.equals(e.getRealm())) { - return false; - } - - if (!(e instanceof ClientSessionEntity)) { - return false; - } - - ClientSessionEntity entity = (ClientSessionEntity) e; - - if (client != null && !entity.getClient().equals(client)) { - return false; - } - - if (userSession != null && !userSession.equals(entity.getUserSession())) { - return false; - } - - if (requireUserSession && entity.getUserSession() == null) { - return false; - } - - if (requireNullUserSession && entity.getUserSession() != null) { - return false; - } - - if (expiredRefresh != null && entity.getTimestamp() > expiredRefresh) { - return false; - } - - return true; - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java index 4907ec1ed2..ec2a2cb853 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Comparators.java @@ -18,6 +18,8 @@ package org.keycloak.models.sessions.infinispan.stream; import org.keycloak.models.sessions.infinispan.UserSessionTimestamp; +import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import java.io.Serializable; import java.util.Comparator; @@ -38,4 +40,17 @@ public class Comparators { } } + + public static Comparator userSessionLastSessionRefresh() { + return new UserSessionLastSessionRefreshComparator(); + } + + private static class UserSessionLastSessionRefreshComparator implements Comparator, Serializable { + + @Override + public int compare(UserSessionEntity u1, UserSessionEntity u2) { + return u1.getLastSessionRefresh() - u2.getLastSessionRefresh(); + } + } + } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java index 6bf13580ed..dd2db6821f 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/Mappers.java @@ -18,10 +18,10 @@ package org.keycloak.models.sessions.infinispan.stream; import org.keycloak.models.sessions.infinispan.UserSessionTimestamp; -import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity; import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity; import java.io.Serializable; import java.util.Map; @@ -33,10 +33,6 @@ import java.util.function.Function; */ public class Mappers { - public static Function, UserSessionTimestamp> clientSessionToUserSessionTimestamp() { - return new ClientSessionToUserSessionTimestampMapper(); - } - public static Function>, UserSessionTimestamp> userSessionTimestamp() { return new UserSessionTimestampMapper(); } @@ -49,23 +45,14 @@ public class Mappers { return new SessionEntityMapper(); } + public static Function, UserSessionEntity> userSessionEntity() { + return new UserSessionEntityMapper(); + } + public static Function, LoginFailureKey> loginFailureId() { return new LoginFailureIdMapper(); } - public static Function, String> clientSessionToUserSessionId() { - return new ClientSessionToUserSessionIdMapper(); - } - - private static class ClientSessionToUserSessionTimestampMapper implements Function, UserSessionTimestamp>, Serializable { - @Override - public UserSessionTimestamp apply(Map.Entry entry) { - SessionEntity e = entry.getValue(); - ClientSessionEntity entity = (ClientSessionEntity) e; - return new UserSessionTimestamp(entity.getUserSession(), entity.getTimestamp()); - } - } - private static class UserSessionTimestampMapper implements Function>, org.keycloak.models.sessions.infinispan.UserSessionTimestamp>, Serializable { @Override public org.keycloak.models.sessions.infinispan.UserSessionTimestamp apply(Map.Entry> e) { @@ -87,6 +74,13 @@ public class Mappers { } } + private static class UserSessionEntityMapper implements Function, UserSessionEntity>, Serializable { + @Override + public UserSessionEntity apply(Map.Entry entry) { + return (UserSessionEntity) entry.getValue(); + } + } + private static class LoginFailureIdMapper implements Function, LoginFailureKey>, Serializable { @Override public LoginFailureKey apply(Map.Entry entry) { @@ -94,12 +88,4 @@ public class Mappers { } } - private static class ClientSessionToUserSessionIdMapper implements Function, String>, Serializable { - @Override - public String apply(Map.Entry entry) { - SessionEntity e = entry.getValue(); - ClientSessionEntity entity = (ClientSessionEntity) e; - return entity.getUserSession(); - } - } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java index 77ff572305..0cc3fccf97 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/UserSessionPredicate.java @@ -33,6 +33,8 @@ public class UserSessionPredicate implements Predicate { em.flush(); } + @Override + public int getActionTokenGeneratedByAdminLifespan() { + return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN, 12 * 60 * 60); + } + + @Override + public void setActionTokenGeneratedByAdminLifespan(int actionTokenGeneratedByAdminLifespan) { + setAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN, actionTokenGeneratedByAdminLifespan); + } + + @Override + public int getActionTokenGeneratedByUserLifespan() { + return getAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN, getAccessCodeLifespanUserAction()); + } + + @Override + public void setActionTokenGeneratedByUserLifespan(int actionTokenGeneratedByUserLifespan) { + setAttribute(RealmAttributes.ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN, actionTokenGeneratedByUserLifespan); + } + protected RequiredCredentialModel initRequiredCredentialModel(String type) { RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type); if (model == null) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java index 499a008647..6ee1074c6c 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmAttributes.java @@ -26,4 +26,8 @@ public interface RealmAttributes { String DISPLAY_NAME_HTML = "displayNameHtml"; + String ACTION_TOKEN_GENERATED_BY_ADMIN_LIFESPAN = "actionTokenGeneratedByAdminLifespan"; + + String ACTION_TOKEN_GENERATED_BY_USER_LIFESPAN = "actionTokenGeneratedByUserLifespan"; + } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java index b3aea63f55..64246e8d6d 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java @@ -17,14 +17,14 @@ package org.keycloak.models.jpa.session; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.session.PersistentClientSessionAdapter; +import org.keycloak.models.session.PersistentAuthenticatedClientSessionAdapter; import org.keycloak.models.session.PersistentClientSessionModel; import org.keycloak.models.session.PersistentUserSessionAdapter; import org.keycloak.models.session.PersistentUserSessionModel; @@ -34,8 +34,9 @@ import javax.persistence.EntityManager; import javax.persistence.Query; import javax.persistence.TypedQuery; import java.util.ArrayList; -import java.util.LinkedList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @author Marek Posolda @@ -68,12 +69,11 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv } @Override - public void createClientSession(ClientSessionModel clientSession, boolean offline) { - PersistentClientSessionAdapter adapter = new PersistentClientSessionAdapter(clientSession); + public void createClientSession(AuthenticatedClientSessionModel clientSession, boolean offline) { + PersistentAuthenticatedClientSessionAdapter adapter = new PersistentAuthenticatedClientSessionAdapter(clientSession); PersistentClientSessionModel model = adapter.getUpdatedModel(); PersistentClientSessionEntity entity = new PersistentClientSessionEntity(); - entity.setClientSessionId(clientSession.getId()); entity.setClientId(clientSession.getClient().getId()); entity.setTimestamp(clientSession.getTimestamp()); String offlineStr = offlineToString(offline); @@ -121,9 +121,9 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv } @Override - public void removeClientSession(String clientSessionId, boolean offline) { + public void removeClientSession(String userSessionId, String clientUUID, boolean offline) { String offlineStr = offlineToString(offline); - PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(clientSessionId, offlineStr)); + PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientUUID, offlineStr)); if (sessionEntity != null) { em.remove(sessionEntity); @@ -227,14 +227,14 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv int j = 0; for (UserSessionModel ss : result) { PersistentUserSessionAdapter userSession = (PersistentUserSessionAdapter) ss; - List currentClientSessions = userSession.getClientSessions(); // This is empty now and we want to fill it + Map currentClientSessions = userSession.getAuthenticatedClientSessions(); // This is empty now and we want to fill it boolean next = true; while (next && j < clientSessions.size()) { PersistentClientSessionEntity clientSession = clientSessions.get(j); if (clientSession.getUserSessionId().equals(userSession.getId())) { - PersistentClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSession); - currentClientSessions.add(clientSessAdapter); + PersistentAuthenticatedClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSession); + currentClientSessions.put(clientSession.getClientId(), clientSessAdapter); j++; } else { next = false; @@ -243,6 +243,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv } } + return result; } @@ -252,21 +253,20 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv model.setLastSessionRefresh(entity.getLastSessionRefresh()); model.setData(entity.getData()); - List clientSessions = new LinkedList<>(); + Map clientSessions = new HashMap<>(); return new PersistentUserSessionAdapter(model, realm, user, clientSessions); } - private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) { + private PersistentAuthenticatedClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) { ClientModel client = realm.getClientById(entity.getClientId()); PersistentClientSessionModel model = new PersistentClientSessionModel(); - model.setClientSessionId(entity.getClientSessionId()); model.setClientId(entity.getClientId()); model.setUserSessionId(userSession.getId()); model.setUserId(userSession.getUser().getId()); model.setTimestamp(entity.getTimestamp()); model.setData(entity.getData()); - return new PersistentClientSessionAdapter(model, realm, client, userSession); + return new PersistentAuthenticatedClientSessionAdapter(model, realm, client, userSession); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java index 35265afe30..e12223df35 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java @@ -20,7 +20,6 @@ package org.keycloak.models.jpa.session; import org.keycloak.Config; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.session.UserSessionPersisterProviderFactory; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java index 7250836580..8910bca948 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -45,12 +45,10 @@ import java.io.Serializable; public class PersistentClientSessionEntity { @Id - @Column(name="CLIENT_SESSION_ID", length = 36) - protected String clientSessionId; - @Column(name = "USER_SESSION_ID", length = 36) protected String userSessionId; + @Id @Column(name="CLIENT_ID", length = 36) protected String clientId; @@ -64,14 +62,6 @@ public class PersistentClientSessionEntity { @Column(name="DATA") protected String data; - public String getClientSessionId() { - return clientSessionId; - } - - public void setClientSessionId(String clientSessionId) { - this.clientSessionId = clientSessionId; - } - public String getUserSessionId() { return userSessionId; } @@ -114,20 +104,27 @@ public class PersistentClientSessionEntity { public static class Key implements Serializable { - protected String clientSessionId; + protected String userSessionId; + + protected String clientId; protected String offline; public Key() { } - public Key(String clientSessionId, String offline) { - this.clientSessionId = clientSessionId; + public Key(String userSessionId, String clientId, String offline) { + this.userSessionId = userSessionId; + this.clientId = clientId; this.offline = offline; } - public String getClientSessionId() { - return clientSessionId; + public String getUserSessionId() { + return userSessionId; + } + + public String getClientId() { + return clientId; } public String getOffline() { @@ -141,7 +138,8 @@ public class PersistentClientSessionEntity { Key key = (Key) o; - if (this.clientSessionId != null ? !this.clientSessionId.equals(key.clientSessionId) : key.clientSessionId != null) return false; + if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false; + if (this.clientId != null ? !this.clientId.equals(key.clientId) : key.clientId != null) return false; if (this.offline != null ? !this.offline.equals(key.offline) : key.offline != null) return false; return true; @@ -149,7 +147,8 @@ public class PersistentClientSessionEntity { @Override public int hashCode() { - int result = this.clientSessionId != null ? this.clientSessionId.hashCode() : 0; + int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0; + result = 37 * result + (this.clientId != null ? this.clientId.hashCode() : 0); result = 31 * result + (this.offline != null ? this.offline.hashCode() : 0); return result; } diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml new file mode 100644 index 0000000000..51be2fd18c --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-3.2.0.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index 59855ec614..ae7d98b4e4 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -47,4 +47,5 @@ + diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index 98632fbe3c..fb6e02997a 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -18,10 +18,10 @@ package org.keycloak.authentication; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.FormMessage; +import org.keycloak.sessions.AuthenticationSessionModel; import java.net.URI; @@ -62,7 +62,7 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon * * @return */ - ClientSessionModel getClientSession(); + AuthenticationSessionModel getAuthenticationSession(); /** * Create a Freemarker form builder that presets the user, action URI, and a generated access code @@ -80,11 +80,19 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon URI getActionUrl(String code); /** - * Get the action URL for the required action. This auto-generates the access code. + * Get the action URL for the action token executor. + * + * @param tokenString String representation (JWT) of action token + * @return + */ + URI getActionTokenUrl(String tokenString); + + /** + * Get the refresh URL for the required action. * * @return */ - URI getActionUrl(); + URI getRefreshExecutionUrl(); /** * End the flow and redirect browser based on protocol specific respones. This should only be executed @@ -99,6 +107,12 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon */ void resetFlow(); + /** + * Reset the current flow to the beginning and restarts it. Allows to add additional listener, which is triggered after flow restarted + * + */ + void resetFlow(Runnable afterResetListener); + /** * Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result * of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email. diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java index 7c7d143f13..2c1255d57a 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/FormContext.java @@ -22,10 +22,10 @@ import org.keycloak.common.ClientConnection; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.UriInfo; @@ -79,11 +79,11 @@ public interface FormContext { RealmModel getRealm(); /** - * ClientSessionModel attached to this flow + * AuthenticationSessionModel attached to this flow * * @return */ - ClientSessionModel getClientSession(); + AuthenticationSessionModel getAuthenticationSession(); /** * Information about the IP address from the connecting HTTP client. diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java index 3ece79e5f4..caaa14e59c 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionContext.java @@ -21,11 +21,10 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.common.ClientConnection; import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -90,8 +89,7 @@ public interface RequiredActionContext { */ UserModel getUser(); RealmModel getRealm(); - ClientSessionModel getClientSession(); - UserSessionModel getUserSession(); + AuthenticationSessionModel getAuthenticationSession(); ClientConnection getConnection(); UriInfo getUriInfo(); KeycloakSession getSession(); diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java index 76dc582e0e..517b5f4a4c 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/RequiredActionFactory.java @@ -35,4 +35,13 @@ public interface RequiredActionFactory extends ProviderFactory } @Override - public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) { + public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) { } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java index 863decb984..ba8276f497 100644 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java @@ -17,9 +17,9 @@ package org.keycloak.broker.provider; import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.UriInfo; @@ -34,16 +34,16 @@ public class AuthenticationRequest { private final HttpRequest httpRequest; private final RealmModel realm; private final String redirectUri; - private final ClientSessionModel clientSession; + private final AuthenticationSessionModel authSession; - public AuthenticationRequest(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) { + public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) { this.session = session; this.realm = realm; this.httpRequest = httpRequest; this.uriInfo = uriInfo; this.state = state; this.redirectUri = redirectUri; - this.clientSession = clientSession; + this.authSession = authSession; } public KeycloakSession getSession() { @@ -76,7 +76,7 @@ public class AuthenticationRequest { return this.redirectUri; } - public ClientSessionModel getClientSession() { - return this.clientSession; + public AuthenticationSessionModel getAuthenticationSession() { + return this.authSession; } } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java index f2c8a7aad7..a2b1dc5624 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java @@ -16,9 +16,9 @@ */ package org.keycloak.broker.provider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.sessions.AuthenticationSessionModel; import java.util.ArrayList; import java.util.HashMap; @@ -46,7 +46,7 @@ public class BrokeredIdentityContext { private IdentityProviderModel idpConfig; private IdentityProvider idp; private Map contextData = new HashMap<>(); - private ClientSessionModel clientSession; + private AuthenticationSessionModel authenticationSession; public BrokeredIdentityContext(String id) { if (id == null) { @@ -190,12 +190,12 @@ public class BrokeredIdentityContext { this.lastName = lastName; } - public ClientSessionModel getClientSession() { - return clientSession; + public AuthenticationSessionModel getAuthenticationSession() { + return authenticationSession; } - public void setClientSession(ClientSessionModel clientSession) { - this.clientSession = clientSession; + public void setAuthenticationSession(AuthenticationSessionModel authenticationSession) { + this.authenticationSession = authenticationSession; } public void setName(String name) { diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index b14572ee5d..c4f1c5c3b6 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -17,7 +17,6 @@ package org.keycloak.broker.provider; import org.keycloak.events.EventBuilder; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -25,6 +24,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.provider.Provider; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -51,7 +51,7 @@ public interface IdentityProvider extends Provi void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context); - void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context); + void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context); void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context); void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context); diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java index 0ef227dabc..5e3a9b37e4 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Details.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java @@ -25,6 +25,7 @@ public interface Details { String EMAIL = "email"; String PREVIOUS_EMAIL = "previous_email"; String UPDATED_EMAIL = "updated_email"; + String ACTION = "action"; String CODE_ID = "code_id"; String REDIRECT_URI = "redirect_uri"; String RESPONSE_TYPE = "response_type"; @@ -63,4 +64,6 @@ public interface Details { String CLIENT_REGISTRATION_POLICY = "client_registration_policy"; + String EXISTING_USER = "previous_user"; + } diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java index e82421f079..ab954bc40a 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java @@ -37,6 +37,7 @@ public interface Errors { String USER_DISABLED = "user_disabled"; String USER_TEMPORARILY_DISABLED = "user_temporarily_disabled"; String INVALID_USER_CREDENTIALS = "invalid_user_credentials"; + String DIFFERENT_USER_AUTHENTICATED = "different_user_authenticated"; String USERNAME_MISSING = "username_missing"; String USERNAME_IN_USE = "username_in_use"; diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java index f77137fc0a..920646fa58 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java @@ -79,6 +79,9 @@ public enum EventType { RESET_PASSWORD(true), RESET_PASSWORD_ERROR(true), + RESTART_AUTHENTICATION(true), + RESTART_AUTHENTICATION_ERROR(true), + INVALID_SIGNATURE(false), INVALID_SIGNATURE_ERROR(false), REGISTER_NODE(false), @@ -89,6 +92,8 @@ public enum EventType { USER_INFO_REQUEST(false), USER_INFO_REQUEST_ERROR(false), + IDENTITY_PROVIDER_LINK_ACCOUNT(true), + IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR(true), IDENTITY_PROVIDER_LOGIN(false), IDENTITY_PROVIDER_LOGIN_ERROR(false), IDENTITY_PROVIDER_FIRST_LOGIN(true), @@ -105,6 +110,8 @@ public enum EventType { CUSTOM_REQUIRED_ACTION_ERROR(true), EXECUTE_ACTIONS(true), EXECUTE_ACTIONS_ERROR(true), + EXECUTE_ACTION_TOKEN(true), + EXECUTE_ACTION_TOKEN_ERROR(true), CLIENT_INFO(false), CLIENT_INFO_ERROR(false), @@ -124,6 +131,10 @@ public enum EventType { this.saveByDefault = saveByDefault; } + /** + * Determines whether this event is stored when the admin has not set a specific set of event types to save. + * @return + */ public boolean isSaveByDefault() { return saveByDefault; } diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java index fcaff7a520..476e3aa8bf 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsPages.java @@ -24,6 +24,6 @@ public enum LoginFormsPages { LOGIN, LOGIN_TOTP, LOGIN_CONFIG_TOTP, LOGIN_VERIFY_EMAIL, LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL, - OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, CODE; + OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, REGISTER, INFO, ERROR, LOGIN_UPDATE_PROFILE, LOGIN_PAGE_EXPIRED, CODE; } diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java index f16f0c2165..32195ef4db 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java @@ -17,12 +17,12 @@ package org.keycloak.forms.login; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.provider.Provider; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -68,16 +68,16 @@ public interface LoginFormsProvider extends Provider { public Response createIdpLinkEmailPage(); + public Response createLoginExpiredPage(); + public Response createErrorPage(); - public Response createOAuthGrant(ClientSessionModel clientSessionModel); + public Response createOAuthGrant(); public Response createCode(); public LoginFormsProvider setClientSessionCode(String accessCode); - public LoginFormsProvider setClientSession(ClientSessionModel clientSession); - public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested, List protocolMappers); public LoginFormsProvider setAccessRequest(String message); diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java new file mode 100644 index 0000000000..4e4a8dbb5f --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProvider.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models; + +import org.keycloak.provider.Provider; + +import java.util.Map; + +/** + * Internal action token store provider. + * @author hmlnarik + */ +public interface ActionTokenStoreProvider extends Provider { + + /** + * Adds a given token to token store. + * @param actionTokenKey key + * @param notes Optional notes to be stored with the token. Can be {@code null} in which case it is treated as an empty map. + */ + void put(ActionTokenKeyModel actionTokenKey, Map notes); + + /** + * Returns token corresponding to the given key from the internal action token store + * @param key key + * @return {@code null} if no token is found for given key and nonce, value otherwise + */ + ActionTokenValueModel get(ActionTokenKeyModel key); + + /** + * Removes token corresponding to the given key from the internal action token store, and returns the stored value + * @param key key + * @param nonce nonce that must match a given key + * @return {@code null} if no token is found for given key and nonce, value otherwise + */ + ActionTokenValueModel remove(ActionTokenKeyModel key); + + void removeAll(String userId, String actionId); + + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java new file mode 100644 index 0000000000..26d086d3a4 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java @@ -0,0 +1,27 @@ +/* + * 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.models; + +import org.keycloak.provider.ProviderFactory; + +/** + * + * @author hmlnarik + */ +public interface ActionTokenStoreProviderFactory extends ProviderFactory { + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreSpi.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreSpi.java new file mode 100644 index 0000000000..66ee518006 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreSpi.java @@ -0,0 +1,50 @@ +/* + * 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.models; + +import org.keycloak.provider.*; + +/** + * SPI for action tokens. + * + * @author hmlnarik + */ +public class ActionTokenStoreSpi implements Spi { + + public static final String NAME = "actionToken"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Class getProviderClass() { + return ActionTokenStoreProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return ActionTokenStoreProviderFactory.class; + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java index f5e58d33bf..10b28a28ba 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java @@ -18,8 +18,8 @@ package org.keycloak.models.session; import org.keycloak.Config; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; @@ -70,7 +70,7 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste } @Override - public void createClientSession(ClientSessionModel clientSession, boolean offline) { + public void createClientSession(AuthenticatedClientSessionModel clientSession, boolean offline) { } @@ -85,7 +85,7 @@ public class DisabledUserSessionPersisterProvider implements UserSessionPersiste } @Override - public void removeClientSession(String clientSessionId, boolean offline) { + public void removeClientSession(String userSessionId, String clientUUID, boolean offline) { } diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java similarity index 69% rename from server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java rename to server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java index f842787a2c..1550d93854 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentAuthenticatedClientSessionAdapter.java @@ -18,25 +18,23 @@ package org.keycloak.models.session; import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; /** * @author Marek Posolda */ -public class PersistentClientSessionAdapter implements ClientSessionModel { +public class PersistentAuthenticatedClientSessionAdapter implements AuthenticatedClientSessionModel { private final PersistentClientSessionModel model; private final RealmModel realm; @@ -45,23 +43,18 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { private PersistentClientSessionData data; - public PersistentClientSessionAdapter(ClientSessionModel clientSession) { + public PersistentAuthenticatedClientSessionAdapter(AuthenticatedClientSessionModel clientSession) { data = new PersistentClientSessionData(); data.setAction(clientSession.getAction()); - data.setAuthMethod(clientSession.getAuthMethod()); - data.setExecutionStatus(clientSession.getExecutionStatus()); + data.setAuthMethod(clientSession.getProtocol()); data.setNotes(clientSession.getNotes()); data.setProtocolMappers(clientSession.getProtocolMappers()); data.setRedirectUri(clientSession.getRedirectUri()); data.setRoles(clientSession.getRoles()); - data.setUserSessionNotes(clientSession.getUserSessionNotes()); model = new PersistentClientSessionModel(); model.setClientId(clientSession.getClient().getId()); - model.setClientSessionId(clientSession.getId()); - if (clientSession.getAuthenticatedUser() != null) { - model.setUserId(clientSession.getAuthenticatedUser().getId()); - } + model.setUserId(clientSession.getUserSession().getUser().getId()); model.setUserSessionId(clientSession.getUserSession().getId()); model.setTimestamp(clientSession.getTimestamp()); @@ -70,7 +63,7 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { userSession = clientSession.getUserSession(); } - public PersistentClientSessionAdapter(PersistentClientSessionModel model, RealmModel realm, ClientModel client, UserSessionModel userSession) { + public PersistentAuthenticatedClientSessionAdapter(PersistentClientSessionModel model, RealmModel realm, ClientModel client, UserSessionModel userSession) { this.model = model; this.realm = realm; this.client = client; @@ -104,7 +97,7 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { @Override public String getId() { - return model.getClientSessionId(); + return null; } @Override @@ -178,37 +171,12 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { } @Override - public Map getExecutionStatus() { - return getData().getExecutionStatus(); - } - - @Override - public void setExecutionStatus(String authenticator, ExecutionStatus status) { - getData().getExecutionStatus().put(authenticator, status); - } - - @Override - public void clearExecutionStatus() { - getData().getExecutionStatus().clear(); - } - - @Override - public UserModel getAuthenticatedUser() { - return userSession.getUser(); - } - - @Override - public void setAuthenticatedUser(UserModel user) { - throw new IllegalStateException("Not supported setAuthenticatedUser"); - } - - @Override - public String getAuthMethod() { + public String getProtocol() { return getData().getAuthMethod(); } @Override - public void setAuthMethod(String method) { + public void setProtocol(String method) { getData().setAuthMethod(method); } @@ -222,7 +190,7 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { public void setNote(String name, String value) { PersistentClientSessionData entity = getData(); if (entity.getNotes() == null) { - entity.setNotes(new HashMap()); + entity.setNotes(new HashMap<>()); } entity.getNotes().put(name, value); } @@ -242,59 +210,12 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { return entity.getNotes(); } - @Override - public Set getRequiredActions() { - return getData().getRequiredActions(); - } - - @Override - public void addRequiredAction(String action) { - getData().getRequiredActions().add(action); - } - - @Override - public void removeRequiredAction(String action) { - getData().getRequiredActions().remove(action); - } - - @Override - public void addRequiredAction(UserModel.RequiredAction action) { - addRequiredAction(action.name()); - } - - @Override - public void removeRequiredAction(UserModel.RequiredAction action) { - removeRequiredAction(action.name()); - } - - @Override - public void setUserSessionNote(String name, String value) { - PersistentClientSessionData entity = getData(); - if (entity.getUserSessionNotes() == null) { - entity.setUserSessionNotes(new HashMap()); - } - entity.getUserSessionNotes().put(name, value); - } - - @Override - public Map getUserSessionNotes() { - PersistentClientSessionData entity = getData(); - if (entity.getUserSessionNotes() == null || entity.getUserSessionNotes().isEmpty()) return Collections.emptyMap(); - return entity.getUserSessionNotes(); - } - - @Override - public void clearUserSessionNotes() { - PersistentClientSessionData entity = getData(); - entity.setUserSessionNotes(new HashMap()); - } - @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof ClientSessionModel)) return false; + if (o == null || !(o instanceof AuthenticatedClientSessionModel)) return false; - ClientSessionModel that = (ClientSessionModel) o; + AuthenticatedClientSessionModel that = (AuthenticatedClientSessionModel) o; return that.getId().equals(getId()); } @@ -320,17 +241,17 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { @JsonProperty("notes") private Map notes; - @JsonProperty("userSessionNotes") - private Map userSessionNotes; - - @JsonProperty("executionStatus") - private Map executionStatus = new HashMap<>(); - @JsonProperty("action") private String action; + // TODO: Keeping those just for backwards compatibility. @JsonIgnoreProperties doesn't work on Wildfly - probably due to classloading issues + @JsonProperty("userSessionNotes") + private Map userSessionNotes; + @JsonProperty("executionStatus") + private Map executionStatus; @JsonProperty("requiredActions") - private Set requiredActions = new HashSet<>(); + private Set requiredActions; + public String getAuthMethod() { return authMethod; @@ -372,6 +293,14 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { this.notes = notes; } + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } + public Map getUserSessionNotes() { return userSessionNotes; } @@ -380,22 +309,14 @@ public class PersistentClientSessionAdapter implements ClientSessionModel { this.userSessionNotes = userSessionNotes; } - public Map getExecutionStatus() { + public Map getExecutionStatus() { return executionStatus; } - public void setExecutionStatus(Map executionStatus) { + public void setExecutionStatus(Map executionStatus) { this.executionStatus = executionStatus; } - public String getAction() { - return action; - } - - public void setAction(String action) { - this.action = action; - } - public Set getRequiredActions() { return requiredActions; } diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java index 5990eeafe1..ee33fedf03 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentClientSessionModel.java @@ -22,20 +22,12 @@ package org.keycloak.models.session; */ public class PersistentClientSessionModel { - private String clientSessionId; private String userSessionId; private String clientId; private String userId; private int timestamp; private String data; - public String getClientSessionId() { - return clientSessionId; - } - - public void setClientSessionId(String clientSessionId) { - this.clientSessionId = clientSessionId; - } public String getUserSessionId() { return userSessionId; diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java index 6047be29ea..23436e05e0 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java @@ -18,7 +18,7 @@ package org.keycloak.models.session; import com.fasterxml.jackson.annotation.JsonProperty; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -27,7 +27,6 @@ import org.keycloak.util.JsonSerialization; import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; /** @@ -38,7 +37,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel { private final PersistentUserSessionModel model; private final UserModel user; private final RealmModel realm; - private final List clientSessions; + private final Map authenticatedClientSessions; private PersistentUserSessionData data; @@ -51,7 +50,9 @@ public class PersistentUserSessionAdapter implements UserSessionModel { data.setNotes(other.getNotes()); data.setRememberMe(other.isRememberMe()); data.setStarted(other.getStarted()); - data.setState(other.getState()); + if (other.getState() != null) { + data.setState(other.getState().toString()); + } this.model = new PersistentUserSessionModel(); this.model.setUserSessionId(other.getId()); @@ -59,14 +60,14 @@ public class PersistentUserSessionAdapter implements UserSessionModel { this.user = other.getUser(); this.realm = other.getRealm(); - this.clientSessions = other.getClientSessions(); + this.authenticatedClientSessions = other.getAuthenticatedClientSessions(); } - public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, List clientSessions) { + public PersistentUserSessionAdapter(PersistentUserSessionModel model, RealmModel realm, UserModel user, Map clientSessions) { this.model = model; this.realm = realm; this.user = user; - this.clientSessions = clientSessions; + this.authenticatedClientSessions = clientSessions; } // Lazily init data @@ -114,6 +115,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel { return user; } + @Override + public void setUser(UserModel user) { + throw new IllegalStateException("Not supported"); + } + @Override public RealmModel getRealm() { return realm; @@ -155,8 +161,8 @@ public class PersistentUserSessionAdapter implements UserSessionModel { } @Override - public List getClientSessions() { - return clientSessions; + public Map getAuthenticatedClientSessions() { + return authenticatedClientSessions; } @Override @@ -188,12 +194,29 @@ public class PersistentUserSessionAdapter implements UserSessionModel { @Override public State getState() { - return getData().getState(); + String state = getData().getState(); + + if (state == null) { + return null; + } + + // Migration to Keycloak 3.2 + if (state.equals("LOGGING_IN")) { + return State.LOGGED_IN; + } + + return State.valueOf(state); } @Override public void setState(State state) { - getData().setState(state); + String stateStr = state==null ? null : state.toString(); + getData().setState(stateStr); + } + + @Override + public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) { + throw new IllegalStateException("Not supported"); } @Override @@ -234,7 +257,7 @@ public class PersistentUserSessionAdapter implements UserSessionModel { private Map notes; @JsonProperty("state") - private State state; + private String state; public String getBrokerSessionId() { return brokerSessionId; @@ -292,11 +315,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel { this.notes = notes; } - public State getState() { + public String getState() { return state; } - public void setState(State state) { + public void setState(String state) { this.state = state; } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java index c0d033acb0..ba5a595f76 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java @@ -17,8 +17,8 @@ package org.keycloak.models.session; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -35,7 +35,7 @@ public interface UserSessionPersisterProvider extends Provider { void createUserSession(UserSessionModel userSession, boolean offline); // Assuming that corresponding userSession is already persisted - void createClientSession(ClientSessionModel clientSession, boolean offline); + void createClientSession(AuthenticatedClientSessionModel clientSession, boolean offline); void updateUserSession(UserSessionModel userSession, boolean offline); @@ -43,7 +43,7 @@ public interface UserSessionPersisterProvider extends Provider { void removeUserSession(String userSessionId, boolean offline); // Called during revoke. It will remove userSession too if this was last clientSession attached to it - void removeClientSession(String clientSessionId, boolean offline); + void removeClientSession(String userSessionId, String clientUUID, boolean offline); void onRealmRemoved(RealmModel realm); void onClientRemoved(RealmModel realm, ClientModel client); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index e90192963f..43e8ef825d 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -45,8 +45,8 @@ import org.keycloak.events.admin.AuthDetails; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.GroupModel; @@ -303,6 +303,8 @@ public class ModelToRepresentation { rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin()); + rep.setActionTokenGeneratedByAdminLifespan(realm.getActionTokenGeneratedByAdminLifespan()); + rep.setActionTokenGeneratedByUserLifespan(realm.getActionTokenGeneratedByUserLifespan()); rep.setSmtpServer(new HashMap<>(realm.getSmtpConfig())); rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders()); rep.setAccountTheme(realm.getAccountTheme()); @@ -485,7 +487,7 @@ public class ModelToRepresentation { rep.setUsername(session.getUser().getUsername()); rep.setUserId(session.getUser().getId()); rep.setIpAddress(session.getIpAddress()); - for (ClientSessionModel clientSession : session.getClientSessions()) { + for (AuthenticatedClientSessionModel clientSession : session.getAuthenticatedClientSessions().values()) { ClientModel client = clientSession.getClient(); rep.getClients().put(client.getId(), client.getClientId()); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index b857bad8b3..7e528fb63e 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -189,6 +189,14 @@ public class RepresentationToModel { newRealm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); else newRealm.setAccessCodeLifespanLogin(1800); + if (rep.getActionTokenGeneratedByAdminLifespan() != null) + newRealm.setActionTokenGeneratedByAdminLifespan(rep.getActionTokenGeneratedByAdminLifespan()); + else newRealm.setActionTokenGeneratedByAdminLifespan(12 * 60 * 60); + + if (rep.getActionTokenGeneratedByUserLifespan() != null) + newRealm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan()); + else newRealm.setActionTokenGeneratedByUserLifespan(newRealm.getAccessCodeLifespanUserAction()); + if (rep.getSslRequired() != null) newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase())); if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed()); @@ -812,6 +820,10 @@ public class RepresentationToModel { realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); if (rep.getAccessCodeLifespanLogin() != null) realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin()); + if (rep.getActionTokenGeneratedByAdminLifespan() != null) + realm.setActionTokenGeneratedByAdminLifespan(rep.getActionTokenGeneratedByAdminLifespan()); + if (rep.getActionTokenGeneratedByUserLifespan() != null) + realm.setActionTokenGeneratedByUserLifespan(rep.getActionTokenGeneratedByUserLifespan()); if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore()); if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken()); if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); diff --git a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java index 086a8edc48..569a2c0dcd 100755 --- a/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java +++ b/server-spi-private/src/main/java/org/keycloak/protocol/LoginProtocol.java @@ -18,12 +18,12 @@ package org.keycloak.protocol; import org.keycloak.events.EventBuilder; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.provider.Provider; -import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; @@ -66,19 +66,19 @@ public interface LoginProtocol extends Provider { LoginProtocol setEventBuilder(EventBuilder event); - Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode); + Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); - Response sendError(ClientSessionModel clientSession, Error error); + Response sendError(AuthenticationSessionModel authSession, Error error); - void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); - Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession); + void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); + Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); Response finishLogout(UserSessionModel userSession); /** * @param userSession - * @param clientSession + * @param authSession * @return true if SSO cookie authentication can't be used. User will need to "actively" reauthenticate */ - boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession); + boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession); } diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java new file mode 100644 index 0000000000..b182458b5e --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 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.sessions; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marek Posolda + */ +public interface AuthenticationSessionProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionSpi.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionSpi.java new file mode 100644 index 0000000000..459350e17d --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionSpi.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 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.sessions; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class AuthenticationSessionSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "authenticationSessions"; + } + + @Override + public Class getProviderClass() { + return AuthenticationSessionProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return AuthenticationSessionProviderFactory.class; + } + +} diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProvider.java b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProvider.java new file mode 100644 index 0000000000..69dad56b8f --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 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.sessions; + +import org.keycloak.provider.Provider; + +/** + * @author Marek Posolda + */ +public interface StickySessionEncoderProvider extends Provider { + + String encodeSessionId(String sessionId); + + String decodeSessionId(String encodedSessionId); + +} diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java new file mode 100644 index 0000000000..6b8c836994 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016 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.sessions; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Marek Posolda + */ +public interface StickySessionEncoderProviderFactory extends ProviderFactory { +} diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderSpi.java b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderSpi.java new file mode 100644 index 0000000000..4e1fdfff28 --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/sessions/StickySessionEncoderSpi.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 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.sessions; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Marek Posolda + */ +public class StickySessionEncoderSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "stickySessionEncoder"; + } + + @Override + public Class getProviderClass() { + return StickySessionEncoderProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return StickySessionEncoderProviderFactory.class; + } +} diff --git a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 9397536ea6..543ef256c1 100755 --- a/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi-private/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -19,6 +19,7 @@ org.keycloak.provider.ExceptionConverterSpi org.keycloak.storage.UserStorageProviderSpi org.keycloak.storage.federated.UserFederatedStorageProviderSpi org.keycloak.models.RealmSpi +org.keycloak.models.ActionTokenStoreSpi org.keycloak.models.UserSessionSpi org.keycloak.models.UserSpi org.keycloak.models.session.UserSessionPersisterSpi @@ -32,6 +33,8 @@ org.keycloak.timer.TimerSpi org.keycloak.scripting.ScriptingSpi org.keycloak.services.managers.BruteForceProtectorSpi org.keycloak.services.resource.RealmResourceSPI +org.keycloak.sessions.AuthenticationSessionSpi +org.keycloak.sessions.StickySessionEncoderSpi org.keycloak.protocol.ClientInstallationSpi org.keycloak.protocol.LoginProtocolSpi org.keycloak.protocol.ProtocolMapperSpi diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java new file mode 100644 index 0000000000..cf9d7d02e1 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenKeyModel.java @@ -0,0 +1,46 @@ +/* + * 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.models; + +import java.util.UUID; + +/** + * + * @author hmlnarik + */ +public interface ActionTokenKeyModel { + + /** + * @return ID of user which this token is for. + */ + String getUserId(); + + /** + * @return Action identifier this token is for. + */ + String getActionId(); + + /** + * Returns absolute number of seconds since the epoch in UTC timezone when the token expires. + */ + int getExpiration(); + + /** + * @return Single-use random value used for verification whether the relevant action is allowed. + */ + UUID getActionVerificationNonce(); +} diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java new file mode 100644 index 0000000000..ba01cb6cbb --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java @@ -0,0 +1,39 @@ +/* + * 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.models; + +import java.util.Map; +import java.util.UUID; + +/** + * This model represents contents of an action token shareable among Keycloak instances in the cluster. + * @author hmlnarik + */ +public interface ActionTokenValueModel { + + /** + * Returns unmodifiable map of all notes. + * @return see description. Returns empty map if no note is set, never returns {@code null}. + */ + Map getNotes(); + + /** + * Returns value of the given note (or {@code null} when no note of this name is present) + * @return see description + */ + String getNote(String name); +} diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java new file mode 100644 index 0000000000..099a39c467 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 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.models; + + +import java.util.Map; + +import org.keycloak.sessions.CommonClientSessionModel; + +/** + * @author Marek Posolda + */ +public interface AuthenticatedClientSessionModel extends CommonClientSessionModel { + + void setUserSession(UserSessionModel userSession); + UserSessionModel getUserSession(); + + String getNote(String name); + void setNote(String name, String value); + void removeNote(String name); + Map getNotes(); +} diff --git a/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java index 84fa64e1c1..12abb10903 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -20,14 +20,12 @@ package org.keycloak.models; import java.util.Map; import java.util.Set; +import org.keycloak.sessions.CommonClientSessionModel; + /** * @author Stian Thorgersen */ -public interface ClientSessionModel { - - public String getId(); - public RealmModel getRealm(); - public ClientModel getClient(); +public interface ClientSessionModel extends CommonClientSessionModel { public UserSessionModel getUserSession(); public void setUserSession(UserSessionModel userSession); @@ -35,41 +33,12 @@ public interface ClientSessionModel { public String getRedirectUri(); public void setRedirectUri(String uri); - public int getTimestamp(); - - public void setTimestamp(int timestamp); - - public String getAction(); - - public void setAction(String action); - - public Set getRoles(); - public void setRoles(Set roles); - - public Set getProtocolMappers(); - public void setProtocolMappers(Set protocolMappers); - public Map getExecutionStatus(); public void setExecutionStatus(String authenticator, ExecutionStatus status); public void clearExecutionStatus(); public UserModel getAuthenticatedUser(); public void setAuthenticatedUser(UserModel user); - - - /** - * Authentication request type, i.e. OAUTH, SAML 2.0, SAML 1.1, etc. - * - * @return - */ - public String getAuthMethod(); - public void setAuthMethod(String method); - - public String getNote(String name); - public void setNote(String name, String value); - public void removeNote(String name); - public Map getNotes(); - /** * Required actions that are attached to this client session. * @@ -103,28 +72,10 @@ public interface ClientSessionModel { public void clearUserSessionNotes(); - public static enum Action { - OAUTH_GRANT, - CODE_TO_TOKEN, - VERIFY_EMAIL, - UPDATE_PROFILE, - CONFIGURE_TOTP, - UPDATE_PASSWORD, - RECOVER_PASSWORD, // deprecated - AUTHENTICATE, - SOCIAL_CALLBACK, - LOGGED_OUT, - RESET_CREDENTIALS, - EXECUTE_ACTIONS, - REQUIRED_ACTIONS - } + public String getNote(String name); + public void setNote(String name, String value); + public void removeNote(String name); + public Map getNotes(); + - public enum ExecutionStatus { - FAILED, - SUCCESS, - SETUP_REQUIRED, - ATTEMPTED, - SKIPPED, - CHALLENGED - } } diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java index 766078d769..0348b68e6e 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakSession.java @@ -20,6 +20,7 @@ package org.keycloak.models; import org.keycloak.component.ComponentModel; import org.keycloak.models.cache.UserCache; import org.keycloak.provider.Provider; +import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.storage.federated.UserFederatedStorageProvider; import java.util.Set; @@ -102,6 +103,9 @@ public interface KeycloakSession { UserSessionProvider sessions(); + AuthenticationSessionProvider authenticationSessions(); + + void close(); diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index dc8bff5333..f6484d6f5b 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -191,6 +191,12 @@ public interface RealmModel extends RoleContainerModel { void setAccessCodeLifespanLogin(int seconds); + int getActionTokenGeneratedByAdminLifespan(); + void setActionTokenGeneratedByAdminLifespan(int seconds); + + int getActionTokenGeneratedByUserLifespan(); + void setActionTokenGeneratedByUserLifespan(int seconds); + List getRequiredCredentials(); void addRequiredCredential(String cred); diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java index d58c40522c..28a31457c1 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java @@ -17,7 +17,6 @@ package org.keycloak.models; -import java.util.List; import java.util.Map; /** @@ -53,7 +52,7 @@ public interface UserSessionModel { void setLastSessionRefresh(int seconds); - List getClientSessions(); + Map getAuthenticatedClientSessions(); public String getNote(String name); public void setNote(String name, String value); @@ -63,8 +62,12 @@ public interface UserSessionModel { State getState(); void setState(State state); + void setUser(UserModel user); + + // Will completely restart whole state of user session. It will just keep same ID. + void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId); + public static enum State { - LOGGING_IN, LOGGED_IN, LOGGING_OUT, LOGGED_OUT diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java index 4102de1f32..d474e89a71 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/UserSessionProvider.java @@ -27,11 +27,9 @@ import java.util.List; */ public interface UserSessionProvider extends Provider { - ClientSessionModel createClientSession(RealmModel realm, ClientModel client); - ClientSessionModel getClientSession(RealmModel realm, String id); - ClientSessionModel getClientSession(String id); + AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession); - UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId); + UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId); UserSessionModel getUserSession(RealmModel realm, String id); List getUserSessions(RealmModel realm, UserModel user); List getUserSessions(RealmModel realm, ClientModel client); @@ -40,13 +38,14 @@ public interface UserSessionProvider extends Provider { UserSessionModel getUserSessionByBrokerSessionId(RealmModel realm, String brokerSessionId); long getActiveUserSessions(RealmModel realm, ClientModel client); + + /** This will remove attached ClientLoginSessionModels too **/ void removeUserSession(RealmModel realm, UserSessionModel session); void removeUserSessions(RealmModel realm, UserModel user); - // Implementation should propagate removal of expired userSessions to userSessionPersister too + /** Implementation should propagate removal of expired userSessions to userSessionPersister too **/ void removeExpired(RealmModel realm); void removeUserSessions(RealmModel realm); - void removeClientSession(RealmModel realm, ClientSessionModel clientSession); UserLoginFailureModel getUserLoginFailure(RealmModel realm, String userId); UserLoginFailureModel addUserLoginFailure(RealmModel realm, String userId); @@ -56,25 +55,22 @@ public interface UserSessionProvider extends Provider { void onRealmRemoved(RealmModel realm); void onClientRemoved(RealmModel realm, ClientModel client); + /** Newly created userSession won't contain attached AuthenticatedClientSessions **/ UserSessionModel createOfflineUserSession(UserSessionModel userSession); UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId); - // Removes the attached clientSessions as well + /** Removes the attached clientSessions as well **/ void removeOfflineUserSession(RealmModel realm, UserSessionModel userSession); - ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession); - ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId); - List getOfflineClientSessions(RealmModel realm, UserModel user); - - // Don't remove userSession even if it's last userSession - void removeOfflineClientSession(RealmModel realm, String clientSessionId); + /** Will automatically attach newly created offline client session to the offlineUserSession **/ + AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession); + List getOfflineUserSessions(RealmModel realm, UserModel user); long getOfflineSessionsCount(RealmModel realm, ClientModel client); List getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max); - // Triggered by persister during pre-load - UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline); - ClientSessionModel importClientSession(ClientSessionModel persistentClientSession, boolean offline); + /** Triggered by persister during pre-load. It optionally imports authenticatedClientSessions too if requested. Otherwise the imported UserSession will have empty list of AuthenticationSessionModel **/ + UserSessionModel importUserSession(UserSessionModel persistentUserSession, boolean offline, boolean importAuthenticatedClientSessions); ClientInitialAccessModel createClientInitialAccessModel(RealmModel realm, int expiration, int count); ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id); diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java new file mode 100644 index 0000000000..8e84f1a445 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java @@ -0,0 +1,132 @@ +/* + * Copyright 2016 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.sessions; + +import java.util.Map; +import java.util.Set; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; + +/** + * Using class for now to avoid many updates among implementations + * + * @author Marek Posolda + */ +public interface AuthenticationSessionModel extends CommonClientSessionModel { + +// +// public UserSessionModel getUserSession(); +// public void setUserSession(UserSessionModel userSession); + + + Map getExecutionStatus(); + void setExecutionStatus(String authenticator, ExecutionStatus status); + void clearExecutionStatus(); + UserModel getAuthenticatedUser(); + void setAuthenticatedUser(UserModel user); + + /** + * Required actions that are attached to this client session. + * + * @return + */ + Set getRequiredActions(); + + void addRequiredAction(String action); + + void removeRequiredAction(String action); + + void addRequiredAction(UserModel.RequiredAction action); + + void removeRequiredAction(UserModel.RequiredAction action); + + + /** + * Sets the given user session note to the given value. User session notes are notes + * you want be applied to the UserSessionModel when the client session is attached to it. + */ + void setUserSessionNote(String name, String value); + /** + * Retrieves value of given user session note. User session notes are notes + * you want be applied to the UserSessionModel when the client session is attached to it. + */ + Map getUserSessionNotes(); + /** + * Clears all user session notes. User session notes are notes + * you want be applied to the UserSessionModel when the client session is attached to it. + */ + void clearUserSessionNotes(); + + /** + * Retrieves value of the given authentication note to the given value. Authentication notes are notes + * used typically by authenticators and authentication flows. They are cleared when + * authentication session is restarted + */ + String getAuthNote(String name); + /** + * Sets the given authentication note to the given value. Authentication notes are notes + * used typically by authenticators and authentication flows. They are cleared when + * authentication session is restarted + */ + void setAuthNote(String name, String value); + /** + * Removes the given authentication note. Authentication notes are notes + * used typically by authenticators and authentication flows. They are cleared when + * authentication session is restarted + */ + void removeAuthNote(String name); + /** + * Clears all authentication note. Authentication notes are notes + * used typically by authenticators and authentication flows. They are cleared when + * authentication session is restarted + */ + void clearAuthNotes(); + + /** + * Retrieves value of the given client note to the given value. Client notes are notes + * specific to client protocol. They are NOT cleared when authentication session is restarted. + */ + String getClientNote(String name); + /** + * Sets the given client note to the given value. Client notes are notes + * specific to client protocol. They are NOT cleared when authentication session is restarted. + */ + void setClientNote(String name, String value); + /** + * Removes the given client note. Client notes are notes + * specific to client protocol. They are NOT cleared when authentication session is restarted. + */ + void removeClientNote(String name); + /** + * Retrieves the (name, value) map of client notes. Client notes are notes + * specific to client protocol. They are NOT cleared when authentication session is restarted. + */ + Map getClientNotes(); + /** + * Clears all client notes. Client notes are notes + * specific to client protocol. They are NOT cleared when authentication session is restarted. + */ + void clearClientNotes(); + + void updateClient(ClientModel client); + + // Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm and client. + void restartSession(RealmModel realm, ClientModel client); +} diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java new file mode 100644 index 0000000000..99806d45b2 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 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.sessions; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.provider.Provider; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public interface AuthenticationSessionProvider extends Provider { + + // Generates random ID + AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client); + + AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client); + + AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId); + + void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession); + + void removeExpired(RealmModel realm); + void onRealmRemoved(RealmModel realm); + void onClientRemoved(RealmModel realm, ClientModel client); + + /** + * Requests update of authNotes of an authentication session that is not owned + * by this instance but might exist somewhere in the cluster. + * + * @param authSessionId + * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}. + */ + void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment); + + +} diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java new file mode 100644 index 0000000000..c47a6a5ea2 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016 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.sessions; + +import java.util.Map; +import java.util.Set; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; + +/** + * Predecesor of AuthenticationSessionModel, ClientLoginSessionModel and ClientSessionModel (then action tickets). Maybe we will remove it later... + * + * @author Marek Posolda + */ +public interface CommonClientSessionModel { + + public String getRedirectUri(); + public void setRedirectUri(String uri); + + public String getId(); + public RealmModel getRealm(); + public ClientModel getClient(); + + public int getTimestamp(); + public void setTimestamp(int timestamp); + + public String getAction(); + public void setAction(String action); + + public String getProtocol(); + public void setProtocol(String method); + + // TODO: Not needed here...? + public Set getRoles(); + public void setRoles(Set roles); + + // TODO: Not needed here...? + public Set getProtocolMappers(); + public void setProtocolMappers(Set protocolMappers); + + public static enum Action { + OAUTH_GRANT, + CODE_TO_TOKEN, + AUTHENTICATE, + LOGGED_OUT, + REQUIRED_ACTIONS + } + + public enum ExecutionStatus { + FAILED, + SUCCESS, + SETUP_REQUIRED, + ATTEMPTED, + SKIPPED, + CHALLENGED + } +} diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index a34e4ee914..242709195f 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -31,8 +31,8 @@ import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -43,13 +43,18 @@ import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol.Error; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.services.ErrorPage; +import org.keycloak.services.ErrorPageException; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.CacheControlUtil; +import org.keycloak.services.util.AuthenticationFlowURLHelper; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.CommonClientSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -64,10 +69,17 @@ import java.util.Map; */ public class AuthenticationProcessor { public static final String CURRENT_AUTHENTICATION_EXECUTION = "current.authentication.execution"; + public static final String LAST_PROCESSED_EXECUTION = "last.processed.execution"; + public static final String CURRENT_FLOW_PATH = "current.flow.path"; + public static final String FORKED_FROM = "forked.from"; + + public static final String BROKER_SESSION_ID = "broker.session.id"; + public static final String BROKER_USER_ID = "broker.user.id"; + protected static final Logger logger = Logger.getLogger(AuthenticationProcessor.class); protected RealmModel realm; protected UserSessionModel userSession; - protected ClientSessionModel clientSession; + protected AuthenticationSessionModel authenticationSession; protected ClientConnection connection; protected UriInfo uriInfo; protected KeycloakSession session; @@ -77,7 +89,7 @@ public class AuthenticationProcessor { protected String flowPath; protected boolean browserFlow; protected BruteForceProtector protector; - protected boolean oneActionWasSuccessful; + protected Runnable afterResetListener; /** * This could be an error message forwarded from another authenticator */ @@ -87,7 +99,6 @@ public class AuthenticationProcessor { * This could be an success message forwarded from another authenticator */ protected FormMessage forwardedSuccessMessage; - protected boolean userSessionCreated; // Used for client authentication protected ClientModel client; @@ -128,8 +139,8 @@ public class AuthenticationProcessor { return clientAuthAttributes; } - public ClientSessionModel getClientSession() { - return clientSession; + public AuthenticationSessionModel getAuthenticationSession() { + return authenticationSession; } public ClientConnection getConnection() { @@ -148,17 +159,13 @@ public class AuthenticationProcessor { return userSession; } - public boolean isUserSessionCreated() { - return userSessionCreated; - } - public AuthenticationProcessor setRealm(RealmModel realm) { this.realm = realm; return this; } - public AuthenticationProcessor setClientSession(ClientSessionModel clientSession) { - this.clientSession = clientSession; + public AuthenticationProcessor setAuthenticationSession(AuthenticationSessionModel authenticationSession) { + this.authenticationSession = authenticationSession; return this; } @@ -213,8 +220,8 @@ public class AuthenticationProcessor { } public String generateCode() { - ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getClientSession()); - clientSession.setTimestamp(Time.currentTime()); + ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession()); + authenticationSession.setTimestamp(Time.currentTime()); return accessCode.getCode(); } @@ -232,15 +239,15 @@ public class AuthenticationProcessor { } public void setAutheticatedUser(UserModel user) { - UserModel previousUser = clientSession.getAuthenticatedUser(); + UserModel previousUser = getAuthenticationSession().getAuthenticatedUser(); if (previousUser != null && !user.getId().equals(previousUser.getId())) throw new AuthenticationFlowException(AuthenticationFlowError.USER_CONFLICT); validateUser(user); - getClientSession().setAuthenticatedUser(user); + getAuthenticationSession().setAuthenticatedUser(user); } public void clearAuthenticatedUser() { - getClientSession().setAuthenticatedUser(null); + getAuthenticationSession().setAuthenticatedUser(null); } public class Result implements AuthenticationFlowContext, ClientAuthenticationFlowContext { @@ -363,7 +370,7 @@ public class AuthenticationProcessor { @Override public UserModel getUser() { - return getClientSession().getAuthenticatedUser(); + return getAuthenticationSession().getAuthenticatedUser(); } @Override @@ -397,8 +404,8 @@ public class AuthenticationProcessor { } @Override - public ClientSessionModel getClientSession() { - return AuthenticationProcessor.this.getClientSession(); + public AuthenticationSessionModel getAuthenticationSession() { + return AuthenticationProcessor.this.getAuthenticationSession(); } @Override @@ -483,19 +490,30 @@ public class AuthenticationProcessor { } @Override - public URI getActionUrl() { - return getActionUrl(generateAccessCode()); + public URI getActionTokenUrl(String tokenString) { + return LoginActionsService.actionTokenProcessor(getUriInfo()) + .queryParam("key", tokenString) + .queryParam("execution", getExecution().getId()) + .build(getRealm().getName()); + } + + @Override + public URI getRefreshExecutionUrl() { + return LoginActionsService.loginActionsBaseUrl(getUriInfo()) + .path(AuthenticationProcessor.this.flowPath) + .queryParam("execution", getExecution().getId()) + .build(getRealm().getName()); } @Override public void cancelLogin() { getEvent().error(Errors.REJECTED_BY_USER); - LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod()); + LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getAuthenticationSession().getProtocol()); protocol.setRealm(getRealm()) .setHttpHeaders(getHttpRequest().getHttpHeaders()) .setUriInfo(getUriInfo()) .setEventBuilder(event); - Response response = protocol.sendError(getClientSession(), Error.CANCELLED_BY_USER); + Response response = protocol.sendError(getAuthenticationSession(), Error.CANCELLED_BY_USER); forceChallenge(response); } @@ -504,6 +522,12 @@ public class AuthenticationProcessor { this.status = FlowStatus.FLOW_RESET; } + @Override + public void resetFlow(Runnable afterResetListener) { + this.status = FlowStatus.FLOW_RESET; + AuthenticationProcessor.this.afterResetListener = afterResetListener; + } + @Override public void fork() { this.status = FlowStatus.FORK; @@ -539,7 +563,7 @@ public class AuthenticationProcessor { public void logFailure() { if (realm.isBruteForceProtected()) { - String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); + String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); // todo need to handle non form failures if (username == null) { @@ -554,7 +578,7 @@ public class AuthenticationProcessor { protected void logSuccess() { if (realm.isBruteForceProtected()) { - String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); + String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); // TODO: as above, need to handle non form success if(username == null) { @@ -569,9 +593,9 @@ public class AuthenticationProcessor { } public boolean isSuccessful(AuthenticationExecutionModel model) { - ClientSessionModel.ExecutionStatus status = clientSession.getExecutionStatus().get(model.getId()); + AuthenticationSessionModel.ExecutionStatus status = authenticationSession.getExecutionStatus().get(model.getId()); if (status == null) return false; - return status == ClientSessionModel.ExecutionStatus.SUCCESS; + return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS; } public Response handleBrowserException(Exception failure) { @@ -602,10 +626,12 @@ public class AuthenticationProcessor { } else if (e.getError() == AuthenticationFlowError.FORK_FLOW) { ForkFlowException reset = (ForkFlowException)e; - ClientSessionModel clone = clone(session, clientSession); - clone.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + AuthenticationSessionModel clone = clone(session, authenticationSession); + clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); + setAuthenticationSession(clone); + AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clone) + processor.setAuthenticationSession(clone) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH) .setFlowId(realm.getBrowserFlow().getId()) .setForwardedErrorMessage(reset.getErrorMessage()) @@ -698,47 +724,50 @@ public class AuthenticationProcessor { public Response redirectToFlow() { - String code = generateCode(); + URI redirect = new AuthenticationFlowURLHelper(session, realm, uriInfo).getLastExecutionUrl(authenticationSession); + + logger.debug("Redirecting to URL: " + redirect.toString()); - URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo()) - .path(flowPath) - .queryParam(OAuth2Constants.CODE, code).build(getRealm().getName()); return Response.status(302).location(redirect).build(); } - public static Response redirectToRequiredActions(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, UriInfo uriInfo) { + public void resetFlow() { + resetFlow(authenticationSession, flowPath); - // redirect to non-action url so browser refresh button works without reposting past data - ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); - accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name()); - clientSession.setTimestamp(Time.currentTime()); - - URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo) - .path(LoginActionsService.REQUIRED_ACTION) - .queryParam(OAuth2Constants.CODE, accessCode.getCode()).build(realm.getName()); - return Response.status(302).location(redirect).build(); - - } - - public static void resetFlow(ClientSessionModel clientSession) { - logger.debug("RESET FLOW"); - clientSession.setTimestamp(Time.currentTime()); - clientSession.setAuthenticatedUser(null); - clientSession.clearExecutionStatus(); - clientSession.clearUserSessionNotes(); - clientSession.removeNote(CURRENT_AUTHENTICATION_EXECUTION); - } - - public static ClientSessionModel clone(KeycloakSession session, ClientSessionModel clientSession) { - ClientSessionModel clone = session.sessions().createClientSession(clientSession.getRealm(), clientSession.getClient()); - for (Map.Entry entry : clientSession.getNotes().entrySet()) { - clone.setNote(entry.getKey(), entry.getValue()); + if (afterResetListener != null) { + afterResetListener.run(); } - clone.setRedirectUri(clientSession.getRedirectUri()); - clone.setAuthMethod(clientSession.getAuthMethod()); + } + + public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) { + logger.debug("RESET FLOW"); + authSession.setTimestamp(Time.currentTime()); + authSession.setAuthenticatedUser(null); + authSession.clearExecutionStatus(); + authSession.clearUserSessionNotes(); + authSession.clearAuthNotes(); + + authSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name()); + + authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath); + } + + public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) { + AuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true); + + // Transfer just the client "notes", but not "authNotes" + for (Map.Entry entry : authSession.getClientNotes().entrySet()) { + clone.setClientNote(entry.getKey(), entry.getValue()); + } + + clone.setRedirectUri(authSession.getRedirectUri()); + clone.setProtocol(authSession.getProtocol()); clone.setTimestamp(Time.currentTime()); - clone.removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + + clone.setAuthNote(FORKED_FROM, authSession.getId()); + logger.debugf("Forked authSession %s from authSession %s", clone.getId(), authSession.getId()); + return clone; } @@ -746,27 +775,25 @@ public class AuthenticationProcessor { public Response authenticationAction(String execution) { logger.debug("authenticationAction"); - checkClientSession(); - String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION); - if (!execution.equals(current)) { + checkClientSession(true); + String current = authenticationSession.getAuthNote(CURRENT_AUTHENTICATION_EXECUTION); + if (execution == null || !execution.equals(current)) { logger.debug("Current execution does not equal executed execution. Might be a page refresh"); - //logFailure(); - //resetFlow(clientSession); - return authenticate(); + return new AuthenticationFlowURLHelper(session, realm, uriInfo).showPageExpired(authenticationSession); } - UserModel authUser = clientSession.getAuthenticatedUser(); + UserModel authUser = authenticationSession.getAuthenticatedUser(); validateUser(authUser); AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution); if (model == null) { logger.debug("Cannot find execution, reseting flow"); logFailure(); - resetFlow(clientSession); + resetFlow(); return authenticate(); } - event.client(clientSession.getClient().getClientId()) - .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) - .detail(Details.AUTH_METHOD, clientSession.getAuthMethod()); - String authType = clientSession.getNote(Details.AUTH_TYPE); + event.client(authenticationSession.getClient().getClientId()) + .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri()) + .detail(Details.AUTH_METHOD, authenticationSession.getProtocol()); + String authType = authenticationSession.getAuthNote(Details.AUTH_TYPE); if (authType != null) { event.detail(Details.AUTH_TYPE, authType); } @@ -774,95 +801,113 @@ public class AuthenticationProcessor { AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, model); Response challenge = authenticationFlow.processAction(execution); if (challenge != null) return challenge; - if (clientSession.getAuthenticatedUser() == null) { + if (authenticationSession.getAuthenticatedUser() == null) { throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER); } return authenticationComplete(); } - public void checkClientSession() { - ClientSessionCode code = new ClientSessionCode(session, realm, clientSession); - String action = ClientSessionModel.Action.AUTHENTICATE.name(); - if (!code.isValidAction(action)) { - throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_SESSION); + private void checkClientSession(boolean checkAction) { + ClientSessionCode code = new ClientSessionCode(session, realm, authenticationSession); + + if (checkAction) { + String action = AuthenticationSessionModel.Action.AUTHENTICATE.name(); + if (!code.isValidAction(action)) { + throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CLIENT_SESSION); + } } if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) { throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE); } - clientSession.setTimestamp(Time.currentTime()); + authenticationSession.setTimestamp(Time.currentTime()); } public Response authenticateOnly() throws AuthenticationFlowException { logger.debug("AUTHENTICATE ONLY"); - checkClientSession(); - event.client(clientSession.getClient().getClientId()) - .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) - .detail(Details.AUTH_METHOD, clientSession.getAuthMethod()); - String authType = clientSession.getNote(Details.AUTH_TYPE); + checkClientSession(false); + event.client(authenticationSession.getClient().getClientId()) + .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri()) + .detail(Details.AUTH_METHOD, authenticationSession.getProtocol()); + String authType = authenticationSession.getAuthNote(Details.AUTH_TYPE); if (authType != null) { event.detail(Details.AUTH_TYPE, authType); } - UserModel authUser = clientSession.getAuthenticatedUser(); + UserModel authUser = authenticationSession.getAuthenticatedUser(); validateUser(authUser); AuthenticationFlow authenticationFlow = createFlowExecution(this.flowId, null); Response challenge = authenticationFlow.processFlow(); if (challenge != null) return challenge; - if (clientSession.getAuthenticatedUser() == null) { + if (authenticationSession.getAuthenticatedUser() == null) { throw new AuthenticationFlowException(AuthenticationFlowError.UNKNOWN_USER); } return challenge; } - /** - * Marks that at least one action was successful - * - */ - public void setActionSuccessful() { -// oneActionWasSuccessful = true; - } + // May create userSession too + public AuthenticatedClientSessionModel attachSession() { + AuthenticatedClientSessionModel clientSession = attachSession(authenticationSession, userSession, session, realm, connection, event); - public Response checkWasSuccessfulBrowserAction() { - if (oneActionWasSuccessful && isBrowserFlow()) { - // redirect to non-action url so browser refresh button works without reposting past data - String code = generateCode(); - - URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo()) - .path(flowPath) - .queryParam(OAuth2Constants.CODE, code).build(getRealm().getName()); - return Response.status(302).location(redirect).build(); - } else { - return null; + if (userSession == null) { + userSession = clientSession.getUserSession(); } + + return clientSession; } - public void attachSession() { - String username = clientSession.getAuthenticatedUser().getUsername(); - String attemptedUsername = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); + // May create new userSession too (if userSession argument is null) + public static AuthenticatedClientSessionModel attachSession(AuthenticationSessionModel authSession, UserSessionModel userSession, KeycloakSession session, RealmModel realm, ClientConnection connection, EventBuilder event) { + String username = authSession.getAuthenticatedUser().getUsername(); + String attemptedUsername = authSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); if (attemptedUsername != null) username = attemptedUsername; - String rememberMe = clientSession.getNote(Details.REMEMBER_ME); + String rememberMe = authSession.getAuthNote(Details.REMEMBER_ME); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true"); + String brokerSessionId = authSession.getAuthNote(BROKER_SESSION_ID); + String brokerUserId = authSession.getAuthNote(BROKER_USER_ID); + if (userSession == null) { // if no authenticator attached a usersession - userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), clientSession.getAuthMethod(), remember, null, null); - userSession.setState(UserSessionModel.State.LOGGING_IN); - userSessionCreated = true; + + userSession = session.sessions().getUserSession(realm, authSession.getId()); + if (userSession == null) { + userSession = session.sessions().createUserSession(authSession.getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol() + , remember, brokerSessionId, brokerUserId); + } else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) { + userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol() + , remember, brokerSessionId, brokerUserId); + } else { + // We have existing userSession even if it wasn't attached to authenticator. Could happen if SSO authentication was ignored (eg. prompt=login) and in some other cases. + // We need to handle case when different user was used + logger.debugf("No SSO login, but found existing userSession with ID '%s' after finished authentication.", userSession.getId()); + if (!authSession.getAuthenticatedUser().equals(userSession.getUser())) { + event.detail(Details.EXISTING_USER, userSession.getUser().getId()); + event.error(Errors.DIFFERENT_USER_AUTHENTICATED); + throw new ErrorPageException(session, Messages.DIFFERENT_USER_AUTHENTICATED, userSession.getUser().getUsername()); + } + } + userSession.setState(UserSessionModel.State.LOGGED_IN); } + if (remember) { event.detail(Details.REMEMBER_ME, "true"); } - TokenManager.attachClientSession(userSession, clientSession); + + AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession); + event.user(userSession.getUser()) .detail(Details.USERNAME, username) .session(userSession); + + return clientSession; } public void evaluateRequiredActionTriggers() { - AuthenticationManager.evaluateRequiredActionTriggers(session, userSession, clientSession, connection, request, uriInfo, event, realm, clientSession.getAuthenticatedUser()); + AuthenticationManager.evaluateRequiredActionTriggers(session, authenticationSession, connection, request, uriInfo, event, realm, authenticationSession.getAuthenticatedUser()); } public Response finishAuthentication(LoginProtocol protocol) { event.success(); - RealmModel realm = clientSession.getRealm(); - return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection, event, protocol); + RealmModel realm = authenticationSession.getRealm(); + AuthenticatedClientSessionModel clientSession = attachSession(); + return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession,clientSession, request, uriInfo, connection, event, protocol); } @@ -877,19 +922,22 @@ public class AuthenticationProcessor { } protected Response authenticationComplete() { - attachSession(); - if (isActionRequired()) { - return redirectToRequiredActions(session, realm, clientSession, uriInfo); + // attachSession(); // Session will be attached after requiredActions + consents are finished. + AuthenticationManager.setRolesAndMappersInSession(authenticationSession); + + String nextRequiredAction = nextRequiredAction(); + if (nextRequiredAction != null) { + return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction); } else { - event.detail(Details.CODE_ID, clientSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set + event.detail(Details.CODE_ID, authenticationSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set // the user has successfully logged in and we can clear his/her previous login failure attempts. logSuccess(); - return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, connection, request, uriInfo, event); + return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event); } } - public boolean isActionRequired() { - return AuthenticationManager.isActionRequired(session, userSession, clientSession, connection, request, uriInfo, event); + public String nextRequiredAction() { + return AuthenticationManager.nextRequiredAction(session, authenticationSession, connection, request, uriInfo, event); } public AuthenticationProcessor.Result createAuthenticatorContext(AuthenticationExecutionModel model, Authenticator authenticator, List executions) { diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index 40433cfee7..3a9c53cf2a 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -20,9 +20,9 @@ package org.keycloak.authentication; import org.jboss.logging.Logger; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.UserModel; import org.keycloak.services.ServicesLogger; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Response; import java.util.Iterator; @@ -51,11 +51,11 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { protected boolean isProcessed(AuthenticationExecutionModel model) { if (model.isDisabled()) return true; - ClientSessionModel.ExecutionStatus status = processor.getClientSession().getExecutionStatus().get(model.getId()); + AuthenticationSessionModel.ExecutionStatus status = processor.getAuthenticationSession().getExecutionStatus().get(model.getId()); if (status == null) return false; - return status == ClientSessionModel.ExecutionStatus.SUCCESS || status == ClientSessionModel.ExecutionStatus.SKIPPED - || status == ClientSessionModel.ExecutionStatus.ATTEMPTED - || status == ClientSessionModel.ExecutionStatus.SETUP_REQUIRED; + return status == AuthenticationSessionModel.ExecutionStatus.SUCCESS || status == AuthenticationSessionModel.ExecutionStatus.SKIPPED + || status == AuthenticationSessionModel.ExecutionStatus.ATTEMPTED + || status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED; } @@ -75,7 +75,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); Response flowChallenge = authenticationFlow.processAction(actionExecution); if (flowChallenge == null) { - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); if (model.isAlternative()) alternativeSuccessful = true; return processFlow(); } else { @@ -90,13 +90,9 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions); logger.debugv("action: {0}", model.getAuthenticator()); authenticator.action(result); - Response response = processResult(result); + Response response = processResult(result, true); if (response == null) { - processor.getClientSession().removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); - if (result.status == FlowStatus.SUCCESS) { - // we do this so that flow can redirect to a non-action URL - processor.setActionSuccessful(); - } + processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); return processFlow(); } else return response; } @@ -119,7 +115,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } if (model.isAlternative() && alternativeSuccessful) { logger.debug("Skip alternative execution"); - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } if (model.isAuthenticatorFlow()) { @@ -127,7 +123,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { AuthenticationFlow authenticationFlow = processor.createFlowExecution(model.getFlowId(), model); Response flowChallenge = authenticationFlow.processFlow(); if (flowChallenge == null) { - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); if (model.isAlternative()) alternativeSuccessful = true; continue; } else { @@ -135,13 +131,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { alternativeChallenge = flowChallenge; challengedAlternativeExecution = model; } else if (model.isRequired()) { - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); return flowChallenge; } else if (model.isOptional()) { - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } else { - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } return flowChallenge; @@ -154,11 +150,11 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } Authenticator authenticator = factory.create(processor.getSession()); logger.debugv("authenticator: {0}", factory.getId()); - UserModel authUser = processor.getClientSession().getAuthenticatedUser(); + UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser(); if (authenticator.requiresUser() && authUser == null) { if (alternativeChallenge != null) { - processor.getClientSession().setExecutionStatus(challengedAlternativeExecution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + processor.getAuthenticationSession().setExecutionStatus(challengedAlternativeExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); return alternativeChallenge; } throw new AuthenticationFlowException("authenticator: " + factory.getId(), AuthenticationFlowError.UNKNOWN_USER); @@ -170,88 +166,88 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { if (model.isRequired()) { if (factory.isUserSetupAllowed()) { logger.debugv("authenticator SETUP_REQUIRED: {0}", factory.getId()); - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SETUP_REQUIRED); - authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getClientSession().getAuthenticatedUser()); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED); + authenticator.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser()); continue; } else { throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED); } } else if (model.isOptional()) { - processor.getClientSession().setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + processor.getAuthenticationSession().setExecutionStatus(model.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } } } // skip if action as successful already - Response redirect = processor.checkWasSuccessfulBrowserAction(); - if (redirect != null) return redirect; +// Response redirect = processor.checkWasSuccessfulBrowserAction(); +// if (redirect != null) return redirect; AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions); logger.debug("invoke authenticator.authenticate"); authenticator.authenticate(context); - Response response = processResult(context); + Response response = processResult(context, false); if (response != null) return response; } return null; } - public Response processResult(AuthenticationProcessor.Result result) { + public Response processResult(AuthenticationProcessor.Result result, boolean isAction) { AuthenticationExecutionModel execution = result.getExecution(); FlowStatus status = result.getStatus(); switch (status) { case SUCCESS: logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); if (execution.isAlternative()) alternativeSuccessful = true; return null; case FAILED: logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); processor.logFailure(); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.FAILED); if (result.getChallenge() != null) { return sendChallenge(result, execution); } throw new AuthenticationFlowException(result.getError()); case FORK: logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); - processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); + processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); case FORCE_CHALLENGE: - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); case CHALLENGE: logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); if (execution.isRequired()) { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); } - UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser(); + UserModel authenticatedUser = processor.getAuthenticationSession().getAuthenticatedUser(); if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); } if (execution.isAlternative()) { alternativeChallenge = result.getChallenge(); challengedAlternativeExecution = execution; } else { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); } return null; case FAILURE_CHALLENGE: logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); processor.logFailure(); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); case ATTEMPTED: logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); } - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED); + processor.getAuthenticationSession().setExecutionStatus(execution.getId(), AuthenticationSessionModel.ExecutionStatus.ATTEMPTED); return null; case FLOW_RESET: - AuthenticationProcessor.resetFlow(processor.getClientSession()); + processor.resetFlow(); return processor.authenticate(); default: logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); @@ -261,7 +257,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { } public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) { - processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); + processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); return result.getChallenge(); } diff --git a/services/src/main/java/org/keycloak/authentication/ExplainedVerificationException.java b/services/src/main/java/org/keycloak/authentication/ExplainedVerificationException.java new file mode 100644 index 0000000000..7102588336 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/ExplainedVerificationException.java @@ -0,0 +1,51 @@ +/* + * 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.common.VerificationException; + +/** + * + * @author hmlnarik + */ +public class ExplainedVerificationException extends VerificationException { + private final String errorEvent; + + public ExplainedVerificationException(String errorEvent) { + super(); + this.errorEvent = errorEvent; + } + + public ExplainedVerificationException(String errorEvent, String message) { + super(message); + this.errorEvent = errorEvent; + } + + public ExplainedVerificationException(String errorEvent, String message, Throwable cause) { + super(message); + this.errorEvent = errorEvent; + } + + public ExplainedVerificationException(String errorEvent, Throwable cause) { + super(cause); + this.errorEvent = errorEvent; + } + + public String getErrorEvent() { + return errorEvent; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java index 59c85fb74a..955879f8ac 100755 --- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java @@ -24,12 +24,12 @@ import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -93,7 +93,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow { @Override public UserModel getUser() { - return getClientSession().getAuthenticatedUser(); + return getAuthenticationSession().getAuthenticatedUser(); } @Override @@ -107,8 +107,8 @@ public class FormAuthenticationFlow implements AuthenticationFlow { } @Override - public ClientSessionModel getClientSession() { - return processor.getClientSession(); + public AuthenticationSessionModel getAuthenticationSession() { + return processor.getAuthenticationSession(); } @Override @@ -166,19 +166,19 @@ public class FormAuthenticationFlow implements AuthenticationFlow { if (!actionExecution.equals(formExecution.getId())) { throw new AuthenticationFlowException("action is not current execution", AuthenticationFlowError.INTERNAL_ERROR); } - Map executionStatus = new HashMap<>(); + Map executionStatus = new HashMap<>(); List requiredActions = new LinkedList<>(); List successes = new LinkedList<>(); List errors = new LinkedList<>(); for (AuthenticationExecutionModel formActionExecution : formActionExecutions) { if (!formActionExecution.isEnabled()) { - executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } FormActionFactory factory = (FormActionFactory)processor.getSession().getKeycloakSessionFactory().getProviderFactory(FormAction.class, formActionExecution.getAuthenticator()); FormAction action = factory.create(processor.getSession()); - UserModel authUser = processor.getClientSession().getAuthenticatedUser(); + UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser(); if (action.requiresUser() && authUser == null) { throw new AuthenticationFlowException("form action: " + formExecution.getAuthenticator() + " requires user", AuthenticationFlowError.UNKNOWN_USER); } @@ -189,14 +189,14 @@ public class FormAuthenticationFlow implements AuthenticationFlow { if (formActionExecution.isRequired()) { if (factory.isUserSetupAllowed()) { AuthenticationProcessor.logger.debugv("authenticator SETUP_REQUIRED: {0}", formExecution.getAuthenticator()); - executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SETUP_REQUIRED); + executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED); requiredActions.add(action); continue; } else { throw new AuthenticationFlowException(AuthenticationFlowError.CREDENTIAL_SETUP_REQUIRED); } } else if (formActionExecution.isOptional()) { - executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SKIPPED); continue; } } @@ -205,10 +205,10 @@ public class FormAuthenticationFlow implements AuthenticationFlow { ValidationContextImpl result = new ValidationContextImpl(formActionExecution, action); action.validate(result); if (result.success) { - executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); + executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.SUCCESS); successes.add(result); } else { - executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + executionStatus.put(formActionExecution.getId(), AuthenticationSessionModel.ExecutionStatus.CHALLENGED); errors.add(result); } } @@ -234,16 +234,15 @@ public class FormAuthenticationFlow implements AuthenticationFlow { context.action.success(context); } // set status and required actions only if form is fully successful - for (Map.Entry entry : executionStatus.entrySet()) { - processor.getClientSession().setExecutionStatus(entry.getKey(), entry.getValue()); + for (Map.Entry entry : executionStatus.entrySet()) { + processor.getAuthenticationSession().setExecutionStatus(entry.getKey(), entry.getValue()); } for (FormAction action : requiredActions) { - action.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getClientSession().getAuthenticatedUser()); + action.setRequiredActions(processor.getSession(), processor.getRealm(), processor.getAuthenticationSession().getAuthenticatedUser()); } - processor.getClientSession().setExecutionStatus(actionExecution, ClientSessionModel.ExecutionStatus.SUCCESS); - processor.getClientSession().removeNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); - processor.setActionSuccessful(); + processor.getAuthenticationSession().setExecutionStatus(actionExecution, AuthenticationSessionModel.ExecutionStatus.SUCCESS); + processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); return null; } @@ -262,7 +261,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow { public Response renderForm(MultivaluedMap formData, List errors) { String executionId = formExecution.getId(); - processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, executionId); + processor.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, executionId); String code = processor.generateCode(); URI actionUrl = getActionUrl(executionId, code); LoginFormsProvider form = processor.getSession().getProvider(LoginFormsProvider.class) diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index 8f830d1e40..87b3403b85 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -23,13 +23,12 @@ import org.keycloak.common.ClientConnection; import org.keycloak.common.util.Time; import org.keycloak.events.EventBuilder; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -40,8 +39,7 @@ import java.net.URI; * @version $Revision: 1 $ */ public class RequiredActionContextResult implements RequiredActionContext { - protected UserSessionModel userSession; - protected ClientSessionModel clientSession; + protected AuthenticationSessionModel authenticationSession; protected RealmModel realm; protected EventBuilder eventBuilder; protected KeycloakSession session; @@ -51,12 +49,11 @@ public class RequiredActionContextResult implements RequiredActionContext { protected UserModel user; protected RequiredActionFactory factory; - public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession, + public RequiredActionContextResult(AuthenticationSessionModel authSession, RealmModel realm, EventBuilder eventBuilder, KeycloakSession session, HttpRequest httpRequest, UserModel user, RequiredActionFactory factory) { - this.userSession = userSession; - this.clientSession = clientSession; + this.authenticationSession = authSession; this.realm = realm; this.eventBuilder = eventBuilder; this.session = session; @@ -81,13 +78,8 @@ public class RequiredActionContextResult implements RequiredActionContext { } @Override - public ClientSessionModel getClientSession() { - return clientSession; - } - - @Override - public UserSessionModel getUserSession() { - return userSession; + public AuthenticationSessionModel getAuthenticationSession() { + return authenticationSession; } @Override @@ -142,14 +134,14 @@ public class RequiredActionContextResult implements RequiredActionContext { public URI getActionUrl(String code) { return LoginActionsService.requiredActionProcessor(getUriInfo()) .queryParam(OAuth2Constants.CODE, code) - .queryParam("action", factory.getId()) + .queryParam("execution", factory.getId()) .build(getRealm().getName()); } @Override public String generateCode() { - ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getClientSession()); - clientSession.setTimestamp(Time.currentTime()); + ClientSessionCode accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession()); + authenticationSession.setTimestamp(Time.currentTime()); return accessCode.getCode(); } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java new file mode 100644 index 0000000000..52d94d95f7 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java @@ -0,0 +1,104 @@ +/* + * 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.actiontoken; + +import org.keycloak.Config.Scope; +import org.keycloak.events.EventType; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.sessions.AuthenticationSessionModel; + +/** + * + * @author hmlnarik + */ +public abstract class AbstractActionTokenHander implements ActionTokenHandler, ActionTokenHandlerFactory { + + private final String id; + private final Class tokenClass; + private final String defaultErrorMessage; + private final EventType defaultEventType; + private final String defaultEventError; + + public AbstractActionTokenHander(String id, Class tokenClass, String defaultErrorMessage, EventType defaultEventType, String defaultEventError) { + this.id = id; + this.tokenClass = tokenClass; + this.defaultErrorMessage = defaultErrorMessage; + this.defaultEventType = defaultEventType; + this.defaultEventError = defaultEventError; + } + + @Override + public ActionTokenHandler create(KeycloakSession session) { + return this; + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public String getId() { + return this.id; + } + + @Override + public void close() { + } + + @Override + public Class getTokenClass() { + return this.tokenClass; + } + + @Override + public EventType eventType() { + return this.defaultEventType; + } + + @Override + public String getDefaultErrorMessage() { + return this.defaultErrorMessage; + } + + @Override + public String getDefaultEventError() { + return this.defaultEventError; + } + + @Override + public String getAuthenticationSessionIdFromToken(T token) { + return token == null ? null : token.getAuthenticationSessionId(); + } + + @Override + public AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext tokenContext) { + AuthenticationSessionModel authSession = tokenContext.createAuthenticationSessionForClient(token.getIssuedFor()); + authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); + return authSession; + } + + @Override + public boolean canUseTokenRepeatedly(T token, ActionTokenContext tokenContext) { + return true; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java new file mode 100644 index 0000000000..a55c5870f7 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java @@ -0,0 +1,163 @@ +/* + * 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.actiontoken; + +import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.common.ClientConnection; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.*; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.sessions.AuthenticationSessionModel; +import java.util.function.Function; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilderException; +import javax.ws.rs.core.UriInfo; +import org.jboss.resteasy.spi.HttpRequest; + +/** + * + * @author hmlnarik + */ +public class ActionTokenContext { + + @FunctionalInterface + public interface ProcessAuthenticateFlow { + Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor); + }; + + @FunctionalInterface + public interface ProcessBrokerFlow { + Response brokerLoginFlow(String code, String execution, String flowPath); + }; + + private final KeycloakSession session; + private final RealmModel realm; + private final UriInfo uriInfo; + private final ClientConnection clientConnection; + private final HttpRequest request; + private EventBuilder event; + private final ActionTokenHandler handler; + private AuthenticationSessionModel authenticationSession; + private boolean authenticationSessionFresh; + private String executionId; + private final ProcessAuthenticateFlow processAuthenticateFlow; + private final ProcessBrokerFlow processBrokerFlow; + + public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo, + ClientConnection clientConnection, HttpRequest request, + EventBuilder event, ActionTokenHandler handler, String executionId, + ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) { + this.session = session; + this.realm = realm; + this.uriInfo = uriInfo; + this.clientConnection = clientConnection; + this.request = request; + this.event = event; + this.handler = handler; + this.executionId = executionId; + this.processAuthenticateFlow = processFlow; + this.processBrokerFlow = processBrokerFlow; + } + + public EventBuilder getEvent() { + return event; + } + + public void setEvent(EventBuilder event) { + this.event = event; + } + + public KeycloakSession getSession() { + return session; + } + + public RealmModel getRealm() { + return realm; + } + + public UriInfo getUriInfo() { + return uriInfo; + } + + public ClientConnection getClientConnection() { + return clientConnection; + } + + public HttpRequest getRequest() { + return request; + } + + public AuthenticationSessionModel createAuthenticationSessionForClient(String clientId) + throws UriBuilderException, IllegalArgumentException { + AuthenticationSessionModel authSession; + + // set up the account service as the endpoint to call. + ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId); + + authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true); + authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); + authSession.setRedirectUri(redirectUri); + authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); + authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); + authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + + return authSession; + } + + public boolean isAuthenticationSessionFresh() { + return authenticationSessionFresh; + } + + public AuthenticationSessionModel getAuthenticationSession() { + return authenticationSession; + } + + public void setAuthenticationSession(AuthenticationSessionModel authenticationSession, boolean isFresh) { + this.authenticationSession = authenticationSession; + this.authenticationSessionFresh = isFresh; + if (this.event != null) { + ClientModel client = authenticationSession == null ? null : authenticationSession.getClient(); + this.event.client((String) (client == null ? null : client.getClientId())); + } + } + + public ActionTokenHandler getHandler() { + return handler; + } + + public String getExecutionId() { + return executionId; + } + + public void setExecutionId(String executionId) { + this.executionId = executionId; + } + + public Response processFlow(boolean action, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) { + return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor); + } + + public Response brokerFlow(String code, String flowPath) { + return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), flowPath); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java new file mode 100644 index 0000000000..f8d02d3468 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java @@ -0,0 +1,104 @@ +/* + * 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.actiontoken; + +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.provider.Provider; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.sessions.AuthenticationSessionModel; +import javax.ws.rs.core.Response; + +/** + * Handler of the action token. + * + * @param Class implementing the action token + * + * @author hmlnarik + */ +public interface ActionTokenHandler extends Provider { + + /** + * Performs the action as per the token details. This method is only called if all verifiers + * returned in {@link #handleToken} succeed. + * + * @param token + * @param tokenContext + * @return + */ + Response handleToken(T token, ActionTokenContext tokenContext); + + /** + * Returns the Java token class for use with deserialization. + * @return + */ + Class getTokenClass(); + + /** + * Returns an array of verifiers that are tested prior to handling the token. All verifiers have to pass successfully + * for token to be handled. The returned array must not be {@code null}. + * @param tokenContext + * @return Verifiers or an empty array. The returned array must not be {@code null}. + */ + default Predicate[] getVerifiers(ActionTokenContext tokenContext) { + return new Predicate[] {}; + } + + /** + * Returns an authentication session ID requested from within the given token + * @param token Token. Can be {@code null} + * @return authentication session ID + */ + String getAuthenticationSessionIdFromToken(T token); + + /** + * Returns a event type logged with {@link EventBuilder} class. + * @return + */ + EventType eventType(); + + /** + * Returns an error to be shown in the {@link EventBuilder} detail when token handling fails and + * no more specific error is provided. + * @return + */ + String getDefaultEventError(); + + /** + * Returns an error to be shown in the response when token handling fails and no more specific + * error message is provided. + * @return + */ + String getDefaultErrorMessage(); + + /** + * Creates a fresh authentication session according to the information from the token. The default + * implementation creates a new authentication session that requests termination after required actions. + * @param token + * @param tokenContext + * @return + */ + AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext tokenContext); + + /** + * Returns {@code true} when the token can be used repeatedly to invoke the action, {@code false} when the token + * is intended to be for single use only. + * @return see above + */ + boolean canUseTokenRepeatedly(T token, ActionTokenContext tokenContext); +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java new file mode 100644 index 0000000000..3ca3c17efb --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerFactory.java @@ -0,0 +1,27 @@ +/* + * 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.actiontoken; + +import org.keycloak.provider.ProviderFactory; +import org.keycloak.representations.JsonWebToken; + +/** + * + * @author hmlnarik + */ +public interface ActionTokenHandlerFactory extends ProviderFactory> { +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java new file mode 100644 index 0000000000..4a82cedacf --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandlerSpi.java @@ -0,0 +1,50 @@ +/* + * 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.actiontoken; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * + * @author hmlnarik + */ +public class ActionTokenHandlerSpi implements Spi { + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return NAME; + } + private static final String NAME = "actionTokenHandler"; + + @Override + public Class getProviderClass() { + return ActionTokenHandler.class; + } + + @Override + public Class getProviderFactoryClass() { + return ActionTokenHandlerFactory.class; + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java new file mode 100644 index 0000000000..ba4488039a --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java @@ -0,0 +1,160 @@ +/* + * 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.actiontoken; + +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.common.VerificationException; + +import org.keycloak.common.util.Time; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.*; +import org.keycloak.services.Urls; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.*; +import javax.ws.rs.core.UriInfo; + +/** + * 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 implements ActionTokenValueModel { + + public static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid"; + + public static final Predicate 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. + */ + public DefaultActionToken() { + super(null, null, 0, null); + } + + /** + * + * @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, absoluteExpirationInSecs, actionVerificationNonce); + } + + /** + * + * @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, String authenticationSessionId) { + super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce); + setAuthenticationSessionId(authenticationSessionId); + } + + @JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID) + public String getAuthenticationSessionId() { + return (String) getOtherClaims().get(JSON_FIELD_AUTHENTICATION_SESSION_ID); + } + + @JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID) + public final void setAuthenticationSessionId(String authenticationSessionId) { + setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId); + } + + @JsonIgnore + @Override + public Map getNotes() { + Map res = new HashMap<>(); + if (getAuthenticationSessionId() != null) { + res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getAuthenticationSessionId()); + } + return res; + } + + @Override + 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; + } + + /** + * Updates the following fields and serializes this token into a signed JWT. The list of updated fields follows: + *

    + *
  • {@code id}: random nonce
  • + *
  • {@code issuedAt}: Current time
  • + *
  • {@code issuer}: URI of the given realm
  • + *
  • {@code audience}: URI of the given realm (same as issuer)
  • + *
+ * + * @param session + * @param realm + * @param uri + * @return + */ + 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()); + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java new file mode 100644 index 0000000000..b41681f303 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java @@ -0,0 +1,79 @@ +/* + * 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.actiontoken; + +import org.keycloak.models.ActionTokenKeyModel; +import org.keycloak.representations.JsonWebToken; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.UUID; + +/** + * + * @author hmlnarik + */ +public class DefaultActionTokenKey extends JsonWebToken implements ActionTokenKeyModel { + + /** The authenticationSession note with ID of the user authenticated via the action token */ + public static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER"; + + public static final String JSON_FIELD_ACTION_VERIFICATION_NONCE = "nonce"; + + @JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true) + private UUID actionVerificationNonce; + + public DefaultActionTokenKey(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) { + this.subject = userId; + this.type = actionId; + this.expiration = absoluteExpirationInSecs; + this.actionVerificationNonce = actionVerificationNonce == null ? UUID.randomUUID() : actionVerificationNonce; + } + + @JsonIgnore + @Override + public String getUserId() { + return getSubject(); + } + + @JsonIgnore + @Override + public String getActionId() { + return getType(); + } + + @Override + public UUID getActionVerificationNonce() { + return actionVerificationNonce; + } + + public String serializeKey() { + return String.format("%s.%d.%s.%s", getUserId(), getExpiration(), getActionVerificationNonce(), getActionId()); + } + + public static DefaultActionTokenKey from(String serializedKey) { + if (serializedKey == null) { + return null; + } + String[] parsed = serializedKey.split("\\.", 4); + if (parsed.length != 4) { + return null; + } + + return new DefaultActionTokenKey(parsed[0], parsed[3], Integer.parseInt(parsed[1]), UUID.fromString(parsed[2])); + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java new file mode 100644 index 0000000000..271b2f26d9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ExplainedTokenVerificationException.java @@ -0,0 +1,60 @@ +/* + * 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.actiontoken; + +import org.keycloak.authentication.ExplainedVerificationException; +import org.keycloak.exceptions.TokenVerificationException; +import org.keycloak.representations.JsonWebToken; + +/** + * Token verification exception that bears an error to be logged via event system + * and a message to show to the user e.g. via {@code ErrorPage.error()}. + * + * @author hmlnarik + */ +public class ExplainedTokenVerificationException extends TokenVerificationException { + private final String errorEvent; + + public ExplainedTokenVerificationException(JsonWebToken token, ExplainedVerificationException cause) { + super(token, cause.getMessage(), cause); + this.errorEvent = cause.getErrorEvent(); + } + + public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent) { + super(token); + this.errorEvent = errorEvent; + } + + public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent, String message) { + super(token, message); + this.errorEvent = errorEvent; + } + + public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent, String message, Throwable cause) { + super(token, message); + this.errorEvent = errorEvent; + } + + public ExplainedTokenVerificationException(JsonWebToken token, String errorEvent, Throwable cause) { + super(token, cause); + this.errorEvent = errorEvent; + } + + public String getErrorEvent() { + return errorEvent; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/TokenUtils.java b/services/src/main/java/org/keycloak/authentication/actiontoken/TokenUtils.java new file mode 100644 index 0000000000..bdaa80415c --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/TokenUtils.java @@ -0,0 +1,85 @@ +/* + * 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.actiontoken; + +import org.keycloak.TokenVerifier; +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.representations.JsonWebToken; +import java.util.function.BooleanSupplier; + +/** + * + * @author hmlnarik + */ +public class TokenUtils { + /** + * Returns a predicate for use in {@link TokenVerifier} using the given boolean-returning function. + * When the function return {@code false}, this predicate throws a {@link ExplainedTokenVerificationException} + * with {@code message} and {@code errorEvent} set from {@code errorMessage} and {@code errorEvent}, . + * + * @param function + * @param errorEvent + * @param errorMessage + * @return + */ + public static Predicate checkThat(BooleanSupplier function, String errorEvent, String errorMessage) { + return (JsonWebToken t) -> { + if (! function.getAsBoolean()) { + throw new ExplainedTokenVerificationException(t, errorEvent, errorMessage); + } + + return true; + }; + } + + /** + * Returns a predicate for use in {@link TokenVerifier} using the given boolean-returning function. + * When the function return {@code false}, this predicate throws a {@link ExplainedTokenVerificationException} + * with {@code message} and {@code errorEvent} set from {@code errorMessage} and {@code errorEvent}, . + * + * @param function + * @param errorEvent + * @param errorMessage + * @return + */ + public static Predicate checkThat(java.util.function.Predicate function, String errorEvent, String errorMessage) { + return (T t) -> { + if (! function.test(t)) { + throw new ExplainedTokenVerificationException(t, errorEvent, errorMessage); + } + + return true; + }; + } + + + /** + * Returns a predicate that is applied only if the given {@code condition} evaluates to {@true}. In case + * it evaluates to {@code false}, the predicate passes. + * @param + * @param condition Condition guarding execution of the predicate + * @param predicate Predicate that gets tested if the condition evaluates to {@code true} + * @return + */ + public static Predicate onlyIf(java.util.function.Predicate condition, Predicate predicate) { + return t -> (! condition.test(t)) || predicate.test(t); + } + + public static Predicate[] predicates(Predicate... predicate) { + return predicate; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java new file mode 100644 index 0000000000..7c32e2dce5 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java @@ -0,0 +1,71 @@ +/* + * 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.actiontoken.execactions; + +import org.keycloak.authentication.actiontoken.DefaultActionToken; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.LinkedList; +import java.util.List; + +/** + * + * @author hmlnarik + */ +public class ExecuteActionsActionToken extends DefaultActionToken { + + public static final String TOKEN_TYPE = "execute-actions"; + private static final String JSON_FIELD_REQUIRED_ACTIONS = "rqac"; + private static final String JSON_FIELD_REDIRECT_URI = "reduri"; + + public ExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, List requiredActions, String redirectUri, String clientId) { + super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null); + setRequiredActions(requiredActions == null ? new LinkedList<>() : new LinkedList<>(requiredActions)); + setRedirectUri(redirectUri); + this.issuedFor = clientId; + } + + private ExecuteActionsActionToken() { + } + + @JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS) + public List getRequiredActions() { + return (List) getOtherClaims().get(JSON_FIELD_REQUIRED_ACTIONS); + } + + @JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS) + public final void setRequiredActions(List requiredActions) { + if (requiredActions == null) { + getOtherClaims().remove(JSON_FIELD_REQUIRED_ACTIONS); + } else { + setOtherClaims(JSON_FIELD_REQUIRED_ACTIONS, requiredActions); + } + } + + @JsonProperty(value = JSON_FIELD_REDIRECT_URI) + public String getRedirectUri() { + return (String) getOtherClaims().get(JSON_FIELD_REDIRECT_URI); + } + + @JsonProperty(value = JSON_FIELD_REDIRECT_URI) + public final void setRedirectUri(String redirectUri) { + if (redirectUri == null) { + getOtherClaims().remove(JSON_FIELD_REDIRECT_URI); + } else { + setOtherClaims(JSON_FIELD_REDIRECT_URI, redirectUri); + } + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java new file mode 100644 index 0000000000..9993ab76e8 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java @@ -0,0 +1,107 @@ +/* + * 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.actiontoken.execactions; + +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.actiontoken.*; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.models.*; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; +import java.util.Objects; +import javax.ws.rs.core.Response; + +/** + * + * @author hmlnarik + */ +public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander { + + public ExecuteActionsActionTokenHandler() { + super( + ExecuteActionsActionToken.TOKEN_TYPE, + ExecuteActionsActionToken.class, + Messages.INVALID_CODE, + EventType.EXECUTE_ACTIONS, + Errors.NOT_ALLOWED + ); + } + + @Override + public Predicate[] getVerifiers(ActionTokenContext tokenContext) { + return TokenUtils.predicates( + TokenUtils.checkThat( + // either redirect URI is not specified or must be valid for the client + t -> t.getRedirectUri() == null + || RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), t.getRedirectUri(), + tokenContext.getRealm(), tokenContext.getAuthenticationSession().getClient()) != null, + Errors.INVALID_REDIRECT_URI, + Messages.INVALID_REDIRECT_URI + ) + ); + } + + @Override + public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext tokenContext) { + AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); + + String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(), + tokenContext.getRealm(), authSession.getClient()); + + if (redirectUri != null) { + authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true"); + + authSession.setRedirectUri(redirectUri); + authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); + } + + token.getRequiredActions().stream().forEach(authSession::addRequiredAction); + + UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser(); + // verify user email as we know it is valid as this entry point would never have gotten here. + user.setEmailVerified(true); + + String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), tokenContext.getUriInfo(), tokenContext.getEvent()); + return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction); + } + + @Override + public boolean canUseTokenRepeatedly(ExecuteActionsActionToken token, ActionTokenContext tokenContext) { + RealmModel realm = tokenContext.getRealm(); + KeycloakSessionFactory sessionFactory = tokenContext.getSession().getKeycloakSessionFactory(); + + return token.getRequiredActions().stream() + .map(actionName -> realm.getRequiredActionProviderByAlias(actionName)) // get realm-specific model from action name and filter out irrelevant + .filter(Objects::nonNull) + .filter(RequiredActionProviderModel::isEnabled) + + .map(RequiredActionProviderModel::getProviderId) // get provider ID from model + + .map(providerId -> (RequiredActionFactory) sessionFactory.getProviderFactory(RequiredActionProvider.class, providerId)) + .filter(Objects::nonNull) + + .noneMatch(RequiredActionFactory::isOneTimeAction); + } + + +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java new file mode 100644 index 0000000000..7776634193 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java @@ -0,0 +1,65 @@ +/* + * 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.actiontoken.idpverifyemail; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.authentication.actiontoken.DefaultActionToken; + +/** + * Representation of a token that represents a time-limited verify e-mail action. + * + * @author hmlnarik + */ +public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { + + public static final String TOKEN_TYPE = "idp-verify-account-via-email"; + + private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu"; + private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa"; + + @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME) + private String identityProviderUsername; + + @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS) + private String identityProviderAlias; + + public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, + String identityProviderUsername, String identityProviderAlias) { + super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId); + this.identityProviderUsername = identityProviderUsername; + this.identityProviderAlias = identityProviderAlias; + } + + private IdpVerifyAccountLinkActionToken() { + } + + public String getIdentityProviderUsername() { + return identityProviderUsername; + } + + public void setIdentityProviderUsername(String identityProviderUsername) { + this.identityProviderUsername = identityProviderUsername; + } + + public String getIdentityProviderAlias() { + return identityProviderAlias; + } + + public void setIdentityProviderAlias(String identityProviderAlias) { + this.identityProviderAlias = identityProviderAlias; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java new file mode 100644 index 0000000000..389441ed34 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java @@ -0,0 +1,98 @@ +/* + * 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.actiontoken.idpverifyemail; + +import org.keycloak.authentication.actiontoken.AbstractActionTokenHander; +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.authentication.actiontoken.*; +import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator; +import org.keycloak.events.*; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.AuthenticationSessionProvider; +import java.util.Collections; +import javax.ws.rs.core.Response; + +/** + * Action token handler for verification of e-mail address. + * @author hmlnarik + */ +public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenHander { + + public IdpVerifyAccountLinkActionTokenHandler() { + super( + IdpVerifyAccountLinkActionToken.TOKEN_TYPE, + IdpVerifyAccountLinkActionToken.class, + Messages.STALE_CODE, + EventType.IDENTITY_PROVIDER_LINK_ACCOUNT, + Errors.INVALID_TOKEN + ); + } + + @Override + public Predicate[] getVerifiers(ActionTokenContext tokenContext) { + return TokenUtils.predicates( + ); + } + + @Override + public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext tokenContext) { + UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser(); + EventBuilder event = tokenContext.getEvent(); + + event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT) + .detail(Details.EMAIL, user.getEmail()) + .detail(Details.IDENTITY_PROVIDER, token.getIdentityProviderAlias()) + .detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername()) + .success(); + + // verify user email as we know it is valid as this entry point would never have gotten here. + user.setEmailVerified(true); + + AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); + if (tokenContext.isAuthenticationSessionFresh()) { + AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession()); + asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true); + + AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions(); + authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId()); + + if (authSession != null) { + authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()); + } else { + authSessProvider.updateNonlocalSessionAuthNotes( + token.getAuthenticationSessionId(), + Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()) + ); + } + + return tokenContext.getSession().getProvider(LoginFormsProvider.class) + .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername()) + .setAttribute("skipLink", true) + .createInfoPage(); + } + + authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()); + + return tokenContext.brokerFlow(null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH)); + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java new file mode 100644 index 0000000000..6cd04589f2 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java @@ -0,0 +1,36 @@ +/* + * 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.actiontoken.resetcred; + +import org.keycloak.authentication.actiontoken.DefaultActionToken; + +/** + * Representation of a token that represents a time-limited reset credentials action. + * + * @author hmlnarik + */ +public class ResetCredentialsActionToken extends DefaultActionToken { + + public static final String TOKEN_TYPE = "reset-credentials"; + + public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId) { + super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId); + } + + private ResetCredentialsActionToken() { + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java new file mode 100644 index 0000000000..0f08bd39a4 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java @@ -0,0 +1,108 @@ +/* + * 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.actiontoken.resetcred; + +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.authentication.actiontoken.*; +import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; +import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; +import org.keycloak.services.ErrorPage; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.services.resources.LoginActionsServiceChecks.IsActionRequired; +import org.keycloak.sessions.CommonClientSessionModel.Action; +import javax.ws.rs.core.Response; +import static org.keycloak.services.resources.LoginActionsService.RESET_CREDENTIALS_PATH; + +/** + * + * @author hmlnarik + */ +public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHander { + + public ResetCredentialsActionTokenHandler() { + super( + ResetCredentialsActionToken.TOKEN_TYPE, + ResetCredentialsActionToken.class, + Messages.RESET_CREDENTIAL_NOT_ALLOWED, + EventType.RESET_PASSWORD, + Errors.NOT_ALLOWED + ); + + } + + @Override + public Predicate[] getVerifiers(ActionTokenContext tokenContext) { + return new Predicate[] { + TokenUtils.checkThat(tokenContext.getRealm()::isResetPasswordAllowed, Errors.NOT_ALLOWED, Messages.RESET_CREDENTIAL_NOT_ALLOWED), + + new IsActionRequired(tokenContext, Action.AUTHENTICATE) + }; + } + + @Override + public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext) { + AuthenticationProcessor authProcessor = new ResetCredsAuthenticationProcessor(); + + return tokenContext.processFlow( + false, + RESET_CREDENTIALS_PATH, + tokenContext.getRealm().getResetCredentialsFlow(), + null, + authProcessor + ); + } + + @Override + public boolean canUseTokenRepeatedly(ResetCredentialsActionToken token, ActionTokenContext tokenContext) { + return false; + } + + public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor { + + @Override + protected Response authenticationComplete() { + boolean firstBrokerLoginInProgress = (authenticationSession.getAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null); + if (firstBrokerLoginInProgress) { + + UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realm, authenticationSession); + if (!linkingUser.getId().equals(authenticationSession.getAuthenticatedUser().getId())) { + return ErrorPage.error(session, + Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, + authenticationSession.getAuthenticatedUser().getUsername(), + linkingUser.getUsername() + ); + } + + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + authenticationSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, serializedCtx.getIdentityProviderId()); + + logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login with identity provider '%s'.", + linkingUser.getUsername(), serializedCtx.getIdentityProviderId()); + + return LoginActionsService.redirectToAfterBrokerLoginEndpoint(session, realm, uriInfo, authenticationSession, true); + } else { + return super.authenticationComplete(); + } + } + + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java new file mode 100644 index 0000000000..656c518718 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java @@ -0,0 +1,51 @@ +/* + * 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.actiontoken.verifyemail; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.authentication.actiontoken.DefaultActionToken; + +/** + * Representation of a token that represents a time-limited verify e-mail action. + * + * @author hmlnarik + */ +public class VerifyEmailActionToken extends DefaultActionToken { + + public static final String TOKEN_TYPE = "verify-email"; + + private static final String JSON_FIELD_EMAIL = "eml"; + + @JsonProperty(value = JSON_FIELD_EMAIL) + private String email; + + public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) { + super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId); + this.email = email; + } + + private VerifyEmailActionToken() { + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java new file mode 100644 index 0000000000..abe2127098 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java @@ -0,0 +1,89 @@ +/* + * 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.actiontoken.verifyemail; + +import org.keycloak.authentication.actiontoken.AbstractActionTokenHander; +import org.keycloak.TokenVerifier.Predicate; +import org.keycloak.authentication.actiontoken.*; +import org.keycloak.events.*; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserModel.RequiredAction; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; +import java.util.Objects; +import javax.ws.rs.core.Response; + +/** + * Action token handler for verification of e-mail address. + * @author hmlnarik + */ +public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander { + + public VerifyEmailActionTokenHandler() { + super( + VerifyEmailActionToken.TOKEN_TYPE, + VerifyEmailActionToken.class, + Messages.STALE_VERIFY_EMAIL_LINK, + EventType.VERIFY_EMAIL, + Errors.INVALID_TOKEN + ); + } + + @Override + public Predicate[] getVerifiers(ActionTokenContext tokenContext) { + return TokenUtils.predicates( + TokenUtils.checkThat( + t -> Objects.equals(t.getEmail(), tokenContext.getAuthenticationSession().getAuthenticatedUser().getEmail()), + Errors.INVALID_EMAIL, getDefaultErrorMessage() + ) + ); + } + + @Override + public Response handleToken(VerifyEmailActionToken token, ActionTokenContext tokenContext) { + UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser(); + EventBuilder event = tokenContext.getEvent(); + + event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()); + + AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); + + // verify user email as we know it is valid as this entry point would never have gotten here. + user.setEmailVerified(true); + user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); + authSession.removeRequiredAction(RequiredAction.VERIFY_EMAIL); + + event.success(); + + if (tokenContext.isAuthenticationSessionFresh()) { + AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession()); + asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true); + return tokenContext.getSession().getProvider(LoginFormsProvider.class) + .setSuccess(Messages.EMAIL_VERIFIED) + .createInfoPage(); + } + + tokenContext.setEvent(event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN)); + + String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession, tokenContext.getClientConnection(), tokenContext.getRequest(), tokenContext.getUriInfo(), event); + return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(), authSession, tokenContext.getUriInfo(), nextAction); + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java index 87108da0a3..82475663df 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/AbstractIdpAuthenticator.java @@ -25,11 +25,11 @@ import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.events.Errors; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Response; @@ -47,25 +47,25 @@ public abstract class AbstractIdpAuthenticator implements Authenticator { // The clientSession note flag to indicate that email provided by identityProvider was changed on updateProfile page public static final String UPDATE_PROFILE_EMAIL_CHANGED = "UPDATE_PROFILE_EMAIL_CHANGED"; - // The clientSession note flag to indicate if re-authentication after first broker login happened in different browser window. This can happen for example during email verification - public static final String IS_DIFFERENT_BROWSER = "IS_DIFFERENT_BROWSER"; - // The clientSession note flag to indicate that updateProfile page will be always displayed even if "updateProfileOnFirstLogin" is off public static final String ENFORCE_UPDATE_PROFILE = "ENFORCE_UPDATE_PROFILE"; // clientSession.note flag specifies if we imported new user to keycloak (true) or we just linked to an existing keycloak user (false) public static final String BROKER_REGISTERED_NEW_USER = "BROKER_REGISTERED_NEW_USER"; + // Set after firstBrokerLogin is successfully finished and contains the providerId of the provider, whose 'first-broker-login' flow was just finished + public static final String FIRST_BROKER_LOGIN_SUCCESS = "FIRST_BROKER_LOGIN_SUCCESS"; + @Override public void authenticate(AuthenticationFlowContext context) { - ClientSessionModel clientSession = context.getClientSession(); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, BROKERED_CONTEXT_NOTE); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); } - BrokeredIdentityContext brokerContext = serializedCtx.deserialize(context.getSession(), clientSession); + BrokeredIdentityContext brokerContext = serializedCtx.deserialize(context.getSession(), authSession); if (!brokerContext.getIdpConfig().isEnabled()) { sendFailureChallenge(context, Errors.IDENTITY_PROVIDER_ERROR, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); @@ -76,9 +76,9 @@ public abstract class AbstractIdpAuthenticator implements Authenticator { @Override public void action(AuthenticationFlowContext context) { - ClientSessionModel clientSession = context.getClientSession(); + AuthenticationSessionModel clientSession = context.getAuthenticationSession(); - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, BROKERED_CONTEXT_NOTE); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(clientSession, BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); } @@ -112,8 +112,8 @@ public abstract class AbstractIdpAuthenticator implements Authenticator { } - public static UserModel getExistingUser(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession) { - String existingUserId = clientSession.getNote(EXISTING_USER_INFO); + public static UserModel getExistingUser(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession) { + String existingUserId = authSession.getAuthNote(EXISTING_USER_INFO); if (existingUserId == null) { throw new AuthenticationFlowException("Unexpected state. There is no existing duplicated user identified in ClientSession", AuthenticationFlowError.INTERNAL_ERROR); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java index 82347004c6..3ed3dd7f90 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpConfirmLinkAuthenticator.java @@ -20,16 +20,17 @@ package org.keycloak.authentication.authenticators.broker; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AuthenticationFlowException; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.ServicesLogger; import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -41,9 +42,9 @@ public class IdpConfirmLinkAuthenticator extends AbstractIdpAuthenticator { @Override protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) { - ClientSessionModel clientSession = context.getClientSession(); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); - String existingUserInfo = clientSession.getNote(EXISTING_USER_INFO); + String existingUserInfo = authSession.getAuthNote(EXISTING_USER_INFO); if (existingUserInfo == null) { ServicesLogger.LOGGER.noDuplicationDetected(); context.attempted(); @@ -65,9 +66,12 @@ public class IdpConfirmLinkAuthenticator extends AbstractIdpAuthenticator { String action = formData.getFirst("submitAction"); if (action != null && action.equals("updateProfile")) { - context.getClientSession().setNote(ENFORCE_UPDATE_PROFILE, "true"); - context.getClientSession().removeNote(EXISTING_USER_INFO); - context.resetFlow(); + context.resetFlow(() -> { + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + + serializedCtx.saveToAuthenticationSession(authSession, BROKERED_CONTEXT_NOTE); + authSession.setAuthNote(ENFORCE_UPDATE_PROFILE, "true"); + }); } else if (action != null && action.equals("linkAccount")) { context.success(); } else { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java index 317cb64873..aacd1e69d3 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java @@ -53,7 +53,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); - if (context.getClientSession().getNote(EXISTING_USER_INFO) != null) { + if (context.getAuthenticationSession().getAuthNote(EXISTING_USER_INFO) != null) { context.attempted(); return; } @@ -61,7 +61,7 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator String username = getUsername(context, serializedCtx, brokerContext); if (username == null) { ServicesLogger.LOGGER.resetFlow(realm.isRegistrationEmailAsUsername() ? "Email" : "Username"); - context.getClientSession().setNote(ENFORCE_UPDATE_PROFILE, "true"); + context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true"); context.resetFlow(); return; } @@ -91,14 +91,14 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator userRegisteredSuccess(context, federatedUser, serializedCtx, brokerContext); context.setUser(federatedUser); - context.getClientSession().setNote(BROKER_REGISTERED_NEW_USER, "true"); + context.getAuthenticationSession().setAuthNote(BROKER_REGISTERED_NEW_USER, "true"); context.success(); } else { logger.debugf("Duplication detected. There is already existing user with %s '%s' .", duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue()); // Set duplicated user, so next authenticators can deal with it - context.getClientSession().setNote(EXISTING_USER_INFO, duplication.serialize()); + context.getAuthenticationSession().setAuthNote(EXISTING_USER_INFO, duplication.serialize()); Response challengeResponse = context.form() .setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue()) diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java index 420eb20924..a2a9b3a320 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java @@ -20,9 +20,10 @@ package org.keycloak.authentication.authenticators.broker; import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; -import org.keycloak.authentication.requiredactions.VerifyEmail; import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.common.util.Time; import org.keycloak.email.EmailException; import org.keycloak.email.EmailTemplateProvider; import org.keycloak.events.Details; @@ -30,19 +31,22 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.ServicesLogger; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; -import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.sessions.AuthenticationSessionModel; -import javax.ws.rs.core.MultivaluedMap; +import java.net.URI; +import java.util.Objects; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.util.concurrent.TimeUnit; +import javax.ws.rs.core.*; /** * @author Marek Posolda @@ -51,45 +55,92 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class); + public static final String VERIFY_ACCOUNT_IDP_USERNAME = "VERIFY_ACCOUNT_IDP_USERNAME"; + @Override protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) { KeycloakSession session = context.getSession(); RealmModel realm = context.getRealm(); - ClientSessionModel clientSession = context.getClientSession(); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); - if (realm.getSmtpConfig().size() == 0) { + if (realm.getSmtpConfig().isEmpty()) { ServicesLogger.LOGGER.smtpNotConfigured(); context.attempted(); return; } - // Create action cookie to detect if email verification happened in same browser - LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getClientSession().getId()); + if (Objects.equals(authSession.getAuthNote(VERIFY_ACCOUNT_IDP_USERNAME), brokerContext.getUsername())) { + UserModel existingUser = getExistingUser(session, realm, authSession); - VerifyEmail.setupKey(clientSession); + logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(), + brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername()); - UserModel existingUser = getExistingUser(session, realm, clientSession); + context.setUser(existingUser); + context.success(); + return; + } - String link = UriBuilder.fromUri(context.getActionUrl()) - .queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY)) - .build().toString(); + UserModel existingUser = getExistingUser(session, realm, authSession); + + // Do not allow resending e-mail by simple page refresh + if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), existingUser.getEmail())) { + authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, existingUser.getEmail()); + sendVerifyEmail(session, context, existingUser, brokerContext); + } else { + showEmailSentPage(context, brokerContext); + } + } + + @Override + protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) { + logger.debugf("Re-sending email requested for user, details follow"); + + // This will allow user to re-send email again + context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY); + + authenticateImpl(context, serializedCtx, brokerContext); + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return false; + } + + private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel existingUser, BrokeredIdentityContext brokerContext) throws UriBuilderException, IllegalArgumentException { + RealmModel realm = session.getContext().getRealm(); + UriInfo uriInfo = session.getContext().getUri(); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + + int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan(); + int absoluteExpirationInSecs = Time.currentTime() + validityInSecs; EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK) .user(existingUser) .detail(Details.USERNAME, existingUser.getUsername()) .detail(Details.EMAIL, existingUser.getEmail()) - .detail(Details.CODE_ID, clientSession.getId()) + .detail(Details.CODE_ID, authSession.getId()) .removeDetail(Details.AUTH_METHOD) .removeDetail(Details.AUTH_TYPE); - long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction()); - try { + IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken( + existingUser.getId(), absoluteExpirationInSecs, authSession.getId(), + brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias() + ); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString(); + long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs); + try { context.getSession().getProvider(EmailTemplateProvider.class) .setRealm(realm) .setUser(existingUser) .setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext) - .sendConfirmIdentityBrokerLink(link, expiration); + .sendConfirmIdentityBrokerLink(link, expirationInMinutes); event.success(); } catch (EmailException e) { @@ -103,62 +154,20 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator return; } + showEmailSentPage(context, brokerContext); + } + + + protected void showEmailSentPage(AuthenticationFlowContext context, BrokeredIdentityContext brokerContext) { + String accessCode = context.generateAccessCode(); + URI action = context.getActionUrl(accessCode); + Response challenge = context.form() .setStatus(Response.Status.OK) .setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext) + .setActionUri(action) .createIdpLinkEmailPage(); context.forceChallenge(challenge); } - @Override - protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) { - MultivaluedMap queryParams = context.getSession().getContext().getUri().getQueryParameters(); - String key = queryParams.getFirst(Constants.KEY); - ClientSessionModel clientSession = context.getClientSession(); - RealmModel realm = context.getRealm(); - KeycloakSession session = context.getSession(); - - if (key != null) { - String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY); - clientSession.removeNote(Constants.VERIFY_EMAIL_KEY); - if (key.equals(keyFromSession)) { - UserModel existingUser = getExistingUser(session, realm, clientSession); - - logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(), - brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername()); - - String actionCookieValue = LoginActionsService.getActionCookie(session.getContext().getRequestHeaders(), realm, session.getContext().getUri(), context.getConnection()); - if (actionCookieValue == null || !actionCookieValue.equals(clientSession.getId())) { - clientSession.setNote(IS_DIFFERENT_BROWSER, "true"); - } - - // User successfully confirmed linking by email verification. His email was defacto verified - existingUser.setEmailVerified(true); - - context.setUser(existingUser); - context.success(); - } else { - ServicesLogger.LOGGER.keyParamDoesNotMatch(); - Response challengeResponse = context.form() - .setError(Messages.INVALID_ACCESS_CODE) - .createErrorPage(); - context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse); - } - } else { - Response challengeResponse = context.form() - .setError(Messages.MISSING_PARAMETER, Constants.KEY) - .createErrorPage(); - context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse); - } - } - - @Override - public boolean requiresUser() { - return false; - } - - @Override - public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { - return false; - } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java index c58e3e16c5..c430fef78b 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java @@ -33,7 +33,6 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.services.ServicesLogger; import org.keycloak.services.resources.AttributeFormDataProcessor; import org.keycloak.services.validation.Validation; @@ -74,7 +73,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { } protected boolean requiresUpdateProfilePage(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) { - String enforceUpdateProfile = context.getClientSession().getNote(ENFORCE_UPDATE_PROFILE); + String enforceUpdateProfile = context.getAuthenticationSession().getAuthNote(ENFORCE_UPDATE_PROFILE); if (Boolean.parseBoolean(enforceUpdateProfile)) { return true; } @@ -123,12 +122,12 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { } userCtx.setEmail(email); - context.getClientSession().setNote(UPDATE_PROFILE_EMAIL_CHANGED, "true"); + context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true"); } AttributeFormDataProcessor.process(formData, realm, userCtx); - userCtx.saveToClientSession(context.getClientSession(), BROKERED_CONTEXT_NOTE); + userCtx.saveToAuthenticationSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE); logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java index cd09c37159..0ea8157d6b 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java @@ -39,7 +39,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm { @Override protected Response challenge(AuthenticationFlowContext context, MultivaluedMap formData) { - UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession()); + UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession()); return setupForm(context, formData, existingUser) .setStatus(Response.Status.OK) @@ -48,7 +48,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm { @Override protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { - UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession()); + UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession()); context.setUser(existingUser); // Restore formData for the case of error @@ -58,7 +58,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm { } protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap formData, UserModel existingUser) { - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(context.getClientSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(context.getAuthenticationSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java index 1e404621c7..a9c6d1e893 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java @@ -24,13 +24,13 @@ import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderDataMarshaller; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.reflections.Reflections; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.services.resources.IdentityBrokerService; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.JsonSerialization; import java.io.IOException; @@ -246,7 +246,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { } } - public BrokeredIdentityContext deserialize(KeycloakSession session, ClientSessionModel clientSession) { + public BrokeredIdentityContext deserialize(KeycloakSession session, AuthenticationSessionModel authSession) { BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId()); ctx.setUsername(getBrokerUsername()); @@ -258,7 +258,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { ctx.setBrokerUserId(getBrokerUserId()); ctx.setToken(getToken()); - RealmModel realm = clientSession.getRealm(); + RealmModel realm = authSession.getRealm(); IdentityProviderModel idpConfig = realm.getIdentityProviderByAlias(getIdentityProviderId()); if (idpConfig == null) { throw new ModelException("Can't find identity provider with ID " + getIdentityProviderId() + " in realm " + realm.getName()); @@ -282,7 +282,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { } } - ctx.setClientSession(clientSession); + ctx.setAuthenticationSession(authSession); return ctx; } @@ -299,7 +299,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { ctx.setToken(context.getToken()); ctx.setIdentityProviderId(context.getIdpConfig().getAlias()); - ctx.emailAsUsername = context.getClientSession().getRealm().isRegistrationEmailAsUsername(); + ctx.emailAsUsername = context.getAuthenticationSession().getRealm().isRegistrationEmailAsUsername(); IdentityProviderDataMarshaller serializer = context.getIdp().getMarshaller(); @@ -313,24 +313,24 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext { return ctx; } - // Save this context as note to clientSession - public void saveToClientSession(ClientSessionModel clientSession, String noteKey) { + // Save this context as note to authSession + public void saveToAuthenticationSession(AuthenticationSessionModel authSession, String noteKey) { try { String asString = JsonSerialization.writeValueAsString(this); - clientSession.setNote(noteKey, asString); + authSession.setAuthNote(noteKey, asString); } catch (IOException ioe) { throw new RuntimeException(ioe); } } - public static SerializedBrokeredIdentityContext readFromClientSession(ClientSessionModel clientSession, String noteKey) { - String asString = clientSession.getNote(noteKey); + public static SerializedBrokeredIdentityContext readFromAuthenticationSession(AuthenticationSessionModel authSession, String noteKey) { + String asString = authSession.getAuthNote(noteKey); if (asString == null) { return null; } else { try { SerializedBrokeredIdentityContext serializedCtx = JsonSerialization.readValue(asString, SerializedBrokeredIdentityContext.class); - serializedCtx.emailAsUsername = clientSession.getRealm().isRegistrationEmailAsUsername(); + serializedCtx.emailAsUsername = authSession.getRealm().isRegistrationEmailAsUsername(); return serializedCtx; } catch (IOException ioe) { throw new RuntimeException(ioe); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java index f837d3ca51..a0f13bce47 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java @@ -126,7 +126,7 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth username = username.trim(); context.getEvent().detail(Details.USERNAME, username); - context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); + context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); UserModel user = null; try { @@ -159,10 +159,10 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth String rememberMe = inputData.getFirst("rememberMe"); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); if (remember) { - context.getClientSession().setNote(Details.REMEMBER_ME, "true"); + context.getAuthenticationSession().setAuthNote(Details.REMEMBER_ME, "true"); context.getEvent().detail(Details.REMEMBER_ME, "true"); } else { - context.getClientSession().removeNote(Details.REMEMBER_ME); + context.getAuthenticationSession().removeAuthNote(Details.REMEMBER_ME); } context.setUser(user); return true; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java index b4552af50f..cf7e1a0ba2 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java @@ -19,12 +19,12 @@ package org.keycloak.authentication.authenticators.browser; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.Authenticator; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.LoginProtocol; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.sessions.AuthenticationSessionModel; /** * @author Bill Burke @@ -44,14 +44,14 @@ public class CookieAuthenticator implements Authenticator { if (authResult == null) { context.attempted(); } else { - ClientSessionModel clientSession = context.getClientSession(); - LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, clientSession.getAuthMethod()); + AuthenticationSessionModel clientSession = context.getAuthenticationSession(); + LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, clientSession.getProtocol()); // Cookie re-authentication is skipped if re-authentication is required if (protocol.requireReauthentication(authResult.getSession(), clientSession)) { context.attempted(); } else { - clientSession.setNote(AuthenticationManager.SSO_AUTH, "true"); + clientSession.setClientNote(AuthenticationManager.SSO_AUTH, "true"); context.setUser(authResult.getUser()); context.attachUserSession(authResult.getSession()); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java index f8408a4ecc..cb31e8d234 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/IdentityProviderAuthenticator.java @@ -63,7 +63,7 @@ public class IdentityProviderAuthenticator implements Authenticator { List identityProviders = context.getRealm().getIdentityProviders(); for (IdentityProviderModel identityProvider : identityProviders) { if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) { - String accessCode = new ClientSessionCode(context.getSession(), context.getRealm(), context.getClientSession()).getCode(); + String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getCode(); Response response = Response.seeOther( Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode)) .build(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java index 0b400f07ac..af639744b7 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticator.java @@ -47,7 +47,7 @@ import java.util.Map; *
  • {@code realm} the {@link RealmModel}
  • *
  • {@code user} the current {@link UserModel}
  • *
  • {@code session} the active {@link KeycloakSession}
  • - *
  • {@code clientSession} the current {@link org.keycloak.models.ClientSessionModel}
  • + *
  • {@code clientSession} the current {@link org.keycloak.sessions.AuthenticationSessionModel}
  • *
  • {@code httpRequest} the current {@link org.jboss.resteasy.spi.HttpRequest}
  • *
  • {@code LOG} a {@link org.jboss.logging.Logger} scoped to {@link ScriptBasedAuthenticator}/li> * @@ -160,7 +160,7 @@ public class ScriptBasedAuthenticator implements Authenticator { bindings.put("user", context.getUser()); bindings.put("session", context.getSession()); bindings.put("httpRequest", context.getHttpRequest()); - bindings.put("clientSession", context.getClientSession()); + bindings.put("clientSession", context.getAuthenticationSession()); bindings.put("LOG", LOGGER); }); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java index 8bfb995316..6b726867ff 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java @@ -30,7 +30,6 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; -import org.keycloak.services.ServicesLogger; import org.keycloak.services.messages.Messages; import javax.ws.rs.core.HttpHeaders; @@ -98,7 +97,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple context.setUser(output.getAuthenticatedUser()); if (output.getState() != null && !output.getState().isEmpty()) { for (Map.Entry entry : output.getState().entrySet()) { - context.getClientSession().setUserSessionNote(entry.getKey(), entry.getValue()); + context.getAuthenticationSession().setUserSessionNote(entry.getKey(), entry.getValue()); } } context.success(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java index 4f8e2d1948..bd81263109 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java @@ -59,7 +59,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl @Override public void authenticate(AuthenticationFlowContext context) { MultivaluedMap formData = new MultivaluedMapImpl<>(); - String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM); + String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM); String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders()); @@ -72,7 +72,6 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl } } Response challengeResponse = challenge(context, formData); - context.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId()); context.challenge(challengeResponse); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java index da7a67f1f7..a1cbbe514f 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/directgrant/ValidateUsername.java @@ -55,7 +55,7 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator { return; } context.getEvent().detail(Details.USERNAME, username); - context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); + context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); UserModel user = null; try { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java index 46097a022f..4be9b9c031 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialChooseUser.java @@ -17,12 +17,10 @@ package org.keycloak.authentication.authenticators.resetcred; +import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; 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.broker.AbstractIdpAuthenticator; import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.events.Details; @@ -34,7 +32,6 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.services.ServicesLogger; import org.keycloak.services.messages.Messages; import javax.ws.rs.core.MultivaluedMap; @@ -53,9 +50,9 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa @Override public void authenticate(AuthenticationFlowContext context) { - String existingUserId = context.getClientSession().getNote(AbstractIdpAuthenticator.EXISTING_USER_INFO); + String existingUserId = context.getAuthenticationSession().getAuthNote(AbstractIdpAuthenticator.EXISTING_USER_INFO); if (existingUserId != null) { - UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getClientSession()); + UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession()); logger.debugf("Forget-password triggered when reauthenticating user after first broker login. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername()); context.setUser(existingUser); @@ -63,6 +60,18 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa return; } + String actionTokenUserId = context.getAuthenticationSession().getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID); + if (actionTokenUserId != null) { + UserModel existingUser = context.getSession().users().getUserById(actionTokenUserId, context.getRealm()); + + // Action token logics handles checks for user ID validity and user being enabled + + logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername()); + context.setUser(existingUser); + context.success(); + return; + } + Response challenge = context.form().createPasswordReset(); context.challenge(challenge); } @@ -89,7 +98,7 @@ public class ResetCredentialChooseUser implements Authenticator, AuthenticatorFa user = context.getSession().users().getUserByEmail(username, realm); } - context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); + context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username); // we don't want people guessing usernames, so if there is a problem, just continue, but don't set the user // a null user will notify further executions, that this was a failure. diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java index 0d41b062c6..4ac9bffdaa 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java @@ -17,43 +17,37 @@ package org.keycloak.authentication.authenticators.resetcred; -import org.jboss.logging.Logger; +import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; 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.actiontoken.resetcred.ResetCredentialsActionToken; import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; +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 org.keycloak.sessions.AuthenticationSessionModel; +import java.util.*; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; -import java.util.List; import java.util.concurrent.TimeUnit; +import org.jboss.logging.Logger; /** * @author Bill Burke * @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); @@ -61,10 +55,9 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory @Override public void authenticate(AuthenticationFlowContext context) { - LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getClientSession().getId()); - UserModel user = context.getUser(); - String username = context.getClientSession().getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); + AuthenticationSessionModel authenticationSession = context.getAuthenticationSession(); + String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); // we don't want people guessing usernames, so if there was a problem obtaining the user, the user will be null. // just reset login for with a success message @@ -73,6 +66,13 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory return; } + String actionTokenUserId = authenticationSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID); + if (actionTokenUserId != null && Objects.equals(user.getId(), actionTokenUserId)) { + logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping " + PROVIDER_ID + " screen and using user '%s' ", user.getUsername()); + context.success(); + return; + } + EventBuilder event = context.getEvent(); // we don't want people guessing usernames, so if there is a problem, just continuously challenge @@ -85,19 +85,23 @@ 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()); - try { + int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan(); + int absoluteExpirationInSecs = Time.currentTime() + validityInSecs; + + // We send the secret in the email in a link as a query param. + ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId()); + String link = UriBuilder + .fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), 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, expirationInMinutes); - context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expiration); event.clone().event(EventType.SEND_RESET_PASSWORD) .user(user) .detail(Details.USERNAME, username) - .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success(); + .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getId()).success(); context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); } catch (EmailException e) { event.clone().event(EventType.SEND_RESET_PASSWORD) @@ -112,22 +116,16 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory } } + public static Long getLastChangedTimestamp(KeycloakSession session, RealmModel realm, UserModel user) { + // TODO(hmlnarik): Make this more generic to support non-password credential types + PasswordCredentialProvider passwordProvider = (PasswordCredentialProvider) session.getProvider(CredentialProvider.class, PasswordCredentialProviderFactory.PROVIDER_ID); + CredentialModel password = passwordProvider.getPassword(realm, user); + + return password == null ? null : password.getCreatedDate(); + } + @Override public void action(AuthenticationFlowContext context) { - 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); - Response challenge = context.form() - .setError(Messages.INVALID_ACCESS_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(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java index 40c703b988..4c1fdadac1 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetOTP.java @@ -33,7 +33,7 @@ public class ResetOTP extends AbstractSetRequiredActionAuthenticator { if (context.getExecution().isRequired() || (context.getExecution().isOptional() && configuredFor(context))) { - context.getClientSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); + context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); } context.success(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java index 64098fa379..68b8bfc21e 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetPassword.java @@ -20,8 +20,6 @@ package org.keycloak.authentication.authenticators.resetcred; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; -import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.resources.LoginActionsService; /** * @author Bill Burke @@ -33,15 +31,10 @@ public class ResetPassword extends AbstractSetRequiredActionAuthenticator { @Override public void authenticate(AuthenticationFlowContext context) { - String actionCookie = LoginActionsService.getActionCookie(context.getSession().getContext().getRequestHeaders(), context.getRealm(), context.getUriInfo(), context.getConnection()); - if (actionCookie == null || !actionCookie.equals(context.getClientSession().getId())) { - context.getClientSession().setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); - } - if (context.getExecution().isRequired() || (context.getExecution().isOptional() && configuredFor(context))) { - context.getClientSession().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + context.getAuthenticationSession().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); } context.success(); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java index e0860fa32b..89048acd5e 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/ValidateX509CertificateUsername.java @@ -92,7 +92,7 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica UserModel user; try { context.getEvent().detail(Details.USERNAME, userIdentity.toString()); - context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString()); + context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString()); user = getUserIdentityToModelMapper(config).find(context, userIdentity); } catch(ModelDuplicateException e) { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java index 21e67ecaaf..2aa5a63140 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/x509/X509ClientCertificateAuthenticator.java @@ -111,7 +111,7 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif UserModel user; try { context.getEvent().detail(Details.USERNAME, userIdentity.toString()); - context.getClientSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString()); + context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString()); user = getUserIdentityToModelMapper(config).find(context, userIdentity); } catch(ModelDuplicateException e) { @@ -166,7 +166,6 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif // to call the method "challenge" results in a wrong/unexpected behavior. // The question is whether calling "forceChallenge" here is ok from // the design viewpoint? - context.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId()); context.forceChallenge(createSuccessResponse(context, certs[0].getSubjectDN().getName())); // Do not set the flow status yet, we want to display a form to let users // choose whether to accept the identity from certificate or to specify username/password explicitly diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java index 90dee70808..6567aef923 100755 --- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java +++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java @@ -134,16 +134,16 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory { user.setEnabled(true); user.setEmail(email); - context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); + context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); AttributeFormDataProcessor.process(formData, context.getRealm(), user); context.setUser(user); context.getEvent().user(user); context.getEvent().success(); context.newEvent().event(EventType.LOGIN); - context.getEvent().client(context.getClientSession().getClient().getClientId()) - .detail(Details.REDIRECT_URI, context.getClientSession().getRedirectUri()) - .detail(Details.AUTH_METHOD, context.getClientSession().getAuthMethod()); - String authType = context.getClientSession().getNote(Details.AUTH_TYPE); + context.getEvent().client(context.getAuthenticationSession().getClient().getClientId()) + .detail(Details.REDIRECT_URI, context.getAuthenticationSession().getRedirectUri()) + .detail(Details.AUTH_METHOD, context.getAuthenticationSession().getProtocol()); + String authType = context.getAuthenticationSession().getAuthNote(Details.AUTH_TYPE); if (authType != null) { context.getEvent().detail(Details.AUTH_TYPE, authType); } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java index aa5bf25db5..6c7f7451af 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java @@ -88,8 +88,8 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac String passwordConfirm = formData.getFirst("password-confirm"); EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR) - .client(context.getClientSession().getClient()) - .user(context.getClientSession().getUserSession().getUser()); + .client(context.getAuthenticationSession().getClient()) + .user(context.getAuthenticationSession().getAuthenticatedUser()); if (Validation.isBlank(passwordNew)) { Response challenge = context.form() @@ -157,4 +157,9 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac public String getId() { return UserModel.RequiredAction.UPDATE_PASSWORD.name(); } + + @Override + public boolean isOneTimeAction() { + return true; + } } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java index 829c705f6d..de7a078ccd 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateTotp.java @@ -118,4 +118,9 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory public String getId() { return UserModel.RequiredAction.CONFIGURE_TOTP.name(); } + + @Override + public boolean isOneTimeAction() { + return true; + } } diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java index 2d683d3afa..baa3c4e9b4 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java @@ -22,20 +22,23 @@ import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken; +import org.keycloak.common.util.Time; +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.forms.login.LoginFormsProvider; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.HmacOTP; -import org.keycloak.services.ServicesLogger; -import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.models.*; +import org.keycloak.services.Urls; import org.keycloak.services.validation.Validation; -import javax.ws.rs.core.Response; +import org.keycloak.sessions.AuthenticationSessionModel; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import javax.ws.rs.core.*; /** * @author Bill Burke @@ -52,32 +55,44 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor } @Override public void requiredActionChallenge(RequiredActionContext context) { + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + if (context.getUser().isEmailVerified()) { context.success(); + authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY); return; } - if (Validation.isBlank(context.getUser().getEmail())) { + String email = context.getUser().getEmail(); + if (Validation.isBlank(email)) { context.ignore(); return; } - context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success(); - LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId()); + LoginFormsProvider loginFormsProvider = context.form(); + Response challenge; - setupKey(context.getClientSession()); + // Do not allow resending e-mail by simple page refresh, i.e. when e-mail sent, it should be resent properly via email-verification endpoint + if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), email)) { + authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, email); + EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, email); + challenge = sendVerifyEmail(context.getSession(), loginFormsProvider, context.getUser(), context.getAuthenticationSession(), event); + } else { + challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); + } - LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class) - .setClientSessionCode(context.generateCode()) - .setClientSession(context.getClientSession()) - .setUser(context.getUser()); - Response challenge = loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); context.challenge(challenge); } + @Override public void processAction(RequiredActionContext context) { - context.failure(); + logger.debugf("Re-sending email requested for user: %s", context.getUser().getUsername()); + + // This will allow user to re-send email again + context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY); + + requiredActionChallenge(context); } @@ -112,8 +127,30 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor return UserModel.RequiredAction.VERIFY_EMAIL.name(); } - public static void setupKey(ClientSessionModel clientSession) { - String secret = HmacOTP.generateSecret(10); - clientSession.setNote(Constants.VERIFY_EMAIL_KEY, secret); + private Response sendVerifyEmail(KeycloakSession session, LoginFormsProvider forms, UserModel user, AuthenticationSessionModel authSession, EventBuilder event) throws UriBuilderException, IllegalArgumentException { + RealmModel realm = session.getContext().getRealm(); + UriInfo uriInfo = session.getContext().getUri(); + + int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(); + int absoluteExpirationInSecs = Time.currentTime() + validityInSecs; + + VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getId(), user.getEmail()); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + String link = builder.build(realm.getName()).toString(); + long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs); + + try { + session + .getProvider(EmailTemplateProvider.class) + .setRealm(realm) + .setUser(user) + .sendVerifyEmail(link, expirationInMinutes); + event.success(); + } catch (EmailException e) { + logger.error("Failed to send verification email", e); + event.error(Errors.EMAIL_SEND_FAILED); + } + + return forms.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); } } diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java index 2e82794927..24e1a8943e 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -38,6 +38,7 @@ import javax.ws.rs.core.Response; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.util.Permissions; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.authorization.PolicyEvaluationRequest; import org.keycloak.authorization.admin.representation.PolicyEvaluationResponseBuilder; @@ -53,20 +54,24 @@ import org.keycloak.authorization.policy.evaluation.EvaluationContext; import org.keycloak.authorization.policy.evaluation.Result; import org.keycloak.authorization.store.ScopeStore; import org.keycloak.authorization.store.StoreFactory; -import org.keycloak.authorization.util.Permissions; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.services.Urls; +import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.resources.admin.RealmAuth; +import org.keycloak.sessions.AuthenticationSessionModel; /** * @author Pedro Igor @@ -192,19 +197,13 @@ public class PolicyEvaluationService { private static class CloseableKeycloakIdentity extends KeycloakIdentity { private UserSessionModel userSession; - private ClientSessionModel clientSession; - public CloseableKeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession, UserSessionModel userSession, ClientSessionModel clientSession) { + public CloseableKeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession, UserSessionModel userSession) { super(accessToken, keycloakSession); this.userSession = userSession; - this.clientSession = clientSession; } public void close() { - if (clientSession != null) { - keycloakSession.sessions().removeClientSession(realm, clientSession); - } - if (userSession != null) { keycloakSession.sessions().removeUserSession(realm, userSession); } @@ -220,7 +219,7 @@ public class PolicyEvaluationService { String subject = representation.getUserId(); - ClientSessionModel clientSession = null; + AuthenticatedClientSessionModel clientSession = null; UserSessionModel userSession = null; if (subject != null) { UserModel userModel = keycloakSession.users().getUserById(subject, realm); @@ -234,11 +233,15 @@ public class PolicyEvaluationService { if (clientId != null) { ClientModel clientModel = realm.getClientById(clientId); - clientSession = keycloakSession.sessions().createClientSession(realm, clientModel); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - userSession = keycloakSession.sessions().createUserSession(realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null); + String id = KeycloakModelUtils.generateId(); - new TokenManager().attachClientSession(userSession, clientSession); + AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createAuthenticationSession(id, realm, clientModel); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + authSession.setAuthenticatedUser(userModel); + userSession = keycloakSession.sessions().createUserSession(id, realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null); + + AuthenticationManager.setRolesAndMappersInSession(authSession); + clientSession = TokenManager.attachAuthenticationSession(keycloakSession, userSession, authSession); Set requestedRoles = new HashSet<>(); for (String roleId : clientSession.getRoles()) { @@ -276,6 +279,6 @@ public class PolicyEvaluationService { representation.getRoleIds().forEach(roleName -> realmAccess.addRole(roleName)); } - return new CloseableKeycloakIdentity(accessToken, keycloakSession, userSession, clientSession); + return new CloseableKeycloakIdentity(accessToken, keycloakSession, userSession); } } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java index 6b5b0275ab..9ea53e2d9f 100644 --- a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java +++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java @@ -23,7 +23,6 @@ import org.keycloak.authorization.attribute.Attributes; import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.util.Tokens; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -118,8 +117,8 @@ public class KeycloakIdentity implements Identity { @Override public String getId() { if (isResourceServer()) { - ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession()); - return clientSession.getClient().getId(); + ClientModel client = getTargetClient(); + return client==null ? null : client.getId(); } return this.accessToken.getSubject(); @@ -137,20 +136,10 @@ public class KeycloakIdentity implements Identity { private boolean isResourceServer() { UserModel clientUser = null; - if (this.accessToken.getClientSession() != null) { - ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession()); + ClientModel clientModel = getTargetClient(); - if (clientSession != null) { - clientUser = this.keycloakSession.users().getServiceAccount(clientSession.getClient()); - } - } - - if (this.accessToken.getIssuedFor() != null) { - ClientModel clientModel = this.keycloakSession.realms().getClientById(this.accessToken.getIssuedFor(), this.realm); - - if (clientModel != null) { - clientUser = this.keycloakSession.users().getServiceAccount(clientModel); - } + if (clientModel != null) { + clientUser = this.keycloakSession.users().getServiceAccount(clientModel); } if (clientUser == null) { @@ -159,4 +148,17 @@ public class KeycloakIdentity implements Identity { return this.accessToken.getSubject().equals(clientUser.getId()); } + + private ClientModel getTargetClient() { + if (this.accessToken.getIssuedFor() != null) { + return realm.getClientByClientId(accessToken.getIssuedFor()); + } + + if (this.accessToken.getAudience() != null && this.accessToken.getAudience().length > 0) { + String audience = this.accessToken.getAudience()[0]; + return realm.getClientByClientId(audience); + } + + return null; + } } diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 7f02e43632..339747a560 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -37,6 +37,7 @@ import org.keycloak.services.messages.Messages; import javax.ws.rs.GET; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; @@ -240,6 +241,8 @@ public abstract class AbstractOAuth2IdentityProvider 0 ? tokenResponse.getExpiresIn() + currentTime : 0; - userSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration)); - userSession.setNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken()); - userSession.setNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken()); - userSession.setNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken()); + authSession.setUserSessionNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration)); + authSession.setUserSessionNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken()); + authSession.setUserSessionNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken()); + authSession.setUserSessionNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken()); } @Override diff --git a/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java b/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java index e46798cd0c..1b91f56830 100755 --- a/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java @@ -87,14 +87,14 @@ public class HardcodedUserSessionAttributeMapper extends AbstractIdentityProvide public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String attribute = mapperModel.getConfig().get(ATTRIBUTE); String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE); - context.getClientSession().setUserSessionNote(attribute, attributeValue); + context.getAuthenticationSession().setUserSessionNote(attribute, attributeValue); } @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String attribute = mapperModel.getConfig().get(ATTRIBUTE); String attributeValue = mapperModel.getConfig().get(ATTRIBUTE_VALUE); - context.getClientSession().setUserSessionNote(attribute, attributeValue); + context.getAuthenticationSession().setUserSessionNote(attribute, attributeValue); } @Override diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java index 5825f60554..dc38d59ab9 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -71,6 +71,7 @@ import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -436,7 +437,8 @@ public class SAMLEndpoint { return callback.authenticated(identity); - + } catch (WebApplicationException e) { + return e.getResponse(); } catch (Exception e) { throw new IdentityBrokerException("Could not process response from SAML identity provider.", e); } diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index c28cda8936..51d6eb8ec6 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -31,7 +31,6 @@ import org.keycloak.dom.saml.v2.assertion.SubjectType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.events.EventBuilder; import org.keycloak.keys.RsaKeyMetadata; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; @@ -56,6 +55,7 @@ import java.util.TreeSet; import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.keys.KeyMetadata; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; +import org.keycloak.sessions.AuthenticationSessionModel; /** * @author Pedro Igor @@ -132,17 +132,17 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider getClients() { Set clients = new HashSet(); - for (ClientSessionModel clientSession : session.getClientSessions()) { - ClientModel client = clientSession.getClient(); + for (String clientUUID : session.getAuthenticatedClientSessions().keySet()) { + ClientModel client = realm.getClientById(clientUUID); clients.add(client.getClientId()); } return clients; diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index 9f60404077..625406c33f 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -23,8 +23,6 @@ import org.keycloak.authentication.requiredactions.util.UpdateProfileContext; import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.common.util.ObjectUtil; -import org.keycloak.email.EmailException; -import org.keycloak.email.EmailTemplateProvider; import org.keycloak.forms.login.LoginFormsPages; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.freemarker.model.ClientBean; @@ -38,18 +36,11 @@ import org.keycloak.forms.login.freemarker.model.RegisterBean; import org.keycloak.forms.login.freemarker.model.RequiredActionUrlFormatterMethod; import org.keycloak.forms.login.freemarker.model.TotpBean; import org.keycloak.forms.login.freemarker.model.UrlBean; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserModel; +import org.keycloak.models.*; import org.keycloak.models.utils.FormMessage; import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.theme.BrowserSecurityHeaderSetup; import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerUtil; @@ -70,14 +61,7 @@ import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.net.URI; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.TimeUnit; +import java.util.*; /** * @author Stian Thorgersen @@ -106,7 +90,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { private UserModel user; - private ClientSessionModel clientSession; private final Map attributes = new HashMap(); public FreeMarkerLoginFormsProvider(KeycloakSession session, FreeMarkerUtil freeMarker) { @@ -123,7 +106,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { public Response createResponse(UserModel.RequiredAction action) { RealmModel realm = session.getContext().getRealm(); - UriInfo uriInfo = session.getContext().getUri(); String actionMessage; LoginFormsPages page; @@ -145,20 +127,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { page = LoginFormsPages.LOGIN_UPDATE_PASSWORD; break; case VERIFY_EMAIL: - try { - UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); - builder.queryParam(OAuth2Constants.CODE, accessCode); - builder.queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY)); - - String link = builder.build(realm.getName()).toString(); - long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); - - session.getProvider(EmailTemplateProvider.class).setRealm(realm).setUser(user).sendVerifyEmail(link, expiration); - } catch (EmailException e) { - logger.error("Failed to send verification email", e); - return setError(Messages.EMAIL_SENT_ERROR).createErrorPage(); - } - actionMessage = Messages.VERIFY_EMAIL; page = LoginFormsPages.LOGIN_VERIFY_EMAIL; break; @@ -182,6 +150,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { String requestURI = uriInfo.getBaseUri().getPath(); UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); + if (page == LoginFormsPages.OAUTH_GRANT) { + // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param + uriBuilder.replaceQuery(null); + } + + URI baseUri = uriBuilder.build(); + + if (accessCode != null) { + uriBuilder.queryParam(OAuth2Constants.CODE, accessCode); + } + URI baseUriWithCode = uriBuilder.build(); for (String k : queryParameterMap.keySet()) { @@ -190,10 +169,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { uriBuilder.replaceQueryParam(k, objects); } - if (accessCode != null) { - uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); - } - ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme; try { @@ -235,11 +210,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } attributes.put("messagesPerField", messagesPerField); - if (page == LoginFormsPages.OAUTH_GRANT) { - // for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param - uriBuilder.replaceQuery(null); - } - URI baseUri = uriBuilder.build(); attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); if (realm != null && user != null && session != null) { attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session)); @@ -250,7 +220,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { List identityProviders = realm.getIdentityProviders(); identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData); - attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo)); + attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode)); attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); @@ -298,7 +268,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { attributes.put("register", new RegisterBean(formData)); break; case OAUTH_GRANT: - attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, protocolMappersRequested, this.accessRequestMessage)); + attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested, protocolMappersRequested, this.accessRequestMessage)); attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle)); break; case CODE: @@ -342,11 +312,14 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { if (objects.length == 1 && objects[0] == null) continue; // uriBuilder.replaceQueryParam(k, objects); } - if (accessCode != null) { - uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); - } + URI baseUri = uriBuilder.build(); + if (accessCode != null) { + uriBuilder.queryParam(OAuth2Constants.CODE, accessCode); + } + URI baseUriWithCode = uriBuilder.build(); + ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme; try { @@ -398,7 +371,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { List identityProviders = realm.getIdentityProviders(); identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData); - attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo)); + attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode)); attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); @@ -466,6 +439,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return createResponse(LoginFormsPages.LOGIN_IDP_LINK_CONFIRM); } + @Override + public Response createLoginExpiredPage() { + return createResponse(LoginFormsPages.LOGIN_PAGE_EXPIRED); + } + @Override public Response createIdpLinkEmailPage() { BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT); @@ -485,8 +463,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } @Override - public Response createOAuthGrant(ClientSessionModel clientSession) { - this.clientSession = clientSession; + public Response createOAuthGrant() { return createResponse(LoginFormsPages.OAUTH_GRANT); } @@ -592,12 +569,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { return this; } - @Override - public LoginFormsProvider setClientSession(ClientSessionModel clientSession) { - this.clientSession = clientSession; - return this; - } - @Override public LoginFormsProvider setAccessRequest(List realmRolesRequested, MultivaluedMap resourceRolesRequested, List protocolMappersRequested) { this.realmRolesRequested = realmRolesRequested; diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java index e28c627593..f2a9d756a5 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java @@ -54,6 +54,8 @@ public class Templates { return "login-update-profile.ftl"; case CODE: return "code.ftl"; + case LOGIN_PAGE_EXPIRED: + return "login-page-expired.ftl"; default: throw new IllegalArgumentException(); } diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java index 696e19b5fc..986d0efc21 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/IdentityProviderBean.java @@ -42,7 +42,7 @@ public class IdentityProviderBean { private RealmModel realm; private final KeycloakSession session; - public IdentityProviderBean(RealmModel realm, KeycloakSession session, List identityProviders, URI baseURI, UriInfo uriInfo) { + public IdentityProviderBean(RealmModel realm, KeycloakSession session, List identityProviders, URI baseURI) { this.realm = realm; this.session = session; diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java index 556db25a47..bf424cb80d 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/OAuthGrantBean.java @@ -18,7 +18,6 @@ package org.keycloak.forms.login.freemarker.model; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RoleModel; @@ -38,7 +37,7 @@ public class OAuthGrantBean { private ClientModel client; private List claimsRequested; - public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List realmRolesRequested, MultivaluedMap resourceRolesRequested, + public OAuthGrantBean(String code, ClientModel client, List realmRolesRequested, MultivaluedMap resourceRolesRequested, List protocolMappersRequested, String accessRequestMessage) { this.code = code; this.client = client; diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java index a622c222e0..0c574c1887 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/model/UrlBean.java @@ -50,6 +50,10 @@ public class UrlBean { return Urls.realmLoginPage(baseURI, realm).toString(); } + public String getLoginRestartFlowUrl() { + return Urls.realmLoginRestartPage(baseURI, realm).toString(); + } + public String getRegistrationAction() { if (this.actionuri != null) { return this.actionuri.toString(); @@ -81,10 +85,6 @@ public class UrlBean { return Urls.loginUsernameReminder(baseURI, realm).toString(); } - public String getLoginEmailVerificationUrl() { - return Urls.loginActionEmailVerification(baseURI, realm).toString(); - } - public String getFirstBrokerLoginUrl() { return Urls.firstBrokerLoginProcessor(baseURI, realm).toString(); } diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java index 0c1462c787..9c1e5a5e8e 100755 --- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java +++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java @@ -17,19 +17,25 @@ package org.keycloak.protocol; +import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.common.ClientConnection; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationFlowModel; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.LoginProtocol.Error; -import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.services.util.CacheControlUtil; +import org.keycloak.services.util.AuthenticationFlowURLHelper; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; @@ -43,6 +49,10 @@ import javax.ws.rs.core.UriInfo; */ public abstract class AuthorizationEndpointBase { + private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class); + + public static final String APP_INITIATED_FLOW = "APP_INITIATED_FLOW"; + protected RealmModel realm; protected EventBuilder event; protected AuthenticationManager authManager; @@ -63,9 +73,9 @@ public abstract class AuthorizationEndpointBase { this.event = event; } - protected AuthenticationProcessor createProcessor(ClientSessionModel clientSession, String flowId, String flowPath) { + protected AuthenticationProcessor createProcessor(AuthenticationSessionModel authSession, String flowId, String flowPath) { AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clientSession) + processor.setAuthenticationSession(authSession) .setFlowPath(flowPath) .setFlowId(flowId) .setBrowserFlow(true) @@ -75,48 +85,54 @@ public abstract class AuthorizationEndpointBase { .setSession(session) .setUriInfo(uriInfo) .setRequest(request); + + authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath); + return processor; } /** * Common method to handle browser authentication request in protocols unified way. * - * @param clientSession for current request + * @param authSession for current request * @param protocol handler for protocol used to initiate login * @param isPassive set to true if login should be passive (without login screen shown) * @param redirectToAuthentication if true redirect to flow url. If initial call to protocol is a POST, you probably want to do this. This is so we can disable the back button on browser * @return response to be returned to the browser */ - protected Response handleBrowserAuthenticationRequest(ClientSessionModel clientSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) { + protected Response handleBrowserAuthenticationRequest(AuthenticationSessionModel authSession, LoginProtocol protocol, boolean isPassive, boolean redirectToAuthentication) { AuthenticationFlowModel flow = getAuthenticationFlow(); String flowId = flow.getId(); - AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.AUTHENTICATE_PATH); - event.detail(Details.CODE_ID, clientSession.getId()); + AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH); + event.detail(Details.CODE_ID, authSession.getId()); if (isPassive) { // OIDC prompt == NONE or SAML 2 IsPassive flag // This means that client is just checking if the user is already completely logged in. // We cancel login if any authentication action or required action is required try { if (processor.authenticateOnly() == null) { - processor.attachSession(); + // processor.attachSession(); } else { - Response response = protocol.sendError(clientSession, Error.PASSIVE_LOGIN_REQUIRED); - session.sessions().removeClientSession(realm, clientSession); + Response response = protocol.sendError(authSession, Error.PASSIVE_LOGIN_REQUIRED); return response; } - if (processor.isActionRequired()) { - Response response = protocol.sendError(clientSession, Error.PASSIVE_INTERACTION_REQUIRED); - session.sessions().removeClientSession(realm, clientSession); - return response; + AuthenticationManager.setRolesAndMappersInSession(authSession); + + if (processor.nextRequiredAction() != null) { + Response response = protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED); + return response; } + + // Attach session once no requiredActions or other things are required + processor.attachSession(); } catch (Exception e) { return processor.handleBrowserException(e); } return processor.finishAuthentication(protocol); } else { try { - RestartLoginCookie.setRestartCookie(session, realm, clientConnection, uriInfo, clientSession); + RestartLoginCookie.setRestartCookie(session, realm, clientConnection, uriInfo, authSession); if (redirectToAuthentication) { return processor.redirectToFlow(); } @@ -131,4 +147,111 @@ public abstract class AuthorizationEndpointBase { return realm.getBrowserFlow(); } + + protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) { + AuthenticationSessionManager manager = new AuthenticationSessionManager(session); + String authSessionId = manager.getCurrentAuthenticationSessionId(realm); + AuthenticationSessionModel authSession = authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId); + + if (authSession != null) { + + ClientSessionCode check = new ClientSessionCode<>(session, realm, authSession); + if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) { + + logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId()); + authSession.restartSession(realm, client); + return new AuthorizationEndpointChecks(authSession); + + } else if (isNewRequest(authSession, client, requestState)) { + // Check if we have lastProcessedExecution and restart the session just if yes. Otherwise update just client information from the AuthorizationEndpoint request. + // This difference is needed, because of logout from JS applications in multiple browser tabs. + if (hasProcessedExecution(authSession)) { + logger.debug("New request from application received, but authentication session already exists. Restart existing authentication session"); + authSession.restartSession(realm, client); + } else { + logger.debug("New request from application received, but authentication session already exists. Update client information in existing authentication session"); + authSession.clearClientNotes(); // update client data + authSession.updateClient(client); + } + + return new AuthorizationEndpointChecks(authSession); + + } else { + logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button."); + + // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form + if (!shouldShowExpirePage(authSession)) { + return new AuthorizationEndpointChecks(authSession); + } else { + CacheControlUtil.noBackButtonCacheControlHeader(); + + Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo) + .showPageExpired(authSession); + return new AuthorizationEndpointChecks(response); + } + } + } + + UserSessionModel userSession = authSessionId==null ? null : session.sessions().getUserSession(realm, authSessionId); + + if (userSession != null) { + logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId); + authSession = session.authenticationSessions().createAuthenticationSession(authSessionId, realm, client); + } else { + authSession = manager.createAuthenticationSession(realm, client, true); + logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId()); + } + + return new AuthorizationEndpointChecks(authSession); + + } + + private boolean hasProcessedExecution(AuthenticationSessionModel authSession) { + String lastProcessedExecution = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION); + return (lastProcessedExecution != null); + } + + // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form + private boolean shouldShowExpirePage(AuthenticationSessionModel authSession) { + if (hasProcessedExecution(authSession)) { + return true; + } + + String initialFlow = authSession.getClientNote(APP_INITIATED_FLOW); + if (initialFlow == null) { + initialFlow = LoginActionsService.AUTHENTICATE_PATH; + } + + String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); + // Check if we transitted between flows (eg. clicking "register" on login screen and then clicking browser 'back', which showed this page) + if (!initialFlow.equals(lastFlow) && AuthenticationSessionModel.Action.AUTHENTICATE.toString().equals(authSession.getAction())) { + logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", initialFlow, lastFlow); + + authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, initialFlow); + authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + return false; + } + + return false; + } + + // Try to see if it is new request from the application, or refresh of some previous request + protected abstract boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestState); + + + protected static class AuthorizationEndpointChecks { + public final AuthenticationSessionModel authSession; + public final Response response; + + private AuthorizationEndpointChecks(Response response) { + this.authSession = null; + this.response = response; + } + + private AuthorizationEndpointChecks(AuthenticationSessionModel authSession) { + this.authSession = authSession; + this.response = null; + } + } + } \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java index 51bdd81034..0e488e18d7 100644 --- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java +++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java @@ -23,24 +23,23 @@ import org.keycloak.common.ClientConnection; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.HMACProvider; -import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.util.CookieHelper; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.crypto.SecretKey; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.UriInfo; -import java.security.PublicKey; import java.util.HashMap; import java.util.Map; /** - * This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the client session + * This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the authentication session * can be restarted. * * @author Bill Burke @@ -49,8 +48,6 @@ import java.util.Map; public class RestartLoginCookie { private static final Logger logger = Logger.getLogger(RestartLoginCookie.class); public static final String KC_RESTART = "KC_RESTART"; - @JsonProperty("cs") - protected String clientSession; @JsonProperty("cid") protected String clientId; @@ -67,14 +64,6 @@ public class RestartLoginCookie { @JsonProperty("notes") protected Map notes = new HashMap<>(); - public String getClientSession() { - return clientSession; - } - - public void setClientSession(String clientSession) { - this.clientSession = clientSession; - } - public Map getNotes() { return notes; } @@ -125,19 +114,18 @@ public class RestartLoginCookie { public RestartLoginCookie() { } - public RestartLoginCookie(ClientSessionModel clientSession) { + public RestartLoginCookie(AuthenticationSessionModel clientSession) { this.action = clientSession.getAction(); this.clientId = clientSession.getClient().getClientId(); - this.authMethod = clientSession.getAuthMethod(); + this.authMethod = clientSession.getProtocol(); this.redirectUri = clientSession.getRedirectUri(); - this.clientSession = clientSession.getId(); - for (Map.Entry entry : clientSession.getNotes().entrySet()) { + for (Map.Entry entry : clientSession.getClientNotes().entrySet()) { notes.put(entry.getKey(), entry.getValue()); } } - public static void setRestartCookie(KeycloakSession session, RealmModel realm, ClientConnection connection, UriInfo uriInfo, ClientSessionModel clientSession) { - RestartLoginCookie restart = new RestartLoginCookie(clientSession); + public static void setRestartCookie(KeycloakSession session, RealmModel realm, ClientConnection connection, UriInfo uriInfo, AuthenticationSessionModel authSession) { + RestartLoginCookie restart = new RestartLoginCookie(authSession); String encoded = restart.encode(session, realm); String path = AuthenticationManager.getRealmCookiePath(realm, uriInfo); boolean secureOnly = realm.getSslRequired().isRequired(connection); @@ -150,7 +138,8 @@ public class RestartLoginCookie { CookieHelper.addCookie(KC_RESTART, "", path, null, null, 0, secureOnly, true); } - public static ClientSessionModel restartSession(KeycloakSession session, RealmModel realm, String code) throws Exception { + + public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm) throws Exception { Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART); if (cook == null) { logger.debug("KC_RESTART cookie doesn't exist"); @@ -164,24 +153,18 @@ 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; - } ClientModel client = realm.getClientByClientId(cookie.getClientId()); if (client == null) return null; - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(cookie.getAuthMethod()); - clientSession.setRedirectUri(cookie.getRedirectUri()); - clientSession.setAction(cookie.getAction()); + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true); + authSession.setProtocol(cookie.getAuthMethod()); + authSession.setRedirectUri(cookie.getRedirectUri()); + authSession.setAction(cookie.getAction()); for (Map.Entry entry : cookie.getNotes().entrySet()) { - clientSession.setNote(entry.getKey(), entry.getValue()); + authSession.setClientNote(entry.getKey(), entry.getValue()); } - return clientSession; + return authSession; } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 4c0691a974..13d24a7926 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -23,8 +23,8 @@ import org.keycloak.common.util.Time; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; @@ -36,8 +36,11 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseType; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; +import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; import javax.ws.rs.core.HttpHeaders; @@ -128,9 +131,7 @@ public class OIDCLoginProtocol implements LoginProtocol { } - private void setupResponseTypeAndMode(ClientSessionModel clientSession) { - String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); - String responseMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM); + private void setupResponseTypeAndMode(String responseType, String responseMode) { this.responseType = OIDCResponseType.parse(responseType); this.responseMode = OIDCResponseMode.parse(responseMode, this.responseType); this.event.detail(Details.RESPONSE_TYPE, responseType); @@ -169,9 +170,12 @@ public class OIDCLoginProtocol implements LoginProtocol { @Override - public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { - ClientSessionModel clientSession = accessCode.getClientSession(); - setupResponseTypeAndMode(clientSession); + public Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, clientSession); + + String responseTypeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + String responseModeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM); + setupResponseTypeAndMode(responseTypeParam, responseModeParam); String redirect = clientSession.getRedirectUri(); OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode); @@ -182,7 +186,7 @@ public class OIDCLoginProtocol implements LoginProtocol { // Standard or hybrid flow if (responseType.hasResponseType(OIDCResponseType.CODE)) { - accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name()); + accessCode.setAction(CommonClientSessionModel.Action.CODE_TO_TOKEN.name()); redirectUri.addParam(OAuth2Constants.CODE, accessCode.getCode()); } @@ -227,16 +231,17 @@ public class OIDCLoginProtocol implements LoginProtocol { @Override - public Response sendError(ClientSessionModel clientSession, Error error) { - setupResponseTypeAndMode(clientSession); + public Response sendError(AuthenticationSessionModel authSession, Error error) { + String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + String responseModeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM); + setupResponseTypeAndMode(responseTypeParam, responseModeParam); - String redirect = clientSession.getRedirectUri(); - String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); + String redirect = authSession.getRedirectUri(); + String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM); OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode).addParam(OAuth2Constants.ERROR, translateError(error)); if (state != null) redirectUri.addParam(OAuth2Constants.STATE, state); - session.sessions().removeClientSession(realm, clientSession); - RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); + new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true); return redirectUri.build(); } @@ -256,13 +261,13 @@ public class OIDCLoginProtocol implements LoginProtocol { } @Override - public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { + public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); new ResourceAdminManager(session).logoutClientSession(uriInfo.getRequestUri(), realm, client, clientSession); } @Override - public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { + public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { // todo oidc redirect support throw new RuntimeException("NOT IMPLEMENTED"); } @@ -289,18 +294,18 @@ public class OIDCLoginProtocol implements LoginProtocol { @Override - public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) { - return isPromptLogin(clientSession) || isAuthTimeExpired(userSession, clientSession); + public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) { + return isPromptLogin(authSession) || isAuthTimeExpired(userSession, authSession); } - protected boolean isPromptLogin(ClientSessionModel clientSession) { - String prompt = clientSession.getNote(OIDCLoginProtocol.PROMPT_PARAM); + protected boolean isPromptLogin(AuthenticationSessionModel authSession) { + String prompt = authSession.getClientNote(OIDCLoginProtocol.PROMPT_PARAM); return TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_LOGIN); } - protected boolean isAuthTimeExpired(UserSessionModel userSession, ClientSessionModel clientSession) { + protected boolean isAuthTimeExpired(UserSessionModel userSession, AuthenticationSessionModel authSession) { String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME); - String maxAge = clientSession.getNote(OIDCLoginProtocol.MAX_AGE_PARAM); + String maxAge = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM); if (maxAge == null) { return false; } @@ -310,7 +315,7 @@ public class OIDCLoginProtocol implements LoginProtocol { if (authTimeInt + maxAgeInt < Time.currentTime()) { logger.debugf("Authentication time is expired, needs to reauthenticate. userSession=%s, clientId=%s, maxAge=%d, authTime=%d", userSession.getId(), - clientSession.getClient().getId(), maxAgeInt, authTimeInt); + authSession.getClient().getId(), maxAgeInt, authTimeInt); return true; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 4cedd6bd98..9782b48717 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -31,14 +31,13 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.jose.jws.crypto.HashProvider; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.models.ModelException; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -58,8 +57,10 @@ import org.keycloak.representations.IDToken; import org.keycloak.representations.RefreshToken; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; import org.keycloak.common.util.Time; @@ -107,10 +108,10 @@ public class TokenManager { public static class TokenValidation { public final UserModel user; public final UserSessionModel userSession; - public final ClientSessionModel clientSession; + public final AuthenticatedClientSessionModel clientSession; public final AccessToken newToken; - public TokenValidation(UserModel user, UserSessionModel userSession, ClientSessionModel clientSession, AccessToken newToken) { + public TokenValidation(UserModel user, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession, AccessToken newToken) { this.user = user; this.userSession = userSession; this.clientSession = clientSession; @@ -129,29 +130,18 @@ public class TokenManager { } UserSessionModel userSession = null; - ClientSessionModel clientSession = null; if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { UserSessionManager sessionManager = new UserSessionManager(session); - clientSession = sessionManager.findOfflineClientSession(realm, oldToken.getClientSession()); - if (clientSession != null) { - userSession = clientSession.getUserSession(); - - if (userSession == null) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found"); - } - - String userSessionId = oldToken.getSessionState(); - if (!userSessionId.equals(userSession.getId())) { - throw new ModelException("User session don't match. Offline client session " + clientSession.getId() + ", It's user session " + userSession.getId() + - " Wanted user session: " + userSessionId); - } + userSession = sessionManager.findOfflineUserSession(realm, oldToken.getSessionState()); + if (userSession != null) { // Revoke timeouted offline userSession if (userSession.getLastSessionRefresh() < Time.currentTime() - realm.getOfflineSessionIdleTimeout()) { sessionManager.revokeOfflineUserSession(userSession); - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not active", "Offline user session session not active"); + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline session not active", "Offline session not active"); } + } } else { // Find userSession regularly for online tokens @@ -160,20 +150,14 @@ public class TokenManager { AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active"); } - - for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) { - if (clientSessionModel.getId().equals(oldToken.getClientSession())) { - clientSession = clientSessionModel; - break; - } - } } - if (clientSession == null) { - throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Client session not active", "Client session not active"); + if (userSession == null) { + throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found"); } - ClientModel client = clientSession.getClient(); + ClientModel client = session.getContext().getClient(); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); if (!client.getClientId().equals(oldToken.getIssuedFor())) { throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Unmatching clients", "Unmatching clients"); @@ -213,17 +197,23 @@ public class TokenManager { return false; } + ClientModel client = realm.getClientByClientId(token.getIssuedFor()); + if (client == null || !client.isEnabled()) { + return false; + } + UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); if (AuthenticationManager.isSessionValid(realm, userSession)) { - ClientSessionModel clientSession = session.sessions().getClientSession(realm, token.getClientSession()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); if (clientSession != null) { return true; } } + userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) { - ClientSessionModel clientSession = session.sessions().getOfflineClientSession(realm, token.getClientSession()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); if (clientSession != null) { return true; } @@ -232,7 +222,8 @@ public class TokenManager { return false; } - public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException { + public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, + String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException { RefreshToken refreshToken = verifyRefreshToken(session, realm, encodedRefreshToken); event.user(refreshToken.getSubject()).session(refreshToken.getSessionState()) @@ -349,7 +340,8 @@ public class TokenManager { } } - public AccessToken createClientAccessToken(KeycloakSession session, Set requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) { + public AccessToken createClientAccessToken(KeycloakSession session, Set requestedRoles, RealmModel realm, ClientModel client, UserModel user, UserSessionModel userSession, + AuthenticatedClientSessionModel clientSession) { AccessToken token = initToken(realm, client, user, userSession, clientSession, session.getContext().getUri()); for (RoleModel role : requestedRoles) { addComposites(token, role); @@ -358,58 +350,49 @@ public class TokenManager { return token; } - public static void attachClientSession(UserSessionModel session, ClientSessionModel clientSession) { - if (clientSession.getUserSession() != null) { - return; + + public static AuthenticatedClientSessionModel attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) { + ClientModel client = authSession.getClient(); + + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + if (clientSession == null) { + clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession); } - UserModel user = session.getUser(); - clientSession.setUserSession(session); - Set requestedRoles = new HashSet(); - // todo scope param protocol independent - String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE); - ClientModel client = clientSession.getClient(); - for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) { - requestedRoles.add(r.getId()); - } - clientSession.setRoles(requestedRoles); + clientSession.setRedirectUri(authSession.getRedirectUri()); + clientSession.setProtocol(authSession.getProtocol()); - Set requestedProtocolMappers = new HashSet(); - ClientTemplateModel clientTemplate = client.getClientTemplate(); - if (clientTemplate != null && client.useTemplateMappers()) { - for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) { - if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) { - requestedProtocolMappers.add(protocolMapper.getId()); - } - } + clientSession.setRoles(authSession.getRoles()); + clientSession.setProtocolMappers(authSession.getProtocolMappers()); - } - for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) { - if (protocolMapper.getProtocol().equals(clientSession.getAuthMethod())) { - requestedProtocolMappers.add(protocolMapper.getId()); - } - } - clientSession.setProtocolMappers(requestedProtocolMappers); - - Map transferredNotes = clientSession.getUserSessionNotes(); + Map transferredNotes = authSession.getClientNotes(); for (Map.Entry entry : transferredNotes.entrySet()) { - session.setNote(entry.getKey(), entry.getValue()); + clientSession.setNote(entry.getKey(), entry.getValue()); } + Map transferredUserSessionNotes = authSession.getUserSessionNotes(); + for (Map.Entry entry : transferredUserSessionNotes.entrySet()) { + userSession.setNote(entry.getKey(), entry.getValue()); + } + + clientSession.setTimestamp(Time.currentTime()); + + // Remove authentication session now + new AuthenticationSessionManager(session).removeAuthenticationSession(userSession.getRealm(), authSession, true); + + return clientSession; } - public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) { + public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, AuthenticatedClientSessionModel clientSession) { UserSessionModel userSession = clientSession.getUserSession(); if (userSession == null) { return; } clientSession.setUserSession(null); - clientSession.setRoles(null); - clientSession.setProtocolMappers(null); - if (userSession.getClientSessions().isEmpty()) { + if (userSession.getAuthenticatedClientSessions().isEmpty()) { sessions.removeUserSession(realm, userSession); } } @@ -543,8 +526,8 @@ public class TokenManager { } public AccessToken transformAccessToken(KeycloakSession session, AccessToken token, RealmModel realm, ClientModel client, UserModel user, - UserSessionModel userSession, ClientSessionModel clientSession) { - Set mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers(); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + Set mappings = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), client); KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); for (ProtocolMapperModel mapping : mappings) { @@ -558,8 +541,8 @@ public class TokenManager { } public AccessToken transformUserInfoAccessToken(KeycloakSession session, AccessToken token, RealmModel realm, ClientModel client, UserModel user, - UserSessionModel userSession, ClientSessionModel clientSession) { - Set mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers(); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + Set mappings = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), client); KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); for (ProtocolMapperModel mapping : mappings) { @@ -573,8 +556,8 @@ public class TokenManager { } public void transformIDToken(KeycloakSession session, IDToken token, RealmModel realm, ClientModel client, UserModel user, - UserSessionModel userSession, ClientSessionModel clientSession) { - Set mappings = new ClientSessionCode(session, realm, clientSession).getRequestedProtocolMappers(); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + Set mappings = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), client); KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); for (ProtocolMapperModel mapping : mappings) { @@ -585,9 +568,8 @@ public class TokenManager { } } - protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, ClientSessionModel clientSession, UriInfo uriInfo) { + protected AccessToken initToken(RealmModel realm, ClientModel client, UserModel user, UserSessionModel session, AuthenticatedClientSessionModel clientSession, UriInfo uriInfo) { AccessToken token = new AccessToken(); - if (clientSession != null) token.clientSession(clientSession.getId()); token.id(KeycloakModelUtils.generateId()); token.type(TokenUtil.TOKEN_TYPE_BEARER); token.subject(user.getId()); @@ -607,9 +589,9 @@ public class TokenManager { token.setAuthTime(Integer.parseInt(authTime)); } - if (session != null) { - token.setSessionState(session.getId()); - } + + token.setSessionState(session.getId()); + int tokenLifespan = getTokenLifespan(realm, clientSession); if (tokenLifespan > 0) { token.expiration(Time.currentTime() + tokenLifespan); @@ -621,7 +603,7 @@ public class TokenManager { return token; } - private int getTokenLifespan(RealmModel realm, ClientSessionModel clientSession) { + private int getTokenLifespan(RealmModel realm, AuthenticatedClientSessionModel clientSession) { boolean implicitFlow = false; String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); if (responseType != null) { @@ -663,7 +645,7 @@ public class TokenManager { return new JWSBuilder().type(JWT).kid(activeRsaKey.getKid()).jsonContent(token).sign(jwsAlgorithm, activeRsaKey.getPrivateKey()); } - public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public AccessTokenResponseBuilder responseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { return new AccessTokenResponseBuilder(realm, client, event, session, userSession, clientSession); } @@ -673,7 +655,7 @@ public class TokenManager { EventBuilder event; KeycloakSession session; UserSessionModel userSession; - ClientSessionModel clientSession; + AuthenticatedClientSessionModel clientSession; AccessToken accessToken; RefreshToken refreshToken; @@ -682,7 +664,7 @@ public class TokenManager { boolean generateAccessTokenHash = false; String codeHash; - public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public AccessTokenResponseBuilder(RealmModel realm, ClientModel client, EventBuilder event, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { this.realm = realm; this.client = client; this.event = event; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index 1588321f31..26d012b0e1 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -28,7 +28,6 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.protocol.AuthorizationEndpointBase; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -44,6 +43,7 @@ import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.util.CacheControlUtil; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.TokenUtil; import javax.ws.rs.GET; @@ -63,12 +63,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { public static final String CODE_AUTH_TYPE = "code"; /** - * Prefix used to store additional HTTP GET params from original client request into {@link ClientSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to + * Prefix used to store additional HTTP GET params from original client request into {@link AuthenticationSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to * prevent collisions with internally used notes. * - * @see ClientSessionModel#getNote(String) + * @see AuthenticationSessionModel#getClientNote(String) */ - public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_"; + public static final String LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_"; // https://tools.ietf.org/html/rfc7636#section-4.2 private static final Pattern VALID_CODE_CHALLENGE_PATTERN = Pattern.compile("^[0-9a-zA-Z\\-\\.~_]+$"); @@ -78,7 +78,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } private ClientModel client; - private ClientSessionModel clientSession; + private AuthenticationSessionModel authenticationSession; private Action action; private OIDCResponseType parsedResponseType; @@ -125,7 +125,14 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { return errorResponse; } - createClientSession(); + AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, request.getState()); + if (checks.response != null) { + return checks.response; + } + + authenticationSession = checks.authSession; + updateAuthenticationSession(); + // So back button doesn't work CacheControlUtil.noBackButtonCacheControlHeader(); switch (action) { @@ -162,6 +169,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { return this; } + private void checkSsl() { if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { event.error(Errors.SSL_REQUIRED); @@ -356,44 +364,62 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { } } - private void createClientSession() { - clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - clientSession.setRedirectUri(redirectUri); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType()); - clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam()); - clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - if (request.getState() != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState()); - if (request.getNonce() != null) clientSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce()); - if (request.getMaxAge() != null) clientSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge())); - if (request.getScope() != null) clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope()); - if (request.getLoginHint() != null) clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint()); - if (request.getPrompt() != null) clientSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt()); - if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint()); - if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode()); + @Override + protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String stateFromRequest) { + if (stateFromRequest==null) { + return true; + } + + // Check if it's different client + if (!clientFromRequest.equals(authSession.getClient())) { + return true; + } + + // If state is same, we likely have the refresh of some previous request + String stateFromSession = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM); + return !stateFromRequest.equals(stateFromSession); + } + + + private void updateAuthenticationSession() { + authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + authenticationSession.setRedirectUri(redirectUri); + authenticationSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); + authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType()); + authenticationSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam()); + authenticationSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + + if (request.getState() != null) authenticationSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, request.getState()); + if (request.getNonce() != null) authenticationSession.setClientNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce()); + if (request.getMaxAge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge())); + if (request.getScope() != null) authenticationSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope()); + if (request.getLoginHint() != null) authenticationSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint()); + if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt()); + if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint()); + if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode()); // https://tools.ietf.org/html/rfc7636#section-4 - if (request.getCodeChallenge() != null) clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge()); + if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge()); if (request.getCodeChallengeMethod() != null) { - clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod()); + authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod()); } else { - clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN); + authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN); } if (request.getAdditionalReqParams() != null) { for (String paramName : request.getAdditionalReqParams().keySet()) { - clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName)); + authenticationSession.setClientNote(LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName)); } } } + private Response buildAuthorizationCodeAuthorizationResponse() { this.event.event(EventType.LOGIN); - clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); + authenticationSession.setAuthNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); - return handleBrowserAuthenticationRequest(clientSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false); + return handleBrowserAuthenticationRequest(authenticationSession, new OIDCLoginProtocol(session, realm, uriInfo, headers, event), TokenUtil.hasPrompt(request.getPrompt(), OIDCLoginProtocol.PROMPT_VALUE_NONE), false); } private Response buildRegister() { @@ -402,7 +428,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { AuthenticationFlowModel flow = realm.getRegistrationFlow(); String flowId = flow.getId(); - AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.REGISTRATION_PATH); + AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.REGISTRATION_PATH); + authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH); return processor.authenticate(); } @@ -413,7 +440,8 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase { AuthenticationFlowModel flow = realm.getResetCredentialsFlow(); String flowId = flow.getId(); - AuthenticationProcessor processor = createProcessor(clientSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH); + AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH); + authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH); return processor.authenticate(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 8fa4341344..83570efd75 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -32,13 +32,12 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.models.UserSessionProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; @@ -48,10 +47,12 @@ import org.keycloak.services.ErrorResponseException; import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.Cors; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.OPTIONS; import javax.ws.rs.POST; @@ -62,7 +63,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -175,6 +175,8 @@ public class TokenEndpoint { if (client.isBearerOnly()) { throw new ErrorResponseException(OAuthErrorException.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST); } + + } private void checkGrantType() { @@ -208,29 +210,35 @@ public class TokenEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST); } - ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm); - if (parseResult.isClientSessionNotFound() || parseResult.isIllegalHash()) { - String[] parts = code.split("\\."); - if (parts.length == 2) { - event.detail(Details.CODE_ID, parts[1]); - } + String[] parts = code.split("\\."); + if (parts.length == 4) { + event.detail(Details.CODE_ID, parts[2]); + } + + ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm, AuthenticatedClientSessionModel.class); + if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) { event.error(Errors.INVALID_CODE); - if (parseResult.getClientSession() != null) { - session.sessions().removeClientSession(realm, parseResult.getClientSession()); + + // Attempt to use same code twice should invalidate existing clientSession + AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); + if (clientSession != null) { + clientSession.setUserSession(null); } + throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code not valid", Response.Status.BAD_REQUEST); } - ClientSessionModel clientSession = parseResult.getClientSession(); - event.detail(Details.CODE_ID, clientSession.getId()); + AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); - if (!parseResult.getCode().isValid(ClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) { + if (!parseResult.getCode().isValid(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), ClientSessionCode.ActionType.CLIENT)) { event.error(Errors.INVALID_CODE); throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Code is expired", Response.Status.BAD_REQUEST); } + // TODO: This shouldn't be needed to write into the AuthenticatedClientSessionModel itself parseResult.getCode().setAction(null); + // TODO: Maybe rather create userSession even at this stage? UserSessionModel userSession = clientSession.getUserSession(); if (userSession == null) { @@ -355,7 +363,8 @@ public class TokenEndpoint { if (!result.isOfflineToken()) { UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState()); - updateClientSessions(userSession.getClientSessions()); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + updateClientSession(clientSession); updateUserSessionFromClientAuth(userSession); } @@ -369,7 +378,7 @@ public class TokenEndpoint { return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); } - private void updateClientSession(ClientSessionModel clientSession) { + private void updateClientSession(AuthenticatedClientSessionModel clientSession) { if(clientSession == null) { ServicesLogger.LOGGER.clientSessionNull(); @@ -388,26 +397,6 @@ public class TokenEndpoint { } } - private void updateClientSessions(List clientSessions) { - if(clientSessions == null) { - ServicesLogger.LOGGER.clientSessionNull(); - return; - } - for (ClientSessionModel clientSession : clientSessions) { - if(clientSession == null) { - ServicesLogger.LOGGER.clientSessionNull(); - continue; - } - if(clientSession.getClient() == null) { - ServicesLogger.LOGGER.clientModelNull(); - continue; - } - if(client.getId().equals(clientSession.getClient().getId())) { - updateClientSession(clientSession); - } - } - } - private void updateUserSessionFromClientAuth(UserSessionModel userSession) { for (Map.Entry attr : clientAuthAttributes.entrySet()) { userSession.setNote(attr.getKey(), attr.getValue()); @@ -428,17 +417,16 @@ public class TokenEndpoint { } String scope = formParams.getFirst(OAuth2Constants.SCOPE); - UserSessionProvider sessions = session.sessions(); - ClientSessionModel clientSession = sessions.createClientSession(realm, client); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope); + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name()); + authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); AuthenticationFlowModel flow = realm.getDirectGrantFlow(); String flowId = flow.getId(); AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clientSession) + processor.setAuthenticationSession(authSession) .setFlowId(flowId) .setConnection(clientConnection) .setEventBuilder(event) @@ -449,13 +437,16 @@ public class TokenEndpoint { Response challenge = processor.authenticateOnly(); if (challenge != null) return challenge; processor.evaluateRequiredActionTriggers(); - UserModel user = clientSession.getAuthenticatedUser(); + UserModel user = authSession.getAuthenticatedUser(); if (user.getRequiredActions() != null && user.getRequiredActions().size() > 0) { event.error(Errors.RESOLVE_REQUIRED_ACTIONS); throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Account is not fully set up", Response.Status.BAD_REQUEST); } - processor.attachSession(); + + AuthenticationManager.setRolesAndMappersInSession(authSession); + + AuthenticatedClientSessionModel clientSession = processor.attachSession(); UserSessionModel userSession = processor.getUserSession(); updateUserSessionFromClientAuth(userSession); @@ -505,17 +496,17 @@ public class TokenEndpoint { String scope = formParams.getFirst(OAuth2Constants.SCOPE); - UserSessionProvider sessions = session.sessions(); + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false); + authSession.setAuthenticatedUser(clientUser); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); - ClientSessionModel clientSession = sessions.createClientSession(realm, client); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope); - - UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null); + UserSessionModel userSession = session.sessions().createUserSession(authSession.getId(), realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null); event.session(userSession); - TokenManager.attachClientSession(userSession, clientSession); + AuthenticationManager.setRolesAndMappersInSession(authSession); + AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession); // Notes about client details userSession.setNote(ServiceAccountConstants.CLIENT_ID, client.getClientId()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index 8984a4db56..6ee2be3b0b 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -29,6 +29,7 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; @@ -139,24 +140,7 @@ public class UserInfoEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token invalid: " + e.getMessage(), Response.Status.UNAUTHORIZED); } - UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); - ClientSessionModel clientSession = session.sessions().getClientSession(token.getClientSession()); - if( userSession == null ) { - userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); - if( AuthenticationManager.isOfflineSessionValid(realm, userSession)) { - clientSession = session.sessions().getOfflineClientSession(realm, token.getClientSession()); - } else { - userSession = null; - clientSession = null; - } - } - - if (userSession == null) { - event.error(Errors.USER_SESSION_NOT_FOUND); - throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST); - } - - event.session(userSession); + UserSessionModel userSession = findValidSession(token, event); UserModel userModel = userSession.getUser(); if (userModel == null) { @@ -168,11 +152,6 @@ public class UserInfoEndpoint { .detail(Details.USERNAME, userModel.getUsername()); - if (clientSession == null || !AuthenticationManager.isSessionValid(realm, userSession)) { - event.error(Errors.SESSION_EXPIRED); - throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED); - } - ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor()); if (clientModel == null) { event.error(Errors.CLIENT_NOT_FOUND); @@ -186,6 +165,12 @@ public class UserInfoEndpoint { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST); } + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId()); + if (clientSession == null) { + event.error(Errors.SESSION_EXPIRED); + throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED); + } + AccessToken userInfo = new AccessToken(); tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession); @@ -224,4 +209,34 @@ public class UserInfoEndpoint { return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build(); } + + private UserSessionModel findValidSession(AccessToken token, EventBuilder event) { + UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState()); + UserSessionModel offlineUserSession = null; + if (AuthenticationManager.isSessionValid(realm, userSession)) { + event.session(userSession); + return userSession; + } else { + offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState()); + if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) { + event.session(offlineUserSession); + return offlineUserSession; + } + } + + if (userSession == null && offlineUserSession == null) { + event.error(Errors.USER_SESSION_NOT_FOUND); + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST); + } + + if (userSession != null) { + event.session(userSession); + } else { + event.session(offlineUserSession); + } + + event.error(Errors.SESSION_EXPIRED); + throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED); + } + } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java index efe9434b84..d267f9128d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractOIDCProtocolMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.oidc.mappers; import org.keycloak.Config; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ProtocolMapperModel; @@ -61,7 +61,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { } public AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { if (!OIDCAttributeMapperHelper.includeInUserInfo(mappingModel)) { return token; @@ -72,7 +72,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { } public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)){ return token; @@ -83,7 +83,7 @@ public abstract class AbstractOIDCProtocolMapper implements ProtocolMapper { } public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)){ return token; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java index 4666034705..09b39c4ae2 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractPairwiseSubMapper.java @@ -1,7 +1,7 @@ package org.keycloak.protocol.oidc.mappers; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperContainerModel; import org.keycloak.models.ProtocolMapperModel; @@ -64,19 +64,19 @@ public abstract class AbstractPairwiseSubMapper extends AbstractOIDCProtocolMapp } @Override - public final IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public final IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId())); return token; } @Override - public final AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public final AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId())); return token; } @Override - public final AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public final AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { setSubject(token, generateSub(mappingModel, getSectorIdentifier(clientSession.getClient(), mappingModel), userSession.getUser().getId())); return token; } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java index f4ef89dfc0..d239951bf9 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AbstractUserRoleMappingMapper.java @@ -93,9 +93,9 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper // get a set of all realm roles assigned to the user or its group Stream clientUserRoles = getAllUserRolesStream(user).filter(restriction); - boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed()); + boolean dontLimitScope = userSession.getAuthenticatedClientSessions().values().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed()); if (! dontLimitScope) { - Set clientRoles = userSession.getClientSessions().stream() + Set clientRoles = userSession.getAuthenticatedClientSessions().values().stream() .flatMap(cs -> cs.getClient().getScopeMappings().stream()) .collect(Collectors.toSet()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java index 1e4ad9df09..1e9b3e251f 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/FullNameMapper.java @@ -17,14 +17,11 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java index 41dbb47db9..b733f5c1e1 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java @@ -17,15 +17,12 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.GroupModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java index 40628245dd..8d48ccf47d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedClaim.java @@ -17,13 +17,10 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java index 03ecb91eb6..19ff92559e 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/HardcodedRole.java @@ -17,7 +17,7 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -82,7 +82,7 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { String role = mappingModel.getConfig().get(ROLE_CONFIG); String[] scopedRole = KeycloakModelUtils.parseRole(role); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java index 71dce26829..e7e0b7b422 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCAccessTokenMapper.java @@ -17,7 +17,7 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +30,5 @@ import org.keycloak.representations.AccessToken; public interface OIDCAccessTokenMapper { AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java index dabc4a35cd..54f380b165 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/OIDCIDTokenMapper.java @@ -17,7 +17,7 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +30,5 @@ import org.keycloak.representations.IDToken; public interface OIDCIDTokenMapper { IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java index fcdc373904..d41063b72e 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/RoleNameMapper.java @@ -17,7 +17,7 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -25,7 +25,6 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; -import org.keycloak.representations.IDToken; import java.util.ArrayList; import java.util.HashMap; @@ -90,7 +89,7 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc @Override public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession) { + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { String role = mappingModel.getConfig().get(ROLE_CONFIG); String newName = mappingModel.getConfig().get(NEW_ROLE_NAME); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java index e6d0d209f5..9b2cf0f24a 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java @@ -17,15 +17,12 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java index 67ac1a2797..e1fc17e900 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserInfoTokenMapper.java @@ -17,7 +17,7 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -29,5 +29,5 @@ import org.keycloak.representations.AccessToken; public interface UserInfoTokenMapper { AccessToken transformUserInfoToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java index 6fd649199d..2fc84ff1e9 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserPropertyMapper.java @@ -17,14 +17,11 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java index fd6bfe1c68..aadee6c9db 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserSessionNoteMapper.java @@ -17,14 +17,11 @@ package org.keycloak.protocol.oidc.mappers; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; import java.util.ArrayList; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java index 6e4498ee50..ffff19c818 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java @@ -53,6 +53,8 @@ public class AuthorizeClientUtil { throw new ErrorResponseException("invalid_client", "Client authentication ended, but client is null", Response.Status.BAD_REQUEST); } + session.getContext().setClient(client); + return new ClientAuthResult(client, processor.getClientAuthAttributes()); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java index 20d86c0404..a8218c17ac 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlProtocol.java @@ -30,8 +30,8 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.protocol.ResponseType; import org.keycloak.events.EventBuilder; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; @@ -57,10 +57,13 @@ import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; import org.keycloak.services.ErrorPage; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ResourceAdminManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.RealmsResource; +import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.AuthenticationSessionModel; import org.w3c.dom.Document; import javax.ws.rs.core.HttpHeaders; @@ -156,9 +159,9 @@ public class SamlProtocol implements LoginProtocol { } @Override - public Response sendError(ClientSessionModel clientSession, Error error) { + public Response sendError(AuthenticationSessionModel authSession, Error error) { try { - ClientModel client = clientSession.getClient(); + ClientModel client = authSession.getClient(); if ("true".equals(client.getAttribute(SAML_IDP_INITIATED_LOGIN))) { if (error == Error.CANCELLED_BY_USER) { @@ -173,9 +176,9 @@ public class SamlProtocol implements LoginProtocol { return ErrorPage.error(session, translateErrorToIdpInitiatedErrorMessage(error)); } } else { - SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(clientSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get()); + SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(authSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get()); try { - JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)); + JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(authSession.getClientNote(GeneralConstants.RELAY_STATE)); SamlClient samlClient = new SamlClient(client); KeyManager keyManager = session.keys(); if (samlClient.requiresRealmSignature()) { @@ -198,22 +201,21 @@ public class SamlProtocol implements LoginProtocol { binding.encrypt(publicKey); } Document document = builder.buildDocument(); - return buildErrorResponse(clientSession, binding, document); + return buildErrorResponse(authSession, binding, document); } catch (Exception e) { return ErrorPage.error(session, Messages.FAILED_TO_PROCESS_RESPONSE); } } } finally { - RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo); - session.sessions().removeClientSession(realm, clientSession); + new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true); } } - protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { - if (isPostBinding(clientSession)) { - return binding.postBinding(document).response(clientSession.getRedirectUri()); + protected Response buildErrorResponse(AuthenticationSessionModel authSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { + if (isPostBinding(authSession)) { + return binding.postBinding(document).response(authSession.getRedirectUri()); } else { - return binding.redirectBinding(document).response(clientSession.getRedirectUri()); + return binding.redirectBinding(document).response(authSession.getRedirectUri()); } } @@ -248,7 +250,13 @@ public class SamlProtocol implements LoginProtocol { return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(); } - protected boolean isPostBinding(ClientSessionModel clientSession) { + protected boolean isPostBinding(AuthenticationSessionModel authSession) { + ClientModel client = authSession.getClient(); + SamlClient samlClient = new SamlClient(client); + return SamlProtocol.SAML_POST_BINDING.equals(authSession.getClientNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding(); + } + + protected boolean isPostBinding(AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); SamlClient samlClient = new SamlClient(client); return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding(); @@ -259,7 +267,7 @@ public class SamlProtocol implements LoginProtocol { return SamlProtocol.SAML_POST_BINDING.equals(note); } - protected boolean isLogoutPostBindingForClient(ClientSessionModel clientSession) { + protected boolean isLogoutPostBindingForClient(AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); SamlClient samlClient = new SamlClient(client); String logoutPostUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_POST_ATTRIBUTE); @@ -284,7 +292,7 @@ public class SamlProtocol implements LoginProtocol { return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty()); } - protected String getNameIdFormat(SamlClient samlClient, ClientSessionModel clientSession) { + protected String getNameIdFormat(SamlClient samlClient, AuthenticatedClientSessionModel clientSession) { String nameIdFormat = clientSession.getNote(GeneralConstants.NAMEID_FORMAT); boolean forceFormat = samlClient.forceNameIDFormat(); @@ -297,7 +305,7 @@ public class SamlProtocol implements LoginProtocol { return nameIdFormat; } - protected String getNameId(String nameIdFormat, ClientSessionModel clientSession, UserSessionModel userSession) { + protected String getNameId(String nameIdFormat, CommonClientSessionModel clientSession, UserSessionModel userSession) { if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) { return userSession.getUser().getEmail(); } else if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get())) { @@ -327,7 +335,7 @@ public class SamlProtocol implements LoginProtocol { * * @return the user's persistent NameId */ - protected String getPersistentNameId(final ClientSessionModel clientSession, final UserSessionModel userSession) { + protected String getPersistentNameId(final CommonClientSessionModel clientSession, final UserSessionModel userSession) { // attempt to retrieve the UserID for the client-specific attribute final UserModel user = userSession.getUser(); final String clientNameId = String.format("%s.%s", SAML_PERSISTENT_NAME_ID_FOR, @@ -351,8 +359,8 @@ public class SamlProtocol implements LoginProtocol { } @Override - public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { - ClientSessionModel clientSession = accessCode.getClientSession(); + public Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { + ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, clientSession); ClientModel client = clientSession.getClient(); SamlClient samlClient = new SamlClient(client); String requestID = clientSession.getNote(SAML_REQUEST_ID); @@ -368,8 +376,12 @@ public class SamlProtocol implements LoginProtocol { clientSession.setNote(SAML_NAME_ID_FORMAT, nameIdFormat); SAML2LoginResponseBuilder builder = new SAML2LoginResponseBuilder(); - builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()).sessionIndex(clientSession.getId()) + builder.requestID(requestID).destination(redirectUri).issuer(responseIssuer).assertionExpiration(realm.getAccessCodeLifespan()).subjectExpiration(realm.getAccessTokenLifespan()) .requestIssuer(clientSession.getClient().getClientId()).nameIdentifier(nameIdFormat, nameId).authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get()); + + String sessionIndex = SamlSessionUtils.getSessionIndex(clientSession); + builder.sessionIndex(sessionIndex); + if (!samlClient.includeAuthnStatement()) { builder.disableAuthnStatement(true); } @@ -460,7 +472,7 @@ public class SamlProtocol implements LoginProtocol { } } - protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { + protected Response buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { if (isPostBinding(clientSession)) { return bindingBuilder.postBinding(samlDocument).response(redirectUri); } else { @@ -479,7 +491,7 @@ public class SamlProtocol implements LoginProtocol { } public AttributeStatementType populateAttributeStatements(List> attributeStatementMappers, KeycloakSession session, UserSessionModel userSession, - ClientSessionModel clientSession) { + AuthenticatedClientSessionModel clientSession) { AttributeStatementType attributeStatement = new AttributeStatementType(); for (ProtocolMapperProcessor processor : attributeStatementMappers) { processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession); @@ -488,14 +500,14 @@ public class SamlProtocol implements LoginProtocol { return attributeStatement; } - public ResponseType transformLoginResponse(List> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public ResponseType transformLoginResponse(List> mappers, ResponseType response, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { for (ProtocolMapperProcessor processor : mappers) { response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession); } return response; } - public void populateRoles(ProtocolMapperProcessor roleListMapper, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, + public void populateRoles(ProtocolMapperProcessor roleListMapper, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession, final AttributeStatementType existingAttributeStatement) { if (roleListMapper == null) return; @@ -509,8 +521,8 @@ public class SamlProtocol implements LoginProtocol { } else { logoutServiceUrl = client.getAttribute(SAML_SINGLE_LOGOUT_SERVICE_URL_REDIRECT_ATTRIBUTE); } - if (logoutServiceUrl == null && client instanceof ClientModel) - logoutServiceUrl = ((ClientModel) client).getManagementUrl(); + if (logoutServiceUrl == null) + logoutServiceUrl = client.getManagementUrl(); if (logoutServiceUrl == null || logoutServiceUrl.trim().equals("")) return null; return ResourceAdminManager.resolveUri(uriInfo.getRequestUri(), client.getRootUrl(), logoutServiceUrl); @@ -518,11 +530,9 @@ public class SamlProtocol implements LoginProtocol { } @Override - public Response frontchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { + public Response frontchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); SamlClient samlClient = new SamlClient(client); - if (!(client instanceof ClientModel)) - return null; try { boolean postBinding = isLogoutPostBindingForClient(clientSession); String bindingUri = getLogoutServiceUrl(uriInfo, client, postBinding ? SAML_POST_BINDING : SAML_REDIRECT_BINDING); @@ -615,7 +625,7 @@ public class SamlProtocol implements LoginProtocol { } @Override - public void backchannelLogout(UserSessionModel userSession, ClientSessionModel clientSession) { + public void backchannelLogout(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { ClientModel client = clientSession.getClient(); SamlClient samlClient = new SamlClient(client); String logoutUrl = getLogoutServiceUrl(uriInfo, client, SAML_POST_BINDING); @@ -674,15 +684,19 @@ public class SamlProtocol implements LoginProtocol { } - protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, ClientSessionModel clientSession, ClientModel client) { + protected SAML2LogoutRequestBuilder createLogoutRequest(String logoutUrl, AuthenticatedClientSessionModel clientSession, ClientModel client) { // build userPrincipal with subject used at login - SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)).sessionIndex(clientSession.getId()) + SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder().assertionExpiration(realm.getAccessCodeLifespan()).issuer(getResponseIssuer(realm)) .userPrincipal(clientSession.getNote(SAML_NAME_ID), clientSession.getNote(SAML_NAME_ID_FORMAT)).destination(logoutUrl); + + String sessionIndex = SamlSessionUtils.getSessionIndex(clientSession); + logoutBuilder.sessionIndex(sessionIndex); + return logoutBuilder; } @Override - public boolean requireReauthentication(UserSessionModel userSession, ClientSessionModel clientSession) { + public boolean requireReauthentication(UserSessionModel userSession, AuthenticationSessionModel authSession) { // Not yet supported return false; } diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java index d67faa2b27..9a6790ba4a 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -37,8 +37,8 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.keys.RsaKeyMetadata; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeyManager; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -86,9 +86,10 @@ import org.keycloak.rotation.HardcodedKeyLocator; import org.keycloak.rotation.KeyLocator; import org.keycloak.saml.SPMetadataDescriptor; import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator; +import org.keycloak.sessions.AuthenticationSessionModel; /** - * Resource class for the oauth/openid connect token service + * Resource class for the saml connect token service * * @author Bill Burke * @version $Revision: 1 $ @@ -97,9 +98,6 @@ public class SamlService extends AuthorizationEndpointBase { protected static final Logger logger = Logger.getLogger(SamlService.class); - @Context - protected KeycloakSession session; - public SamlService(RealmModel realm, EventBuilder event) { super(realm, event); } @@ -270,13 +268,19 @@ public class SamlService extends AuthorizationEndpointBase { return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI); } - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); - clientSession.setRedirectUri(redirect); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(SamlProtocol.SAML_BINDING, bindingType); - clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); - clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID()); + AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, relayState); + if (checks.response != null) { + return checks.response; + } + + AuthenticationSessionModel authSession = checks.authSession; + + authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL); + authSession.setRedirectUri(redirect); + authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); + authSession.setClientNote(SamlProtocol.SAML_BINDING, bindingType); + authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState); + authSession.setClientNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID()); // Handle NameIDPolicy from SP NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy(); @@ -285,7 +289,7 @@ public class SamlService extends AuthorizationEndpointBase { String nameIdFormat = nameIdFormatUri.toString(); // TODO: Handle AllowCreate too, relevant for persistent NameID. if (isSupportedNameIdFormat(nameIdFormat)) { - clientSession.setNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat); + authSession.setClientNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat); } else { event.detail(Details.REASON, "unsupported_nameid_format"); event.error(Errors.INVALID_SAML_AUTHN_REQUEST); @@ -301,13 +305,13 @@ public class SamlService extends AuthorizationEndpointBase { BaseIDAbstractType baseID = subject.getSubType().getBaseID(); if (baseID != null && baseID instanceof NameIDType) { NameIDType nameID = (NameIDType) baseID; - clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue()); + authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue()); } } } - return newBrowserAuthentication(clientSession, requestAbstractType.isIsPassive(), redirectToAuthentication); + return newBrowserAuthentication(authSession, requestAbstractType.isIsPassive(), redirectToAuthentication); } protected String getBindingType(AuthnRequestType requestAbstractType) { @@ -368,31 +372,22 @@ public class SamlService extends AuthorizationEndpointBase { userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod()); userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL); // remove client from logout requests - for (ClientSessionModel clientSession : userSession.getClientSessions()) { - if (clientSession.getClient().getId().equals(client.getId())) { - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); - } + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + if (clientSession != null) { + clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name()); } logger.debug("browser Logout"); return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers); } else if (logoutRequest.getSessionIndex() != null) { for (String sessionIndex : logoutRequest.getSessionIndex()) { - ClientSessionModel clientSession = session.sessions().getClientSession(realm, sessionIndex); + + AuthenticatedClientSessionModel clientSession = SamlSessionUtils.getClientSession(session, realm, sessionIndex); if (clientSession == null) continue; UserSessionModel userSession = clientSession.getUserSession(); if (clientSession.getClient().getClientId().equals(client.getClientId())) { // remove requesting client from logout - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); - - // Remove also other clientSessions of this client as there could be more in this UserSession - if (userSession != null) { - for (ClientSessionModel clientSession2 : userSession.getClientSessions()) { - if (clientSession2.getClient().getId().equals(client.getId())) { - clientSession2.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); - } - } - } + clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name()); } try { @@ -442,6 +437,16 @@ public class SamlService extends AuthorizationEndpointBase { return !realm.getSslRequired().isRequired(clientConnection); } } + + public Response execute(String samlRequest, String samlResponse, String relayState) { + Response response = basicChecks(samlRequest, samlResponse); + if (response != null) + return response; + if (samlRequest != null) + return handleSamlRequest(samlRequest, relayState); + else + return handleSamlResponse(samlResponse, relayState); + } } protected class PostBindingProtocol extends BindingProtocol { @@ -466,16 +471,6 @@ public class SamlService extends AuthorizationEndpointBase { return SamlProtocol.SAML_POST_BINDING; } - public Response execute(String samlRequest, String samlResponse, String relayState) { - Response response = basicChecks(samlRequest, samlResponse); - if (response != null) - return response; - if (samlRequest != null) - return handleSamlRequest(samlRequest, relayState); - else - return handleSamlResponse(samlResponse, relayState); - } - } protected class RedirectBindingProtocol extends BindingProtocol { @@ -506,25 +501,15 @@ public class SamlService extends AuthorizationEndpointBase { return SamlProtocol.SAML_REDIRECT_BINDING; } - public Response execute(String samlRequest, String samlResponse, String relayState) { - Response response = basicChecks(samlRequest, samlResponse); - if (response != null) - return response; - if (samlRequest != null) - return handleSamlRequest(samlRequest, relayState); - else - return handleSamlResponse(samlResponse, relayState); - } - } - protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, boolean redirectToAuthentication) { + protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication) { SamlProtocol samlProtocol = new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo); - return newBrowserAuthentication(clientSession, isPassive, redirectToAuthentication, samlProtocol); + return newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, samlProtocol); } - protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) { - return handleBrowserAuthenticationRequest(clientSession, samlProtocol, isPassive, redirectToAuthentication); + protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) { + return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication); } /** @@ -609,15 +594,19 @@ public class SamlService extends AuthorizationEndpointBase { event.error(Errors.CLIENT_NOT_FOUND); return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND); } + if (!client.isEnabled()) { + event.error(Errors.CLIENT_DISABLED); + return ErrorPage.error(session, Messages.CLIENT_DISABLED); + } if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) { logger.error("SAML assertion consumer url not set up"); event.error(Errors.INVALID_REDIRECT_URI); return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI); } - ClientSessionModel clientSession = createClientSessionForIdpInitiatedSso(this.session, this.realm, client, relayState); + AuthenticationSessionModel authSession = getOrCreateLoginSessionForIdpInitiatedSso(this.session, this.realm, client, relayState); - return newBrowserAuthentication(clientSession, false, false); + return newBrowserAuthentication(authSession, false, false); } /** @@ -631,7 +620,7 @@ public class SamlService extends AuthorizationEndpointBase { * @param relayState Optional relay state - free field as per SAML specification * @return */ - public static ClientSessionModel createClientSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) { + public AuthenticationSessionModel getOrCreateLoginSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) { String bindingType = SamlProtocol.SAML_POST_BINDING; if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) { bindingType = SamlProtocol.SAML_REDIRECT_BINDING; @@ -647,21 +636,47 @@ public class SamlService extends AuthorizationEndpointBase { redirect = client.getManagementUrl(); } - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING); - clientSession.setNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true"); - clientSession.setRedirectUri(redirect); + AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, null); + if (checks.response != null) { + throw new IllegalStateException("Not expected to detect re-sent request for IDP initiated SSO"); + } + + AuthenticationSessionModel authSession = checks.authSession; + authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL); + authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); + authSession.setClientNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING); + authSession.setClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true"); + authSession.setRedirectUri(redirect); if (relayState == null) { relayState = client.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_RELAY_STATE); } if (relayState != null && !relayState.trim().equals("")) { - clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); + authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState); } - return clientSession; + return authSession; + } + + + @Override + protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestRelayState) { + // No support of browser "refresh" or "back" buttons for SAML IDP initiated SSO. So always treat as new request + String idpInitiated = authSession.getClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN); + if (Boolean.parseBoolean(idpInitiated)) { + return true; + } + + if (requestRelayState == null) { + return true; + } + + // Check if it's different client + if (!clientFromRequest.equals(authSession.getClient())) { + return true; + } + + return !requestRelayState.equals(authSession.getClientNote(GeneralConstants.RELAY_STATE)); } @POST diff --git a/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java new file mode 100644 index 0000000000..0083fdc0dd --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/saml/SamlSessionUtils.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 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.protocol.saml; + +import java.util.regex.Pattern; + +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; + +/** + * @author Marek Posolda + */ +public class SamlSessionUtils { + + private static final String DELIMITER = "::"; + + // Just perf optimization + private static final Pattern PATTERN = Pattern.compile(DELIMITER); + + + public static String getSessionIndex(AuthenticatedClientSessionModel clientSession) { + UserSessionModel userSession = clientSession.getUserSession(); + ClientModel client = clientSession.getClient(); + + return userSession.getId() + DELIMITER + client.getId(); + } + + + public static AuthenticatedClientSessionModel getClientSession(KeycloakSession session, RealmModel realm, String sessionIndex) { + if (sessionIndex == null) { + return null; + } + + String[] parts = PATTERN.split(sessionIndex); + if (parts.length != 2) { + return null; + } + + UserSessionModel userSession = session.sessions().getUserSession(realm, parts[0]); + if (userSession == null) { + return null; + } + + return userSession.getAuthenticatedClientSessions().get(parts[1]); + } + +} diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java index 1a2db26f59..d193ee381b 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java @@ -19,7 +19,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; @@ -117,7 +117,7 @@ public class GroupMembershipMapper extends AbstractSAMLProtocolMapper implements @Override - public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { String single = mappingModel.getConfig().get(SINGLE_GROUP_ATTRIBUTE); boolean singleAttribute = Boolean.parseBoolean(single); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java index b8a62313d9..43c024154e 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/HardcodedAttributeMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -76,7 +76,7 @@ public class HardcodedAttributeMapper extends AbstractSAMLProtocolMapper impleme } @Override - public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { String attributeValue = mappingModel.getConfig().get(ATTRIBUTE_VALUE); AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java index 5650333c11..322735071d 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/RoleListMapper.java @@ -19,7 +19,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.ProtocolMapperModel; @@ -111,14 +111,14 @@ public class RoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRo } @Override - public void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { String single = mappingModel.getConfig().get(SINGLE_ROLE_ATTRIBUTE); boolean singleAttribute = Boolean.parseBoolean(single); List> roleNameMappers = new LinkedList<>(); KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); AttributeType singleAttributeType = null; - Set requestedProtocolMappers = new ClientSessionCode(session, clientSession.getRealm(), clientSession).getRequestedProtocolMappers(); + Set requestedProtocolMappers = ClientSessionCode.getRequestedProtocolMappers(clientSession.getProtocolMappers(), clientSession.getClient()); for (ProtocolMapperModel mapping : requestedProtocolMappers) { ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper()); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java index 48edfaa81b..a26b0e0334 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLAttributeStatementMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +30,5 @@ import org.keycloak.models.UserSessionModel; public interface SAMLAttributeStatementMapper { void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java index cf5c9c8bd4..1f962feaab 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLLoginResponseMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.protocol.ResponseType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +30,5 @@ import org.keycloak.models.UserSessionModel; public interface SAMLLoginResponseMapper { ResponseType transformLoginResponse(ResponseType response, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java index a822d8cff0..991c2238e8 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/SAMLRoleListMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -30,5 +30,5 @@ import org.keycloak.models.UserSessionModel; public interface SAMLRoleListMapper { void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, - UserSessionModel userSession, ClientSessionModel clientSession); + UserSessionModel userSession, AuthenticatedClientSessionModel clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java index f29d972234..2579af1c71 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; @@ -77,7 +77,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp } @Override - public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { UserModel user = userSession.getUser(); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); List attributeValues = KeycloakModelUtils.resolveAttribute(user, attributeName); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java index fd0de2a87c..1d7d0384f3 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserModel; @@ -76,7 +76,7 @@ public class UserPropertyAttributeStatementMapper extends AbstractSAMLProtocolMa } @Override - public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { UserModel user = userSession.getUser(); String propertyName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); String propertyValue = ProtocolMapperUtils.getUserModelValue(user, propertyName); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java index d6fd4d05a6..b4b24b5fec 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserSessionNoteStatementMapper.java @@ -18,7 +18,7 @@ package org.keycloak.protocol.saml.mappers; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.UserSessionModel; @@ -74,7 +74,7 @@ public class UserSessionNoteStatementMapper extends AbstractSAMLProtocolMapper i } @Override - public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { + public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) { String note = mappingModel.getConfig().get("note"); String value = userSession.getNote(note); if (value == null) return; diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java index d2aaad68e6..b90a165004 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java +++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/SamlEcpProfileService.java @@ -20,8 +20,8 @@ package org.keycloak.protocol.saml.profile.ecp; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; @@ -35,6 +35,7 @@ import org.keycloak.saml.common.constants.JBossSAMLConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ProcessingException; +import org.keycloak.sessions.AuthenticationSessionModel; import org.w3c.dom.Document; import javax.ws.rs.core.Response; @@ -85,15 +86,15 @@ public class SamlEcpProfileService extends SamlService { } @Override - protected Response newBrowserAuthentication(ClientSessionModel clientSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) { - return super.newBrowserAuthentication(clientSession, isPassive, redirectToAuthentication, createEcpSamlProtocol()); + protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) { + return super.newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, createEcpSamlProtocol()); } private SamlProtocol createEcpSamlProtocol() { return new SamlProtocol() { // method created to send a SOAP Binding response instead of a HTTP POST response @Override - protected Response buildAuthenticatedResponse(ClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { + protected Response buildAuthenticatedResponse(AuthenticatedClientSessionModel clientSession, String redirectUri, Document samlDocument, JaxrsSAML2BindingBuilder bindingBuilder) throws ConfigurationException, ProcessingException, IOException { Document document = bindingBuilder.postBinding(samlDocument).getDocument(); try { @@ -113,7 +114,7 @@ public class SamlEcpProfileService extends SamlService { } } - private void createRequestAuthenticatedHeader(ClientSessionModel clientSession, Soap.SoapMessageBuilder messageBuilder) { + private void createRequestAuthenticatedHeader(AuthenticatedClientSessionModel clientSession, Soap.SoapMessageBuilder messageBuilder) { ClientModel client = clientSession.getClient(); if ("true".equals(client.getAttribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE))) { @@ -133,7 +134,7 @@ public class SamlEcpProfileService extends SamlService { } @Override - protected Response buildErrorResponse(ClientSessionModel clientSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { + protected Response buildErrorResponse(AuthenticationSessionModel authSession, JaxrsSAML2BindingBuilder binding, Document document) throws ConfigurationException, ProcessingException, IOException { return Soap.createMessage().addToBody(document).build(); } diff --git a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java index ddaec72b80..f21eff3bf0 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java +++ b/services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java @@ -104,7 +104,7 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory { boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password)); if (valid) { - context.getClientSession().setAuthenticatedUser(user); + context.getAuthenticationSession().setAuthenticatedUser(user); context.success(); } else { context.getEvent().user(user); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 9d615a5c36..67cce1ff61 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -33,6 +33,7 @@ import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.UserCache; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; +import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.storage.UserStorageManager; import org.keycloak.storage.federated.UserFederatedStorageProvider; @@ -54,10 +55,10 @@ public class DefaultKeycloakSession implements KeycloakSession { private final DefaultKeycloakTransactionManager transactionManager; private final Map attributes = new HashMap<>(); private RealmProvider model; - private UserProvider userModel; private UserStorageManager userStorageManager; private UserCredentialStoreManager userCredentialStorageManager; private UserSessionProvider sessionProvider; + private AuthenticationSessionProvider authenticationSessionProvider; private UserFederatedStorageProvider userFederatedStorageProvider; private KeycloakContext context; private KeyManager keyManager; @@ -236,6 +237,14 @@ public class DefaultKeycloakSession implements KeycloakSession { return sessionProvider; } + @Override + public AuthenticationSessionProvider authenticationSessions() { + if (authenticationSessionProvider == null) { + authenticationSessionProvider = getProvider(AuthenticationSessionProvider.class); + } + return authenticationSessionProvider; + } + @Override public KeyManager keys() { if (keyManager == null) { diff --git a/services/src/main/java/org/keycloak/services/ErrorPageException.java b/services/src/main/java/org/keycloak/services/ErrorPageException.java index 4bcbbc8499..51ee9c84dc 100644 --- a/services/src/main/java/org/keycloak/services/ErrorPageException.java +++ b/services/src/main/java/org/keycloak/services/ErrorPageException.java @@ -37,6 +37,8 @@ public class ErrorPageException extends WebApplicationException { this.parameters = parameters; } + + @Override public Response getResponse() { return ErrorPage.error(session, errorMessage, parameters); diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index 21eb047159..e92aa05a74 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -178,8 +178,9 @@ public class Urls { return loginResetCredentialsBuilder(baseUri).build(realmName); } - public static UriBuilder executeActionsBuilder(URI baseUri) { - return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActions"); + public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) { + return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken") + .queryParam("key", tokenString); } public static UriBuilder loginResetCredentialsBuilder(URI baseUri) { @@ -206,6 +207,11 @@ public class Urls { return loginActionsBase(baseUri).path(LoginActionsService.class, "authenticate").build(realmName); } + public static URI realmLoginRestartPage(URI baseUri, String realmId) { + return loginActionsBase(baseUri).path(LoginActionsService.class, "restartSession") + .build(realmId); + } + private static UriBuilder realmLogout(URI baseUri) { return tokenBase(baseUri).path(OIDCLoginProtocolService.class, "logout"); } diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java index 714a3a2c70..8b6086e04b 100755 --- a/services/src/main/java/org/keycloak/services/managers/Auth.java +++ b/services/src/main/java/org/keycloak/services/managers/Auth.java @@ -17,8 +17,8 @@ package org.keycloak.services.managers; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; @@ -35,7 +35,7 @@ public class Auth { private final UserModel user; private final ClientModel client; private final UserSessionModel session; - private ClientSessionModel clientSession; + private AuthenticatedClientSessionModel clientSession; public Auth(RealmModel realm, AccessToken token, UserModel user, ClientModel client, UserSessionModel session, boolean cookie) { this.cookie = cookie; @@ -71,11 +71,11 @@ public class Auth { return session; } - public ClientSessionModel getClientSession() { + public AuthenticatedClientSessionModel getClientSession() { return clientSession; } - public void setClientSession(ClientSessionModel clientSession) { + public void setClientSession(AuthenticatedClientSessionModel clientSession) { this.clientSession = clientSession; } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index cf7d73cbd1..31217f1eae 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -19,11 +19,14 @@ package org.keycloak.services.managers; import org.jboss.logging.Logger; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.OAuth2Constants; import org.keycloak.TokenVerifier; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContextResult; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.common.ClientConnection; import org.keycloak.common.VerificationException; @@ -35,17 +38,7 @@ import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.jose.jws.AlgorithmType; import org.keycloak.jose.jws.JWSBuilder; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.KeyManager; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ProtocolMapperModel; -import org.keycloak.models.RealmModel; -import org.keycloak.models.RequiredActionProviderModel; -import org.keycloak.models.RoleModel; -import org.keycloak.models.UserConsentModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; +import org.keycloak.models.*; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol.Error; @@ -55,9 +48,12 @@ import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.IdentityBrokerService; +import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.P3PHelper; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.CommonClientSessionModel; import javax.crypto.SecretKey; import javax.ws.rs.core.Cookie; @@ -65,12 +61,11 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.security.PublicKey; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; /** * Stateless object that manages authentication @@ -81,6 +76,10 @@ import java.util.Set; public class AuthenticationManager { public static final String SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS= "SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS"; public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS"; + public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN"; + + // Last authenticated client in userSession. + public static final String LAST_AUTHENTICATED_CLIENT = "LAST_AUTHENTICATED_CLIENT"; // userSession note with authTime (time when authentication flow including requiredActions was finished) public static final String AUTH_TIME = "AUTH_TIME"; @@ -124,7 +123,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 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); @@ -159,7 +161,7 @@ public class AuthenticationManager { logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId()); expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection); - for (ClientSessionModel clientSession : userSession.getClientSessions()) { + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers); } if (logoutBroker) { @@ -169,6 +171,7 @@ public class AuthenticationManager { try { identityProvider.backchannelLogout(session, userSession, uriInfo, realm); } catch (Exception e) { + logger.warn("Exception at broker backchannel logout for broker " + brokerId, e); } } } @@ -176,17 +179,17 @@ public class AuthenticationManager { session.sessions().removeUserSession(realm, userSession); } - public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) { + public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, AuthenticatedClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) { ClientModel client = clientSession.getClient(); - if (client instanceof ClientModel && !client.isFrontchannelLogout() && !ClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) { - String authMethod = clientSession.getAuthMethod(); + if (!client.isFrontchannelLogout() && !AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) { + String authMethod = clientSession.getProtocol(); if (authMethod == null) return; // must be a keycloak service like account LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); protocol.setRealm(realm) .setHttpHeaders(headers) .setUriInfo(uriInfo); protocol.backchannelLogout(userSession, clientSession); - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); + clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); } } @@ -197,8 +200,8 @@ public class AuthenticationManager { List userSessions = session.sessions().getUserSessions(realm, user); for (UserSessionModel userSession : userSessions) { - List clientSessions = userSession.getClientSessions(); - for (ClientSessionModel clientSession : clientSessions) { + Collection clientSessions = userSession.getAuthenticatedClientSessions().values(); + for (AuthenticatedClientSessionModel clientSession : clientSessions) { if (clientSession.getClient().getId().equals(clientId)) { AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers); TokenManager.dettachClientSession(session.sessions(), realm, clientSession); @@ -215,16 +218,16 @@ public class AuthenticationManager { if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) { userSession.setState(UserSessionModel.State.LOGGING_OUT); } - List redirectClients = new LinkedList(); - for (ClientSessionModel clientSession : userSession.getClientSessions()) { + List redirectClients = new LinkedList<>(); + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { ClientModel client = clientSession.getClient(); - if (ClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue; + if (AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue; if (client.isFrontchannelLogout()) { - String authMethod = clientSession.getAuthMethod(); + String authMethod = clientSession.getProtocol(); if (authMethod == null) continue; // must be a keycloak service like account redirectClients.add(clientSession); } else { - String authMethod = clientSession.getAuthMethod(); + String authMethod = clientSession.getProtocol(); if (authMethod == null) continue; // must be a keycloak service like account LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); protocol.setRealm(realm) @@ -233,21 +236,21 @@ public class AuthenticationManager { try { logger.debugv("backchannel logout to: {0}", client.getClientId()); protocol.backchannelLogout(userSession, clientSession); - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); + clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); } catch (Exception e) { ServicesLogger.LOGGER.failedToLogoutClient(e); } } } - for (ClientSessionModel nextRedirectClient : redirectClients) { - String authMethod = nextRedirectClient.getAuthMethod(); + for (AuthenticatedClientSessionModel nextRedirectClient : redirectClients) { + String authMethod = nextRedirectClient.getProtocol(); LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); protocol.setRealm(realm) .setHttpHeaders(headers) .setUriInfo(uriInfo); // setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not - nextRedirectClient.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); + nextRedirectClient.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name()); try { logger.debugv("frontchannel logout to: {0}", nextRedirectClient.getClient().getClientId()); Response response = protocol.frontchannelLogout(userSession, nextRedirectClient); @@ -410,20 +413,20 @@ public class AuthenticationManager { public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession, - ClientSessionModel clientSession, + AuthenticatedClientSessionModel clientSession, HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, - EventBuilder event) { - LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); - protocol.setRealm(realm) + EventBuilder event, String protocol) { + LoginProtocol protocolImpl = session.getProvider(LoginProtocol.class, protocol); + protocolImpl.setRealm(realm) .setHttpHeaders(request.getHttpHeaders()) .setUriInfo(uriInfo) .setEventBuilder(event); - return redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event, protocol); + return redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event, protocolImpl); } public static Response redirectAfterSuccessfulFlow(KeycloakSession session, RealmModel realm, UserSessionModel userSession, - ClientSessionModel clientSession, + AuthenticatedClientSessionModel clientSession, HttpRequest request, UriInfo uriInfo, ClientConnection clientConnection, EventBuilder event, LoginProtocol protocol) { Cookie sessionCookie = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_SESSION_COOKIE); @@ -460,32 +463,66 @@ public class AuthenticationManager { userSession.setNote(AUTH_TIME, String.valueOf(authTime)); } - return protocol.authenticated(userSession, new ClientSessionCode(session, realm, clientSession)); + userSession.setNote(LAST_AUTHENTICATED_CLIENT, clientSession.getClient().getId()); + + return protocol.authenticated(userSession, clientSession); } - public static boolean isSSOAuthentication(ClientSessionModel clientSession) { + public static boolean isSSOAuthentication(AuthenticatedClientSessionModel clientSession) { String ssoAuth = clientSession.getNote(SSO_AUTH); return Boolean.parseBoolean(ssoAuth); } - public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, + public static Response nextActionAfterAuthentication(KeycloakSession session, AuthenticationSessionModel authSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { - Response requiredAction = actionRequired(session, userSession, clientSession, clientConnection, request, uriInfo, event); + Response requiredAction = actionRequired(session, authSession, clientConnection, request, uriInfo, event); if (requiredAction != null) return requiredAction; - return finishedRequiredActions(session, userSession, clientSession, clientConnection, request, uriInfo, event); + return finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event); } - public static Response finishedRequiredActions(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { - if (clientSession.getNote(END_AFTER_REQUIRED_ACTIONS) != null) { + + public static Response redirectToRequiredActions(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, UriInfo uriInfo, String requiredAction) { + // redirect to non-action url so browser refresh button works without reposting past data + ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession); + accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name()); + authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, LoginActionsService.REQUIRED_ACTION); + authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, requiredAction); + + UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo) + .path(LoginActionsService.REQUIRED_ACTION); + + if (requiredAction != null) { + uriBuilder.queryParam("execution", requiredAction); + } + + URI redirect = uriBuilder.build(realm.getName()); + return Response.status(302).location(redirect).build(); + + } + + + public static Response finishedRequiredActions(KeycloakSession session, AuthenticationSessionModel authSession, UserSessionModel userSession, + ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { + String actionTokenKeyToInvalidate = authSession.getAuthNote(INVALIDATE_ACTION_TOKEN); + if (actionTokenKeyToInvalidate != null) { + ActionTokenKeyModel actionTokenKey = DefaultActionTokenKey.from(actionTokenKeyToInvalidate); + + if (actionTokenKey != null) { + ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class); + actionTokenStore.put(actionTokenKey, null); // Token is invalidated + } + } + + if (authSession.getAuthNote(END_AFTER_REQUIRED_ACTIONS) != null) { LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class) .setSuccess(Messages.ACCOUNT_UPDATED); - if (clientSession.getNote(SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS) != null) { - if (clientSession.getRedirectUri() != null) { - infoPage.setAttribute("pageRedirectUri", clientSession.getRedirectUri()); + if (authSession.getAuthNote(SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS) != null) { + if (authSession.getRedirectUri() != null) { + infoPage.setAttribute("pageRedirectUri", authSession.getRedirectUri()); } } else { @@ -493,44 +530,56 @@ public class AuthenticationManager { } Response response = infoPage .createInfoPage(); - session.sessions().removeUserSession(session.getContext().getRealm(), userSession); return response; + // Don't remove authentication session for now, to ensure that browser buttons (back/refresh) will still work fine. + } + RealmModel realm = authSession.getRealm(); + + AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, userSession, session, realm, clientConnection, event); + + event.event(EventType.LOGIN); + event.session(clientSession.getUserSession()); event.success(); - RealmModel realm = clientSession.getRealm(); - return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection, event); + return redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol()); } - public static boolean isActionRequired(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, - final ClientConnection clientConnection, - final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) { - final RealmModel realm = clientSession.getRealm(); - final UserModel user = userSession.getUser(); - final ClientModel client = clientSession.getClient(); + // Return null if action is not required. Or the name of the requiredAction in case it is required. + public static String nextRequiredAction(final KeycloakSession session, final AuthenticationSessionModel authSession, + final ClientConnection clientConnection, + final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) { + final RealmModel realm = authSession.getRealm(); + final UserModel user = authSession.getAuthenticatedUser(); + final ClientModel client = authSession.getClient(); - evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user); + evaluateRequiredActionTriggers(session, authSession, clientConnection, request, uriInfo, event, realm, user); - if (!user.getRequiredActions().isEmpty() || !clientSession.getRequiredActions().isEmpty()) return true; + if (!user.getRequiredActions().isEmpty()) { + return user.getRequiredActions().iterator().next(); + } + if (!authSession.getRequiredActions().isEmpty()) { + return authSession.getRequiredActions().iterator().next(); + } if (client.isConsentRequired()) { UserConsentModel grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId()); - ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); + ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession); for (RoleModel r : accessCode.getRequestedRoles()) { // Consent already granted by user if (grantedConsent != null && grantedConsent.isRoleGranted(r)) { continue; } - return true; + return CommonClientSessionModel.Action.OAUTH_GRANT.name(); } for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) { if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) { if (grantedConsent == null || !grantedConsent.isProtocolMapperGranted(protocolMapper)) { - return true; + return CommonClientSessionModel.Action.OAUTH_GRANT.name(); } } } @@ -539,32 +588,32 @@ public class AuthenticationManager { } else { event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED); } - return false; + return null; } - public static Response actionRequired(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, + public static Response actionRequired(final KeycloakSession session, final AuthenticationSessionModel authSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) { - final RealmModel realm = clientSession.getRealm(); - final UserModel user = userSession.getUser(); - final ClientModel client = clientSession.getClient(); + final RealmModel realm = authSession.getRealm(); + final UserModel user = authSession.getAuthenticatedUser(); + final ClientModel client = authSession.getClient(); - evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user); + evaluateRequiredActionTriggers(session, authSession, clientConnection, request, uriInfo, event, realm, user); logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired()); - event.detail(Details.CODE_ID, clientSession.getId()); + event.detail(Details.CODE_ID, authSession.getId()); Set requiredActions = user.getRequiredActions(); - Response action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions); + Response action = executionActions(session, authSession, request, event, realm, user, requiredActions); if (action != null) return action; // executionActions() method should remove any duplicate actions that might be in the clientSession - requiredActions = clientSession.getRequiredActions(); - action = executionActions(session, userSession, clientSession, request, event, realm, user, requiredActions); + requiredActions = authSession.getRequiredActions(); + action = executionActions(session, authSession, request, event, realm, user, requiredActions); if (action != null) return action; if (client.isConsentRequired()) { @@ -573,7 +622,7 @@ public class AuthenticationManager { List realmRoles = new LinkedList<>(); MultivaluedMap resourceRoles = new MultivaluedMapImpl<>(); - ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); + ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession); for (RoleModel r : accessCode.getRequestedRoles()) { // Consent already granted by user @@ -599,13 +648,15 @@ public class AuthenticationManager { // Skip grant screen if everything was already approved by this user if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) { - accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name()); - clientSession.setNote(CURRENT_REQUIRED_ACTION, ClientSessionModel.Action.OAUTH_GRANT.name()); + accessCode. + + setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name()); + authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name()); return session.getProvider(LoginFormsProvider.class) .setClientSessionCode(accessCode.getCode()) .setAccessRequest(realmRoles, resourceRoles, protocolMappers) - .createOAuthGrant(clientSession); + .createOAuthGrant(); } else { String consentDetail = (grantedConsent != null) ? Details.CONSENT_VALUE_PERSISTED_CONSENT : Details.CONSENT_VALUE_NO_CONSENT_REQUIRED; event.detail(Details.CONSENT, consentDetail); @@ -617,7 +668,38 @@ public class AuthenticationManager { } - protected static Response executionActions(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, + + public static void setRolesAndMappersInSession(AuthenticationSessionModel authSession) { + ClientModel client = authSession.getClient(); + UserModel user = authSession.getAuthenticatedUser(); + + Set requestedRoles = new HashSet(); + // todo scope param protocol independent + String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE); + for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) { + requestedRoles.add(r.getId()); + } + authSession.setRoles(requestedRoles); + + Set requestedProtocolMappers = new HashSet(); + ClientTemplateModel clientTemplate = client.getClientTemplate(); + if (clientTemplate != null && client.useTemplateMappers()) { + for (ProtocolMapperModel protocolMapper : clientTemplate.getProtocolMappers()) { + if (protocolMapper.getProtocol().equals(authSession.getProtocol())) { + requestedProtocolMappers.add(protocolMapper.getId()); + } + } + + } + for (ProtocolMapperModel protocolMapper : client.getProtocolMappers()) { + if (protocolMapper.getProtocol().equals(authSession.getProtocol())) { + requestedProtocolMappers.add(protocolMapper.getId()); + } + } + authSession.setProtocolMappers(requestedProtocolMappers); + } + + protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession, HttpRequest request, EventBuilder event, RealmModel realm, UserModel user, Set requiredActions) { for (String action : requiredActions) { @@ -635,34 +717,34 @@ public class AuthenticationManager { throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?"); } RequiredActionProvider actionProvider = factory.create(session); - RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory); + RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory); actionProvider.requiredActionChallenge(context); if (context.getStatus() == RequiredActionContext.Status.FAILURE) { - LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod()); + LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getAuthenticationSession().getProtocol()); protocol.setRealm(context.getRealm()) .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) .setUriInfo(context.getUriInfo()) .setEventBuilder(event); - Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED); + Response response = protocol.sendError(context.getAuthenticationSession(), Error.CONSENT_DENIED); event.error(Errors.REJECTED_BY_USER); return response; } else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { - clientSession.setNote(CURRENT_REQUIRED_ACTION, model.getProviderId()); + authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, model.getProviderId()); return context.getChallenge(); } else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) { event.clone().event(EventType.CUSTOM_REQUIRED_ACTION).detail(Details.CUSTOM_REQUIRED_ACTION, factory.getId()).success(); // don't have to perform the same action twice, so remove it from both the user and session required actions - clientSession.getUserSession().getUser().removeRequiredAction(factory.getId()); - clientSession.removeRequiredAction(factory.getId()); + authSession.getAuthenticatedUser().removeRequiredAction(factory.getId()); + authSession.removeRequiredAction(factory.getId()); } } return null; } - public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) { + public static void evaluateRequiredActionTriggers(final KeycloakSession session, final AuthenticationSessionModel authSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) { // see if any required actions need triggering, i.e. an expired password for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) { @@ -672,7 +754,7 @@ public class AuthenticationManager { throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?"); } RequiredActionProvider provider = factory.create(session); - RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, factory) { + RequiredActionContextResult result = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory) { @Override public void challenge(Response response) { throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()"); @@ -702,7 +784,11 @@ public class AuthenticationManager { public static AuthResult verifyIdentityToken(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection connection, boolean checkActive, boolean checkTokenType, boolean isCookie, String tokenString, HttpHeaders headers) { try { - TokenVerifier verifier = TokenVerifier.create(tokenString).realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())).checkActive(checkActive).checkTokenType(checkTokenType); + TokenVerifier verifier = TokenVerifier.create(tokenString, AccessToken.class) + .withDefaultChecks() + .realmUrl(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())) + .checkActive(checkActive) + .checkTokenType(checkTokenType); String kid = verifier.getHeader().getKeyId(); AlgorithmType algorithmType = verifier.getHeader().getAlgorithm().getType(); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java new file mode 100644 index 0000000000..1cba9dcf3a --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016 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.managers; + +import javax.ws.rs.core.UriInfo; + +import org.jboss.logging.Logger; +import org.keycloak.common.ClientConnection; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.RestartLoginCookie; +import org.keycloak.services.util.CookieHelper; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.StickySessionEncoderProvider; + +/** + * @author Marek Posolda + */ +public class AuthenticationSessionManager { + + public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID"; + + private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class); + + private final KeycloakSession session; + + public AuthenticationSessionManager(KeycloakSession session) { + this.session = session; + } + + + public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) { + AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client); + + if (browserCookie) { + setAuthSessionCookie(authSession.getId(), realm); + } + + return authSession; + } + + + public String getCurrentAuthenticationSessionId(RealmModel realm) { + return getAuthSessionCookieDecoded(realm); + } + + + public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) { + String authSessionId = getAuthSessionCookieDecoded(realm); + return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId); + } + + + public void setAuthSessionCookie(String authSessionId, RealmModel realm) { + UriInfo uriInfo = session.getContext().getUri(); + String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo); + + boolean sslRequired = realm.getSslRequired().isRequired(session.getContext().getConnection()); + + StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class); + String encodedAuthSessionId = encoder.encodeSessionId(authSessionId); + + CookieHelper.addCookie(AUTH_SESSION_ID, encodedAuthSessionId, cookiePath, null, null, -1, sslRequired, true); + + log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId); + } + + + private String getAuthSessionCookieDecoded(RealmModel realm) { + String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID); + + if (cookieVal != null) { + log.debugf("Found AUTH_SESSION_ID cookie with value %s", cookieVal); + + StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class); + String decodedAuthSessionId = encoder.decodeSessionId(cookieVal); + + // Check if owner of this authentication session changed due to re-hashing (usually node failover or addition of new node) + String reencoded = encoder.encodeSessionId(decodedAuthSessionId); + if (!reencoded.equals(cookieVal)) { + log.debugf("Route changed. Will update authentication session cookie"); + setAuthSessionCookie(decodedAuthSessionId, realm); + } + + return decodedAuthSessionId; + } else { + log.debugf("Not found AUTH_SESSION_ID cookie"); + return null; + } + } + + + public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) { + log.debugf("Removing authSession '%s'. Expire restart cookie: %b", authSession.getId(), expireRestartCookie); + session.authenticationSessions().removeAuthenticationSession(realm, authSession); + + // expire restart cookie + if (expireRestartCookie) { + ClientConnection clientConnection = session.getContext().getConnection(); + UriInfo uriInfo = session.getContext().getUri(); + RestartLoginCookie.expireRestartCookie(realm, clientConnection, uriInfo); + } + } + + + // Check to see if we already have authenticationSession with same ID + public UserSessionModel getUserSession(AuthenticationSessionModel authSession) { + return session.sessions().getUserSession(authSession.getRealm(), authSession.getId()); + } + +} diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index fec49c92c5..1bcfaf5add 100644 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -39,6 +39,7 @@ import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.adapters.config.BaseRealmConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.sessions.AuthenticationSessionProvider; import java.net.URI; import java.util.Collections; @@ -104,6 +105,11 @@ public class ClientManager { sessionsPersister.onClientRemoved(realm, client); } + AuthenticationSessionProvider authSessions = realmManager.getSession().authenticationSessions(); + if (authSessions != null) { + authSessions.onClientRemoved(realm, client); + } + UserModel serviceAccountUser = realmManager.getSession().users().getServiceAccount(client); if (serviceAccountUser != null) { new UserManager(realmManager.getSession()).removeUser(realm, serviceAccountUser); diff --git a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java similarity index 60% rename from server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java rename to services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index 3d536ddf05..59158e6c31 100755 --- a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -21,14 +21,13 @@ import org.jboss.logging.Logger; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.Time; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.utils.KeycloakModelUtils; -import org.keycloak.OAuth2Constants; +import org.keycloak.sessions.CommonClientSessionModel; import java.security.MessageDigest; import java.util.HashSet; @@ -38,17 +37,15 @@ import java.util.Set; * @author Bill Burke * @version $Revision: 1 $ */ -public class ClientSessionCode { +public class ClientSessionCode { private static final String ACTIVE_CODE = "active_code"; private static final Logger logger = Logger.getLogger(ClientSessionCode.class); - private static final String NEXT_CODE = ClientSessionCode.class.getName() + ".nextCode"; - private KeycloakSession session; private final RealmModel realm; - private final ClientSessionModel clientSession; + private final CLIENT_SESSION commonLoginSession; public enum ActionType { CLIENT, @@ -56,45 +53,45 @@ public class ClientSessionCode { USER } - public ClientSessionCode(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession) { + public ClientSessionCode(KeycloakSession session, RealmModel realm, CLIENT_SESSION commonLoginSession) { this.session = session; this.realm = realm; - this.clientSession = clientSession; + this.commonLoginSession = commonLoginSession; } - public static class ParseResult { - ClientSessionCode code; - boolean clientSessionNotFound; + public static class ParseResult { + ClientSessionCode code; + boolean authSessionNotFound; boolean illegalHash; - ClientSessionModel clientSession; + CLIENT_SESSION clientSession; - public ClientSessionCode getCode() { + public ClientSessionCode getCode() { return code; } - public boolean isClientSessionNotFound() { - return clientSessionNotFound; + public boolean isAuthSessionNotFound() { + return authSessionNotFound; } public boolean isIllegalHash() { return illegalHash; } - public ClientSessionModel getClientSession() { + public CLIENT_SESSION getClientSession() { return clientSession; } } - public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm) { - ParseResult result = new ParseResult(); + public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm, Class sessionClass) { + ParseResult result = new ParseResult<>(); if (code == null) { result.illegalHash = true; return result; } try { - result.clientSession = getClientSession(code, session, realm); + result.clientSession = getClientSession(code, session, realm, sessionClass); if (result.clientSession == null) { - result.clientSessionNotFound = true; + result.authSessionNotFound = true; return result; } @@ -103,7 +100,7 @@ public class ClientSessionCode { return result; } - result.code = new ClientSessionCode(session, realm, result.clientSession); + result.code = new ClientSessionCode(session, realm, result.clientSession); return result; } catch (RuntimeException e) { result.illegalHash = true; @@ -111,44 +108,24 @@ public class ClientSessionCode { } } - public static ClientSessionCode parse(String code, KeycloakSession session, RealmModel realm) { - try { - ClientSessionModel clientSession = getClientSession(code, session, realm); - if (clientSession == null) { - return null; - } + public static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, Class sessionClass) { + CommonClientSessionModel clientSessionn = CodeGenerateUtil.getParser(sessionClass).parseSession(code, session, realm);; + CLIENT_SESSION clientSession = sessionClass.cast(clientSessionn); - if (!verifyCode(code, clientSession)) { - return null; - } - - return new ClientSessionCode(session, realm, clientSession); - } catch (RuntimeException e) { - return null; - } - } - - public static ClientSessionModel getClientSession(String code, KeycloakSession session, RealmModel realm) { - try { - String[] parts = code.split("\\."); - String id = parts[1]; - return session.sessions().getClientSession(realm, id); - } catch (ArrayIndexOutOfBoundsException e) { - return null; - } - } - - public ClientSessionModel getClientSession() { return clientSession; } + public CLIENT_SESSION getClientSession() { + return commonLoginSession; + } + public boolean isValid(String requestedAction, ActionType actionType) { if (!isValidAction(requestedAction)) return false; return isActionActive(actionType); } public boolean isActionActive(ActionType actionType) { - int timestamp = clientSession.getTimestamp(); + int timestamp = commonLoginSession.getTimestamp(); int lifespan; switch (actionType) { @@ -169,7 +146,7 @@ public class ClientSessionCode { } public boolean isValidAction(String requestedAction) { - String action = clientSession.getAction(); + String action = commonLoginSession.getAction(); if (action == null) { return false; } @@ -179,8 +156,17 @@ public class ClientSessionCode { return true; } + public void removeExpiredClientSession() { + CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(commonLoginSession.getClass()); + parser.removeExpiredSession(session, commonLoginSession); + } + public Set getRequestedRoles() { + return getRequestedRoles(commonLoginSession, realm); + } + + public static Set getRequestedRoles(CommonClientSessionModel clientSession, RealmModel realm) { Set requestedRoles = new HashSet<>(); for (String roleId : clientSession.getRoles()) { RoleModel role = realm.getRoleById(roleId); @@ -192,9 +178,11 @@ public class ClientSessionCode { } public Set getRequestedProtocolMappers() { + return getRequestedProtocolMappers(commonLoginSession.getProtocolMappers(), commonLoginSession.getClient()); + } + + public static Set getRequestedProtocolMappers(Set protocolMappers, ClientModel client) { Set requestedProtocolMappers = new HashSet<>(); - Set protocolMappers = clientSession.getProtocolMappers(); - ClientModel client = clientSession.getClient(); ClientTemplateModel template = client.getClientTemplate(); if (protocolMappers != null) { for (String protocolMapperId : protocolMappers) { @@ -211,46 +199,34 @@ public class ClientSessionCode { } public void setAction(String action) { - clientSession.setAction(action); - clientSession.setTimestamp(Time.currentTime()); + commonLoginSession.setAction(action); + commonLoginSession.setTimestamp(Time.currentTime()); } public String getCode() { - String nextCode = (String) session.getAttribute(NEXT_CODE + "." + clientSession.getId()); + CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(commonLoginSession.getClass()); + String nextCode = parser.getNote(commonLoginSession, ACTIVE_CODE); if (nextCode == null) { - nextCode = generateCode(clientSession); - session.setAttribute(NEXT_CODE + "." + clientSession.getId(), nextCode); + nextCode = generateCode(commonLoginSession); } else { - logger.debug("Code already generated for session, using code from session attributes"); + logger.debug("Code already generated for session, using same code"); } return nextCode; } - private static String generateCode(ClientSessionModel clientSession) { + private static String generateCode(CommonClientSessionModel authSession) { try { String actionId = Base64Url.encode(KeycloakModelUtils.generateSecret()); StringBuilder sb = new StringBuilder(); sb.append(actionId); sb.append('.'); - sb.append(clientSession.getId()); + sb.append(authSession.getId()); - // https://tools.ietf.org/html/rfc7636#section-4 - String codeChallenge = clientSession.getNote(OAuth2Constants.CODE_CHALLENGE); - String codeChallengeMethod = clientSession.getNote(OAuth2Constants.CODE_CHALLENGE_METHOD); - if (codeChallenge != null) { - logger.debugf("PKCE received codeChallenge = %s", codeChallenge); - if (codeChallengeMethod == null) { - logger.debug("PKCE not received codeChallengeMethod, treating plain"); - codeChallengeMethod = OAuth2Constants.PKCE_METHOD_PLAIN; - } else { - logger.debugf("PKCE received codeChallengeMethod = %s", codeChallengeMethod); - } - } + CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass()); - String code = sb.toString(); - - clientSession.setNote(ACTIVE_CODE, code); + String code = parser.generateCode(authSession, actionId); + parser.setNote(authSession, ACTIVE_CODE, code); return code; } catch (Exception e) { @@ -258,15 +234,17 @@ public class ClientSessionCode { } } - private static boolean verifyCode(String code, ClientSessionModel clientSession) { + public static boolean verifyCode(String code, CommonClientSessionModel authSession) { try { - String activeCode = clientSession.getNote(ACTIVE_CODE); + CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass()); + + String activeCode = parser.getNote(authSession, ACTIVE_CODE); if (activeCode == null) { logger.debug("Active code not found in client session"); return false; } - clientSession.removeNote(ACTIVE_CODE); + parser.removeNote(authSession, ACTIVE_CODE); return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes()); } catch (Exception e) { diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java new file mode 100644 index 0000000000..a975aa5cd7 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java @@ -0,0 +1,171 @@ +/* + * Copyright 2016 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.managers; + +import java.util.HashMap; +import java.util.Map; + +import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; +import org.keycloak.models.AuthenticatedClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.AuthenticationSessionModel; + +/** + * + * @author Marek Posolda + */ +class CodeGenerateUtil { + + private static final Logger logger = Logger.getLogger(CodeGenerateUtil.class); + + private static final Map, ClientSessionParser> PARSERS = new HashMap<>(); + + static { + PARSERS.put(AuthenticationSessionModel.class, new AuthenticationSessionModelParser()); + PARSERS.put(AuthenticatedClientSessionModel.class, new AuthenticatedClientSessionModelParser()); + } + + + + static ClientSessionParser getParser(Class clientSessionClass) { + for (Class c : PARSERS.keySet()) { + if (c.isAssignableFrom(clientSessionClass)) { + return PARSERS.get(c); + } + } + return null; + } + + + interface ClientSessionParser { + + CS parseSession(String code, KeycloakSession session, RealmModel realm); + + String generateCode(CS clientSession, String actionId); + + void removeExpiredSession(KeycloakSession session, CS clientSession); + + String getNote(CS clientSession, String name); + + void removeNote(CS clientSession, String name); + + void setNote(CS clientSession, String name, String value); + + } + + + // IMPLEMENTATIONS + + + private static class AuthenticationSessionModelParser implements ClientSessionParser { + + @Override + public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) { + // Read authSessionID from cookie. Code is ignored for now + return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); + } + + @Override + public String generateCode(AuthenticationSessionModel clientSession, String actionId) { + return actionId; + } + + @Override + public void removeExpiredSession(KeycloakSession session, AuthenticationSessionModel clientSession) { + new AuthenticationSessionManager(session).removeAuthenticationSession(clientSession.getRealm(), clientSession, true); + } + + @Override + public String getNote(AuthenticationSessionModel clientSession, String name) { + return clientSession.getAuthNote(name); + } + + @Override + public void removeNote(AuthenticationSessionModel clientSession, String name) { + clientSession.removeAuthNote(name); + } + + @Override + public void setNote(AuthenticationSessionModel clientSession, String name, String value) { + clientSession.setAuthNote(name, value); + } + } + + + private static class AuthenticatedClientSessionModelParser implements ClientSessionParser { + + @Override + public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) { + try { + String[] parts = code.split("\\."); + String userSessionId = parts[2]; + String clientUUID = parts[3]; + + UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); + if (userSession == null) { + return null; + } + + return userSession.getAuthenticatedClientSessions().get(clientUUID); + } catch (ArrayIndexOutOfBoundsException e) { + return null; + } + } + + @Override + public String generateCode(AuthenticatedClientSessionModel clientSession, String actionId) { + String userSessionId = clientSession.getUserSession().getId(); + String clientUUID = clientSession.getClient().getId(); + StringBuilder sb = new StringBuilder(); + sb.append("uss."); + sb.append(actionId); + sb.append('.'); + sb.append(userSessionId); + sb.append('.'); + sb.append(clientUUID); + + return sb.toString(); + } + + @Override + public void removeExpiredSession(KeycloakSession session, AuthenticatedClientSessionModel clientSession) { + throw new IllegalStateException("Not yet implemented"); + } + + @Override + public String getNote(AuthenticatedClientSessionModel clientSession, String name) { + return clientSession.getNote(name); + } + + @Override + public void removeNote(AuthenticatedClientSessionModel clientSession, String name) { + clientSession.removeNote(name); + } + + @Override + public void setNote(AuthenticatedClientSessionModel clientSession, String name, String value) { + clientSession.setNote(name, value); + } + } + + +} diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 3306bcdedb..e94ff3c00f 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -47,6 +47,7 @@ import org.keycloak.representations.idm.OAuthClientRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.sessions.AuthenticationSessionProvider; import org.keycloak.storage.UserStorageProviderModel; import org.keycloak.services.clientregistration.policy.DefaultClientRegistrationPolicies; @@ -248,6 +249,11 @@ public class RealmManager { sessionsPersister.onRealmRemoved(realm); } + AuthenticationSessionProvider authSessions = session.authenticationSessions(); + if (authSessions != null) { + authSessions.onRealmRemoved(realm); + } + // Refresh periodic sync tasks for configured storageProviders List storageProviders = realm.getUserStorageProviders(); UserStorageSyncManager storageSync = new UserStorageSyncManager(); diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java index 12e0449b32..9aa4b69293 100755 --- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java @@ -24,8 +24,8 @@ import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.Time; import org.keycloak.connections.httpclient.HttpClientProvider; import org.keycloak.constants.AdapterConstants; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; @@ -113,7 +113,7 @@ public class ResourceAdminManager { protected void logoutUserSessions(URI requestUri, RealmModel realm, List userSessions) { // Map from "app" to clientSessions for this app - MultivaluedHashMap clientSessions = new MultivaluedHashMap(); + MultivaluedHashMap clientSessions = new MultivaluedHashMap<>(); for (UserSessionModel userSession : userSessions) { putClientSessions(clientSessions, userSession); } @@ -121,37 +121,40 @@ public class ResourceAdminManager { logger.debugv("logging out {0} resources ", clientSessions.size()); //logger.infov("logging out resources: {0}", clientSessions); - for (Map.Entry> entry : clientSessions.entrySet()) { - logoutClientSessions(requestUri, realm, entry.getKey(), entry.getValue()); + for (Map.Entry> entry : clientSessions.entrySet()) { + if (entry.getValue().size() == 0) { + continue; + } + logoutClientSessions(requestUri, realm, entry.getValue().get(0).getClient(), entry.getValue()); } } - private void putClientSessions(MultivaluedHashMap clientSessions, UserSessionModel userSession) { - for (ClientSessionModel clientSession : userSession.getClientSessions()) { - ClientModel client = clientSession.getClient(); - clientSessions.add(client, clientSession); + private void putClientSessions(MultivaluedHashMap clientSessions, UserSessionModel userSession) { + for (Map.Entry entry : userSession.getAuthenticatedClientSessions().entrySet()) { + clientSessions.add(entry.getKey(), entry.getValue()); } } public void logoutUserFromClient(URI requestUri, RealmModel realm, ClientModel resource, UserModel user) { List userSessions = session.sessions().getUserSessions(realm, user); - List ourAppClientSessions = null; + List ourAppClientSessions = new LinkedList<>(); if (userSessions != null) { - MultivaluedHashMap clientSessions = new MultivaluedHashMap(); for (UserSessionModel userSession : userSessions) { - putClientSessions(clientSessions, userSession); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(resource.getId()); + if (clientSession != null) { + ourAppClientSessions.add(clientSession); + } } - ourAppClientSessions = clientSessions.get(resource); } logoutClientSessions(requestUri, realm, resource, ourAppClientSessions); } - public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, ClientSessionModel clientSession) { + public boolean logoutClientSession(URI requestUri, RealmModel realm, ClientModel resource, AuthenticatedClientSessionModel clientSession) { return logoutClientSessions(requestUri, realm, resource, Arrays.asList(clientSession)); } - protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List clientSessions) { + protected boolean logoutClientSessions(URI requestUri, RealmModel realm, ClientModel resource, List clientSessions) { String managementUrl = getManagementUrl(requestUri, resource); if (managementUrl != null) { @@ -160,7 +163,7 @@ public class ResourceAdminManager { List userSessions = new LinkedList<>(); if (clientSessions != null && clientSessions.size() > 0) { adapterSessionIds = new MultivaluedHashMap(); - for (ClientSessionModel clientSession : clientSessions) { + for (AuthenticatedClientSessionModel clientSession : clientSessions) { String adapterSessionId = clientSession.getNote(AdapterConstants.CLIENT_SESSION_STATE); if (adapterSessionId != null) { String host = clientSession.getNote(AdapterConstants.CLIENT_SESSION_HOST); diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java index 4c8c2fe89e..f347d4ce69 100644 --- a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java +++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java @@ -18,11 +18,10 @@ package org.keycloak.services.managers; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; @@ -31,7 +30,6 @@ import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.services.ServicesLogger; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -52,7 +50,7 @@ public class UserSessionManager { this.persister = session.getProvider(UserSessionPersisterProvider.class); } - public void createOrUpdateOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) { + public void createOrUpdateOfflineSession(AuthenticatedClientSessionModel clientSession, UserSessionModel userSession) { UserModel user = userSession.getUser(); // Create and persist offline userSession if we don't have one @@ -65,50 +63,50 @@ public class UserSessionManager { } // Create and persist clientSession - ClientSessionModel offlineClientSession = kcSession.sessions().getOfflineClientSession(clientSession.getRealm(), clientSession.getId()); + AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessions().get(clientSession.getClient().getId()); if (offlineClientSession == null) { createOfflineClientSession(user, clientSession, offlineUserSession); } } - // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation - public ClientSessionModel findOfflineClientSession(RealmModel realm, String clientSessionId) { - return kcSession.sessions().getOfflineClientSession(realm, clientSessionId); + + public UserSessionModel findOfflineUserSession(RealmModel realm, String userSessionId) { + return kcSession.sessions().getOfflineUserSession(realm, userSessionId); } public Set findClientsWithOfflineToken(RealmModel realm, UserModel user) { - List clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user); + List userSessions = kcSession.sessions().getOfflineUserSessions(realm, user); Set clients = new HashSet<>(); - for (ClientSessionModel clientSession : clientSessions) { - clients.add(clientSession.getClient()); + for (UserSessionModel userSession : userSessions) { + Set clientIds = userSession.getAuthenticatedClientSessions().keySet(); + for (String clientUUID : clientIds) { + ClientModel client = realm.getClientById(clientUUID); + clients.add(client); + } } return clients; } - public List findOfflineSessions(RealmModel realm, ClientModel client, UserModel user) { - List clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user); - List userSessions = new LinkedList<>(); - for (ClientSessionModel clientSession : clientSessions) { - userSessions.add(clientSession.getUserSession()); - } - return userSessions; + public List findOfflineSessions(RealmModel realm, UserModel user) { + return kcSession.sessions().getOfflineUserSessions(realm, user); } public boolean revokeOfflineToken(UserModel user, ClientModel client) { RealmModel realm = client.getRealm(); - List clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user); + List userSessions = kcSession.sessions().getOfflineUserSessions(realm, user); boolean anyRemoved = false; - for (ClientSessionModel clientSession : clientSessions) { - if (clientSession.getClient().getId().equals(client.getId())) { + for (UserSessionModel userSession : userSessions) { + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + if (clientSession != null) { if (logger.isTraceEnabled()) { - logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .", - user.getUsername(), client.getClientId(), clientSession.getId()); + logger.tracef("Removing existing offline token for user '%s' and client '%s' .", + user.getUsername(), client.getClientId()); } - kcSession.sessions().removeOfflineClientSession(realm, clientSession.getId()); - persister.removeClientSession(clientSession.getId(), true); - checkOfflineUserSessionHasClientSessions(realm, user, clientSession.getUserSession(), clientSessions); + clientSession.setUserSession(null); + persister.removeClientSession(userSession.getId(), client.getId(), true); + checkOfflineUserSessionHasClientSessions(realm, user, userSession); anyRemoved = true; } } @@ -124,7 +122,7 @@ public class UserSessionManager { persister.removeUserSession(userSession.getId(), true); } - public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) { + public boolean isOfflineTokenAllowed(AuthenticatedClientSessionModel clientSession) { RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE); if (offlineAccessRole == null) { ServicesLogger.LOGGER.roleNotInRealm(Constants.OFFLINE_ACCESS_ROLE); @@ -144,30 +142,26 @@ public class UserSessionManager { return offlineUserSession; } - private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) { + private void createOfflineClientSession(UserModel user, AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) { if (logger.isTraceEnabled()) { logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" , - clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId()); + clientSession.getId(), offlineUserSession.getId(), user.getUsername(), clientSession.getClient().getClientId()); } - ClientSessionModel offlineClientSession = kcSession.sessions().createOfflineClientSession(clientSession); - offlineClientSession.setUserSession(userSession); + kcSession.sessions().createOfflineClientSession(clientSession, offlineUserSession); persister.createClientSession(clientSession, true); } // Check if userSession has any offline clientSessions attached to it. Remove userSession if not - private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, UserSessionModel userSession, List clientSessions) { - String userSessionId = userSession.getId(); - for (ClientSessionModel clientSession : clientSessions) { - if (clientSession.getUserSession().getId().equals(userSessionId)) { - return; - } + private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, UserSessionModel userSession) { + if (userSession.getAuthenticatedClientSessions().size() > 0) { + return; } if (logger.isTraceEnabled()) { - logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId); + logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSession.getId()); } kcSession.sessions().removeOfflineUserSession(realm, userSession); - persister.removeUserSession(userSessionId, true); + persister.removeUserSession(userSession.getId(), true); } } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index d9d3c4ea97..295f07b27e 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -33,6 +33,8 @@ public class Messages { public static final String EXPIRED_CODE = "expiredCodeMessage"; + public static final String EXPIRED_ACTION = "expiredActionMessage"; + public static final String MISSING_FIRST_NAME = "missingFirstNameMessage"; public static final String MISSING_LAST_NAME = "missingLastNameMessage"; @@ -197,5 +199,10 @@ public class Messages { public static final String FAILED_LOGOUT = "failedLogout"; public static final String CONSENT_DENIED="consentDenied"; + public static final String ALREADY_LOGGED_IN="alreadyLoggedIn"; + + public static final String DIFFERENT_USER_AUTHENTICATED = "differentUserAuthenticated"; + + public static final String BROKER_LINKING_SESSION_EXPIRED = "brokerLinkingSessionExpired"; } diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index e0e5f8b55d..9dd9c4b55c 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -30,8 +30,8 @@ import org.keycloak.forms.account.AccountPages; import org.keycloak.forms.account.AccountProvider; import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.models.AccountRoles; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; @@ -44,7 +44,6 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.ModelToRepresentation; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ForbiddenException; @@ -53,11 +52,11 @@ import org.keycloak.services.Urls; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.storage.ReadOnlyException; import org.keycloak.util.JsonSerialization; @@ -67,13 +66,12 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; -import javax.ws.rs.core.Variant; + import java.io.IOException; import java.lang.reflect.Method; import java.net.URI; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -164,19 +162,11 @@ public class AccountService extends AbstractSecuredLocalService { if (authResult != null) { UserSessionModel userSession = authResult.getSession(); if (userSession != null) { - boolean associated = false; - for (ClientSessionModel c : userSession.getClientSessions()) { - if (c.getClient().equals(client)) { - auth.setClientSession(c); - associated = true; - break; - } - } - if (!associated) { - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setUserSession(userSession); - auth.setClientSession(clientSession); + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); + if (clientSession == null) { + clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession); } + auth.setClientSession(clientSession); } account.setUser(auth.getUser()); @@ -216,14 +206,18 @@ public class AccountService extends AbstractSecuredLocalService { setReferrerOnPage(); - String forwardedError = auth.getClientSession().getNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE); - if (forwardedError != null) { - try { - FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class); - account.setError(errorMessage.getMessage(), errorMessage.getParameters()); - auth.getClientSession().removeNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE); - } catch (IOException ioe) { - throw new RuntimeException(ioe); + UserSessionModel userSession = auth.getClientSession().getUserSession(); + AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId()); + if (authSession != null) { + String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE); + if (forwardedError != null) { + try { + FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class); + account.setError(errorMessage.getMessage(), errorMessage.getParameters()); + authSession.removeAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } } } @@ -777,7 +771,7 @@ public class AccountService extends AbstractSecuredLocalService { try { String nonce = UUID.randomUUID().toString(); MessageDigest md = MessageDigest.getInstance("SHA-256"); - String input = nonce + auth.getSession().getId() + auth.getClientSession().getId() + providerId; + String input = nonce + auth.getSession().getId() + client.getClientId() + providerId; byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); String hash = Base64Url.encode(check); URI linkUrl = Urls.identityProviderLinkRequest(this.uriInfo.getBaseUri(), providerId, realm.getName()); diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index f9efe19ab6..333a1cf029 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -20,8 +20,6 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyProviderFactory; - -import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants; @@ -43,10 +41,10 @@ import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderMapperModel; @@ -72,26 +70,16 @@ import org.keycloak.services.ServicesLogger; import org.keycloak.services.Urls; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.AuthenticationManager.AuthResult; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; +import org.keycloak.services.util.BrowserHistoryHelper; import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.validation.Validation; +import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.util.JsonSerialization; -import javax.ws.rs.GET; -import javax.ws.rs.OPTIONS; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -106,10 +94,18 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; -import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS; -import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE; -import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; +import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; /** *

    @@ -118,6 +114,9 @@ import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; */ public class IdentityBrokerService implements IdentityProvider.AuthenticationCallback { + // Authentication session note, which references identity provider that is currently linked + private static final String LINKING_IDENTITY_PROVIDER = "LINKING_IDENTITY_PROVIDER"; + private static final Logger logger = Logger.getLogger(IdentityBrokerService.class); private final RealmModel realmModel; @@ -205,13 +204,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal this.event.event(EventType.CLIENT_INITIATED_ACCOUNT_LINKING); checkRealm(); ClientModel client = checkClient(clientId); - AuthenticationManager authenticationManager = new AuthenticationManager(); redirectUri = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realmModel, client); if (redirectUri == null) { event.error(Errors.INVALID_REDIRECT_URI); throw new ErrorPageException(session, Messages.INVALID_REQUEST); } + event.detail(Details.REDIRECT_URI, redirectUri); + if (nonce == null || hash == null) { event.error(Errors.INVALID_REDIRECT_URI); throw new ErrorPageException(session, Messages.INVALID_REQUEST); @@ -230,7 +230,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } } - AuthResult cookieResult = authenticationManager.authenticateIdentityCookie(session, realmModel, true); + AuthenticationManager.AuthResult cookieResult = AuthenticationManager.authenticateIdentityCookie(session, realmModel, true); String errorParam = "link_error"; if (cookieResult == null) { event.error(Errors.NOT_LOGGED_IN); @@ -241,10 +241,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return Response.status(302).location(builder.build()).build(); } + cookieResult.getSession(); + event.session(cookieResult.getSession()); + event.user(cookieResult.getUser()); + event.detail(Details.USERNAME, cookieResult.getUser().getUsername()); - - ClientSessionModel clientSession = null; - for (ClientSessionModel cs : cookieResult.getSession().getClientSessions()) { + AuthenticatedClientSessionModel clientSession = null; + for (AuthenticatedClientSessionModel cs : cookieResult.getSession().getAuthenticatedClientSessions().values()) { if (cs.getClient().getClientId().equals(clientId)) { byte[] decoded = Base64Url.decode(hash); MessageDigest md = null; @@ -253,7 +256,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } catch (NoSuchAlgorithmException e) { throw new ErrorPageException(session, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST); } - String input = nonce + cookieResult.getSession().getId() + cs.getId() + providerId; + String input = nonce + cookieResult.getSession().getId() + clientId + providerId; byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); if (MessageDigest.isEqual(decoded, check)) { clientSession = cs; @@ -266,14 +269,14 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal throw new ErrorPageException(session, Messages.INVALID_REQUEST); } + event.detail(Details.IDENTITY_PROVIDER, providerId); - - ClientModel accountService = this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID); + ClientModel accountService = this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); if (!accountService.getId().equals(client.getId())) { - RoleModel manageAccountRole = accountService.getRole(MANAGE_ACCOUNT); + RoleModel manageAccountRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT); if (!clientSession.getRoles().contains(manageAccountRole.getId())) { - RoleModel linkRole = accountService.getRole(MANAGE_ACCOUNT_LINKS); + RoleModel linkRole = accountService.getRole(AccountRoles.MANAGE_ACCOUNT_LINKS); if (!clientSession.getRoles().contains(linkRole.getId())) { event.error(Errors.NOT_ALLOWED); UriBuilder builder = UriBuilder.fromUri(redirectUri) @@ -296,16 +299,22 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } + // Create AuthenticationSessionModel with same ID like userSession and refresh cookie + UserSessionModel userSession = cookieResult.getSession(); + AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(userSession.getId(), realmModel, client); + new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel); - ClientSessionCode clientSessionCode = new ClientSessionCode(session, realmModel, clientSession); - clientSessionCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + ClientSessionCode clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession); + clientSessionCode.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); clientSessionCode.getCode(); - clientSession.setRedirectUri(redirectUri); - clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString()); + authSession.setProtocol(client.getProtocol()); + authSession.setRedirectUri(redirectUri); + authSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString()); + authSession.setAuthNote(LINKING_IDENTITY_PROVIDER, cookieResult.getSession().getId() + clientId + providerId); + event.detail(Details.CODE_ID, userSession.getId()); event.success(); - try { IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId); Response response = identityProvider.performLogin(createAuthenticationRequest(providerId, clientSessionCode)); @@ -414,7 +423,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal try { AppAuthManager authManager = new AppAuthManager(); - AuthResult authResult = authManager.authenticateBearerToken(this.session, this.realmModel, this.uriInfo, this.clientConnection, this.request.getHttpHeaders()); + AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(this.session, this.realmModel, this.uriInfo, this.clientConnection, this.request.getHttpHeaders()); if (authResult != null) { AccessToken token = authResult.getToken(); @@ -475,7 +484,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (parsedCode.response != null) { return parsedCode.response; } - ClientSessionCode clientCode = parsedCode.clientSessionCode; + ClientSessionCode clientCode = parsedCode.clientSessionCode; String providerId = identityProviderConfig.getAlias(); if (!identityProviderConfig.isStoreToken()) { @@ -485,10 +494,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal context.setToken(null); } - ClientSessionModel clientSession = clientCode.getClientSession(); - context.setClientSession(clientSession); + AuthenticationSessionModel authenticationSession = clientCode.getClientSession(); + context.setAuthenticationSession(authenticationSession); - session.getContext().setClient(clientSession.getClient()); + session.getContext().setClient(authenticationSession.getClient()); context.getIdp().preprocessFederatedIdentity(session, realmModel, context); Set mappers = realmModel.getIdentityProviderMappersByAlias(context.getIdpConfig().getAlias()); @@ -504,14 +513,16 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal context.getUsername(), context.getToken()); this.event.event(EventType.IDENTITY_PROVIDER_LOGIN) - .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) + .detail(Details.REDIRECT_URI, authenticationSession.getRedirectUri()) + .detail(Details.IDENTITY_PROVIDER, providerId) .detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); UserModel federatedUser = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, this.realmModel); // Check if federatedUser is already authenticated (this means linking social into existing federatedUser account) - if (clientSession.getUserSession() != null) { - return performAccountLinking(clientSession, context, federatedIdentityModel, federatedUser); + UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authenticationSession); + if (shouldPerformAccountLinking(authenticationSession, userSession, providerId)) { + return performAccountLinking(authenticationSession, userSession, context, federatedIdentityModel, federatedUser); } if (federatedUser == null) { @@ -531,13 +542,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal username = username.trim(); context.setModelUsername(username); - clientSession.setTimestamp(Time.currentTime()); + // Redirect to firstBrokerLogin after successful login and ensure that previous authentication state removed + AuthenticationProcessor.resetFlow(authenticationSession, LoginActionsService.FIRST_BROKER_LOGIN_PATH); SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context); - ctx.saveToClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + ctx.saveToAuthenticationSession(authenticationSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo) - .queryParam(OAuth2Constants.CODE, clientCode.getCode()) .build(realmModel.getName()); return Response.status(302).location(redirect).build(); @@ -548,12 +559,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } updateFederatedIdentity(context, federatedUser); - clientSession.setAuthenticatedUser(federatedUser); + authenticationSession.setAuthenticatedUser(federatedUser); - return finishOrRedirectToPostBrokerLogin(clientSession, context, false, parsedCode.clientSessionCode); + return finishOrRedirectToPostBrokerLogin(authenticationSession, context, false, parsedCode.clientSessionCode); } } + public Response validateUser(UserModel user, RealmModel realm) { if (!user.isEnabled()) { event.error(Errors.USER_DISABLED); @@ -580,29 +592,35 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return afterFirstBrokerLogin(parsedCode.clientSessionCode); } - private Response afterFirstBrokerLogin(ClientSessionCode clientSessionCode) { - ClientSessionModel clientSession = clientSessionCode.getClientSession(); + private Response afterFirstBrokerLogin(ClientSessionCode clientSessionCode) { + AuthenticationSessionModel authSession = clientSessionCode.getClientSession(); try { - this.event.detail(Details.CODE_ID, clientSession.getId()) + this.event.detail(Details.CODE_ID, authSession.getId()) .removeDetail("auth_method"); - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); if (serializedCtx == null) { throw new IdentityBrokerException("Not found serialized context in clientSession"); } - BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession); + BrokeredIdentityContext context = serializedCtx.deserialize(session, authSession); String providerId = context.getIdpConfig().getAlias(); event.detail(Details.IDENTITY_PROVIDER, providerId); event.detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); - // firstBrokerLogin workflow finished. Removing note now - clientSession.removeNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + // Ensure the first-broker-login flow was successfully finished + String authProvider = authSession.getAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS); + if (authProvider == null || !authProvider.equals(providerId)) { + throw new IdentityBrokerException("Invalid request. Not found the flag that first-broker-login flow was finished"); + } - UserModel federatedUser = clientSession.getAuthenticatedUser(); + // firstBrokerLogin workflow finished. Removing note now + authSession.removeAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + + UserModel federatedUser = authSession.getAuthenticatedUser(); if (federatedUser == null) { - throw new IdentityBrokerException("Couldn't found authenticated federatedUser in clientSession"); + throw new IdentityBrokerException("Couldn't found authenticated federatedUser in authentication session"); } event.user(federatedUser); @@ -623,7 +641,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel); - String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER); + String isRegisteredNewUser = authSession.getAuthNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER); if (Boolean.parseBoolean(isRegisteredNewUser)) { logger.debugf("Registered new user '%s' after first login with identity provider '%s'. Identity provider username is '%s' . ", federatedUser.getUsername(), providerId, context.getUsername()); @@ -638,7 +656,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } } - if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(federatedUser.getEmail()) && !Boolean.parseBoolean(clientSession.getNote(AbstractIdpAuthenticator.UPDATE_PROFILE_EMAIL_CHANGED))) { + if (context.getIdpConfig().isTrustEmail() && !Validation.isBlank(federatedUser.getEmail()) && !Boolean.parseBoolean(authSession.getAuthNote(AbstractIdpAuthenticator.UPDATE_PROFILE_EMAIL_CHANGED))) { logger.debugf("Email verified automatically after registration of user '%s' through Identity provider '%s' ", federatedUser.getUsername(), context.getIdpConfig().getAlias()); federatedUser.setEmailVerified(true); } @@ -657,7 +675,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal updateFederatedIdentity(context, federatedUser); } - return finishOrRedirectToPostBrokerLogin(clientSession, context, true, clientSessionCode); + return finishOrRedirectToPostBrokerLogin(authSession, context, true, clientSessionCode); } catch (Exception e) { return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e); @@ -665,25 +683,24 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } - private Response finishOrRedirectToPostBrokerLogin(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode clientSessionCode) { + private Response finishOrRedirectToPostBrokerLogin(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode clientSessionCode) { String postBrokerLoginFlowId = context.getIdpConfig().getPostBrokerLoginFlowId(); if (postBrokerLoginFlowId == null) { logger.debugf("Skip redirect to postBrokerLogin flow. PostBrokerLogin flow not set for identityProvider '%s'.", context.getIdpConfig().getAlias()); - return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin, clientSessionCode); + return afterPostBrokerLoginFlowSuccess(authSession, context, wasFirstBrokerLogin, clientSessionCode); } else { logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias()); - clientSession.setTimestamp(Time.currentTime()); + authSession.setTimestamp(Time.currentTime()); SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context); - ctx.saveToClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); + ctx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); - clientSession.setNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin)); + authSession.setAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(wasFirstBrokerLogin)); URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo) - .queryParam(OAuth2Constants.CODE, clientSessionCode.getCode()) .build(realmModel.getName()); return Response.status(302).location(redirect).build(); } @@ -699,87 +716,89 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (parsedCode.response != null) { return parsedCode.response; } - ClientSessionModel clientSession = parsedCode.clientSessionCode.getClientSession(); + AuthenticationSessionModel authenticationSession = parsedCode.clientSessionCode.getClientSession(); try { - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authenticationSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); if (serializedCtx == null) { throw new IdentityBrokerException("Not found serialized context in clientSession. Note " + PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT + " was null"); } - BrokeredIdentityContext context = serializedCtx.deserialize(session, clientSession); + BrokeredIdentityContext context = serializedCtx.deserialize(session, authenticationSession); - String wasFirstBrokerLoginNote = clientSession.getNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); + String wasFirstBrokerLoginNote = authenticationSession.getAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); boolean wasFirstBrokerLogin = Boolean.parseBoolean(wasFirstBrokerLoginNote); // Ensure the post-broker-login flow was successfully finished String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + context.getIdpConfig().getAlias(); - String authState = clientSession.getNote(authStateNoteKey); + String authState = authenticationSession.getAuthNote(authStateNoteKey); if (!Boolean.parseBoolean(authState)) { throw new IdentityBrokerException("Invalid request. Not found the flag that post-broker-login flow was finished"); } // remove notes - clientSession.removeNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); - clientSession.removeNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); + authenticationSession.removeAuthNote(PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); + authenticationSession.removeAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN); - return afterPostBrokerLoginFlowSuccess(clientSession, context, wasFirstBrokerLogin, parsedCode.clientSessionCode); + return afterPostBrokerLoginFlowSuccess(authenticationSession, context, wasFirstBrokerLogin, parsedCode.clientSessionCode); } catch (IdentityBrokerException e) { return redirectToErrorPage(Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR, e); } } - private Response afterPostBrokerLoginFlowSuccess(ClientSessionModel clientSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode clientSessionCode) { + private Response afterPostBrokerLoginFlowSuccess(AuthenticationSessionModel authSession, BrokeredIdentityContext context, boolean wasFirstBrokerLogin, ClientSessionCode clientSessionCode) { String providerId = context.getIdpConfig().getAlias(); - UserModel federatedUser = clientSession.getAuthenticatedUser(); + UserModel federatedUser = authSession.getAuthenticatedUser(); if (wasFirstBrokerLogin) { - - String isDifferentBrowser = clientSession.getNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER); - if (Boolean.parseBoolean(isDifferentBrowser)) { - session.sessions().removeClientSession(realmModel, clientSession); - return session.getProvider(LoginFormsProvider.class) - .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername()) - .createInfoPage(); - } else { - return finishBrokerAuthentication(context, federatedUser, clientSession, providerId); - } - + return finishBrokerAuthentication(context, federatedUser, authSession, providerId); } else { - boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null); + boolean firstBrokerLoginInProgress = (authSession.getAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null); if (firstBrokerLoginInProgress) { logger.debugf("Reauthenticated with broker '%s' when linking user '%s' with other broker", context.getIdpConfig().getAlias(), federatedUser.getUsername()); - UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, clientSession); + UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realmModel, authSession); if (!linkingUser.getId().equals(federatedUser.getId())) { return redirectToErrorPage(Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, federatedUser.getUsername(), linkingUser.getUsername()); } + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); + authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, serializedCtx.getIdentityProviderId()); + return afterFirstBrokerLogin(clientSessionCode); } else { - return finishBrokerAuthentication(context, federatedUser, clientSession, providerId); + return finishBrokerAuthentication(context, federatedUser, authSession, providerId); } } } - private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, ClientSessionModel clientSession, String providerId) { - UserSessionModel userSession = this.session.sessions() - .createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false, context.getBrokerSessionId(), context.getBrokerUserId()); + private Response finishBrokerAuthentication(BrokeredIdentityContext context, UserModel federatedUser, AuthenticationSessionModel authSession, String providerId) { + authSession.setAuthNote(AuthenticationProcessor.BROKER_SESSION_ID, context.getBrokerSessionId()); + authSession.setAuthNote(AuthenticationProcessor.BROKER_USER_ID, context.getBrokerUserId()); this.event.user(federatedUser); - this.event.session(userSession); - TokenManager.attachClientSession(userSession, clientSession); - context.getIdp().attachUserSession(userSession, clientSession, context); - userSession.setNote(Details.IDENTITY_PROVIDER, providerId); - userSession.setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); + context.getIdp().authenticationFinished(authSession, context); + authSession.setUserSessionNote(Details.IDENTITY_PROVIDER, providerId); + authSession.setUserSessionNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); + + event.detail(Details.IDENTITY_PROVIDER, providerId) + .detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); if (isDebugEnabled()) { logger.debugf("Performing local authentication for user [%s].", federatedUser); } - return AuthenticationProcessor.redirectToRequiredActions(session, realmModel, clientSession, uriInfo); + AuthenticationManager.setRolesAndMappersInSession(authSession); + + String nextRequiredAction = AuthenticationManager.nextRequiredAction(session, authSession, clientConnection, request, uriInfo, event); + if (nextRequiredAction != null) { + return AuthenticationManager.redirectToRequiredActions(session, realmModel, authSession, uriInfo, nextRequiredAction); + } else { + event.detail(Details.CODE_ID, authSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set + return AuthenticationManager.finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event); + } } @@ -789,7 +808,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (parsedCode.response != null) { return parsedCode.response; } - ClientSessionCode clientCode = parsedCode.clientSessionCode; + ClientSessionCode clientCode = parsedCode.clientSessionCode; Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.CONSENT_DENIED); if (accountManagementFailedLinking != null) { @@ -805,7 +824,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (parsedCode.response != null) { return parsedCode.response; } - ClientSessionCode clientCode = parsedCode.clientSessionCode; + ClientSessionCode clientCode = parsedCode.clientSessionCode; Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), message); if (accountManagementFailedLinking != null) { @@ -815,23 +834,50 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return browserAuthentication(clientCode.getClientSession(), message); } - private Response performAccountLinking(ClientSessionModel clientSession, BrokeredIdentityContext context, FederatedIdentityModel newModel, UserModel federatedUser) { + + private boolean shouldPerformAccountLinking(AuthenticationSessionModel authSession, UserSessionModel userSession, String providerId) { + String noteFromSession = authSession.getAuthNote(LINKING_IDENTITY_PROVIDER); + if (noteFromSession == null) { + return false; + } + + boolean linkingValid; + if (userSession == null) { + linkingValid = false; + } else { + String expectedNote = userSession.getId() + authSession.getClient().getClientId() + providerId; + linkingValid = expectedNote.equals(noteFromSession); + } + + if (linkingValid) { + authSession.removeAuthNote(LINKING_IDENTITY_PROVIDER); + return true; + } else { + throw new ErrorPageException(session, Messages.BROKER_LINKING_SESSION_EXPIRED); + } + } + + + private Response performAccountLinking(AuthenticationSessionModel authSession, UserSessionModel userSession, BrokeredIdentityContext context, FederatedIdentityModel newModel, UserModel federatedUser) { + logger.debugf("Will try to link identity provider [%s] to user [%s]", context.getIdpConfig().getAlias(), userSession.getUser().getUsername()); + this.event.event(EventType.FEDERATED_IDENTITY_LINK); - UserModel authenticatedUser = clientSession.getUserSession().getUser(); + UserModel authenticatedUser = userSession.getUser(); + authSession.setAuthenticatedUser(authenticatedUser); if (federatedUser != null && !authenticatedUser.getId().equals(federatedUser.getId())) { - return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias()); + return redirectToErrorWhenLinkingFailed(authSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias()); } - if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(MANAGE_ACCOUNT))) { + if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(AccountRoles.MANAGE_ACCOUNT))) { return redirectToErrorPage(Messages.INSUFFICIENT_PERMISSION); } if (!authenticatedUser.isEnabled()) { - return redirectToAccountErrorPage(clientSession, Messages.ACCOUNT_DISABLED); + return redirectToErrorWhenLinkingFailed(authSession, Messages.ACCOUNT_DISABLED); } @@ -849,8 +895,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } else { this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, newModel); } - context.getIdp().attachUserSession(clientSession.getUserSession(), clientSession, context); + context.getIdp().authenticationFinished(authSession, context); + AuthenticationManager.setRolesAndMappersInSession(authSession); + TokenManager.attachAuthenticationSession(session, userSession, authSession); if (isDebugEnabled()) { logger.debugf("Linking account [%s] from identity provider [%s] to user [%s].", newModel, context.getIdpConfig().getAlias(), authenticatedUser); @@ -863,13 +911,26 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal .success(); // we do this to make sure that the parent IDP is logged out when this user session is complete. + // But for the case when userSession was previously authenticated with broker1 and now is linked to another broker2, we shouldn't override broker1 notes with the broker2 for sure. + // Maybe broker logout should be rather always skiped in case of broker-linking + if (userSession.getNote(Details.IDENTITY_PROVIDER) == null) { + userSession.setNote(Details.IDENTITY_PROVIDER, context.getIdpConfig().getAlias()); + userSession.setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); + } - clientSession.getUserSession().setNote(Details.IDENTITY_PROVIDER, context.getIdpConfig().getAlias()); - clientSession.getUserSession().setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); - - return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); + return Response.status(302).location(UriBuilder.fromUri(authSession.getRedirectUri()).build()).build(); } + + private Response redirectToErrorWhenLinkingFailed(AuthenticationSessionModel authSession, String message, Object... parameters) { + if (authSession.getClient() != null && authSession.getClient().getClientId().equals(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)) { + return redirectToAccountErrorPage(authSession, message, parameters); + } else { + return redirectToErrorPage(message, parameters); // Should rather redirect to app instead and display error here? + } + } + + private void updateFederatedIdentity(BrokeredIdentityContext context, UserModel federatedUser) { FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel); @@ -900,45 +961,39 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } private ParsedCodeContext parseClientSessionCode(String code) { - ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel); - - if (clientCode != null) { - ClientSessionModel clientSession = clientCode.getClientSession(); - - if (clientSession.getUserSession() != null) { - this.event.session(clientSession.getUserSession()); - } - - ClientModel client = clientSession.getClient(); - - if (client != null) { - - logger.debugf("Got authorization code from client [%s].", client.getClientId()); - this.event.client(client); - this.session.getContext().setClient(client); - - if (!clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { - logger.debugf("Authorization code is not valid. Client session ID: %s, Client session's action: %s", clientSession.getId(), clientSession.getAction()); - - // Check if error happened during login or during linking from account management - Response accountManagementFailedLinking = checkAccountManagementFailedLinking(clientCode.getClientSession(), Messages.STALE_CODE_ACCOUNT); - Response staleCodeError = (accountManagementFailedLinking != null) ? accountManagementFailedLinking : redirectToErrorPage(Messages.STALE_CODE); - - - return ParsedCodeContext.response(staleCodeError); - } - - if (isDebugEnabled()) { - logger.debugf("Authorization code is valid."); - } - - return ParsedCodeContext.clientSessionCode(clientCode); - } + if (code == null) { + logger.debugf("Invalid request. Authorization code was null"); + Response staleCodeError = redirectToErrorPage(Messages.INVALID_REQUEST); + return ParsedCodeContext.response(staleCodeError); } - logger.debugf("Authorization code is not valid. Code: %s", code); - Response staleCodeError = redirectToErrorPage(Messages.STALE_CODE); - return ParsedCodeContext.response(staleCodeError); + SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, clientConnection, session, event, code, null, LoginActionsService.AUTHENTICATE_PATH); + checks.initialVerify(); + if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { + + AuthenticationSessionModel authSession = checks.getAuthenticationSession(); + if (authSession != null) { + // Check if error happened during login or during linking from account management + Response accountManagementFailedLinking = checkAccountManagementFailedLinking(authSession, Messages.STALE_CODE_ACCOUNT); + if (accountManagementFailedLinking != null) { + return ParsedCodeContext.response(accountManagementFailedLinking); + } else { + Response errorResponse = checks.getResponse(); + + // Remove "code" from browser history + errorResponse = BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, errorResponse, true); + return ParsedCodeContext.response(errorResponse); + } + } else { + return ParsedCodeContext.response(checks.getResponse()); + } + } else { + if (isDebugEnabled()) { + logger.debugf("Authorization code is valid."); + } + + return ParsedCodeContext.clientSessionCode(checks.getClientCode()); + } } /** @@ -962,56 +1017,38 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return ParsedCodeContext.response(redirectToErrorPage(Messages.CLIENT_NOT_FOUND)); } - ClientSessionModel clientSession = SamlService.createClientSessionForIdpInitiatedSso(session, realmModel, oClient.get(), null); + SamlService samlService = new SamlService(realmModel, event); + ResteasyProviderFactory.getInstance().injectProperties(samlService); + AuthenticationSessionModel authSession = samlService.getOrCreateLoginSessionForIdpInitiatedSso(session, realmModel, oClient.get(), null); - return ParsedCodeContext.clientSessionCode(new ClientSessionCode(session, this.realmModel, clientSession)); + return ParsedCodeContext.clientSessionCode(new ClientSessionCode<>(session, this.realmModel, authSession)); } - /** - * Returns {@code true} if the client session is defined for the given code - * in the current session and for the current realm. - * Does not check the session validity. To obtain client session if - * and only if it exists and is valid, use {@link ClientSessionCode#parse}. - * - * @param code - * @return - */ - protected boolean isClientSessionRegistered(String code) { - if (code == null) { - return false; - } - - try { - return ClientSessionCode.getClientSession(code, this.session, this.realmModel) != null; - } catch (RuntimeException e) { - return false; - } - } - - private Response checkAccountManagementFailedLinking(ClientSessionModel clientSession, String error, Object... parameters) { - if (clientSession.getUserSession() != null && clientSession.getClient() != null && clientSession.getClient().getClientId().equals(ACCOUNT_MANAGEMENT_CLIENT_ID)) { + private Response checkAccountManagementFailedLinking(AuthenticationSessionModel authSession, String error, Object... parameters) { + UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authSession); + if (userSession != null && authSession.getClient() != null && authSession.getClient().getClientId().equals(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID)) { this.event.event(EventType.FEDERATED_IDENTITY_LINK); - UserModel user = clientSession.getUserSession().getUser(); + UserModel user = userSession.getUser(); this.event.user(user); this.event.detail(Details.USERNAME, user.getUsername()); - return redirectToAccountErrorPage(clientSession, error, parameters); + return redirectToAccountErrorPage(authSession, error, parameters); } else { return null; } } - private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode clientSessionCode) { - ClientSessionModel clientSession = null; + private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode clientSessionCode) { + AuthenticationSessionModel authSession = null; String relayState = null; if (clientSessionCode != null) { - clientSession = clientSessionCode.getClientSession(); + authSession = clientSessionCode.getClientSession(); relayState = clientSessionCode.getCode(); } - return new AuthenticationRequest(this.session, this.realmModel, clientSession, this.request, this.uriInfo, relayState, getRedirectUri(providerId)); + return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.uriInfo, relayState, getRedirectUri(providerId)); } private String getRedirectUri(String providerId) { @@ -1028,40 +1065,36 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } fireErrorEvent(message, throwable); + + if (throwable != null && throwable instanceof WebApplicationException) { + WebApplicationException webEx = (WebApplicationException) throwable; + return webEx.getResponse(); + } + return ErrorPage.error(this.session, message, parameters); } - private Response redirectToAccountErrorPage(ClientSessionModel clientSession, String message, Object ... parameters) { + private Response redirectToAccountErrorPage(AuthenticationSessionModel authSession, String message, Object ... parameters) { fireErrorEvent(message); FormMessage errorMessage = new FormMessage(message, parameters); try { String serializedError = JsonSerialization.writeValueAsString(errorMessage); - clientSession.setNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError); + authSession.setAuthNote(AccountService.ACCOUNT_MGMT_FORWARDED_ERROR_NOTE, serializedError); } catch (IOException ioe) { throw new RuntimeException(ioe); } - return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); + return Response.status(302).location(UriBuilder.fromUri(authSession.getRedirectUri()).build()).build(); } - private Response redirectToLoginPage(Throwable t, ClientSessionCode clientCode) { - String message = t.getMessage(); - if (message == null) { - message = Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR; - } - - fireErrorEvent(message); - return browserAuthentication(clientCode.getClientSession(), message); - } - - protected Response browserAuthentication(ClientSessionModel clientSession, String errorMessage) { + protected Response browserAuthentication(AuthenticationSessionModel authSession, String errorMessage) { this.event.event(EventType.LOGIN); AuthenticationFlowModel flow = realmModel.getBrowserFlow(); String flowId = flow.getId(); AuthenticationProcessor processor = new AuthenticationProcessor(); - processor.setClientSession(clientSession) + processor.setAuthenticationSession(authSession) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH) .setFlowId(flowId) .setBrowserFlow(true) @@ -1084,12 +1117,12 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal private Response badRequest(String message) { fireErrorEvent(message); - return ErrorResponse.error(message, Status.BAD_REQUEST); + return ErrorResponse.error(message, Response.Status.BAD_REQUEST); } private Response forbidden(String message) { fireErrorEvent(message); - return ErrorResponse.error(message, Status.FORBIDDEN); + return ErrorResponse.error(message, Response.Status.FORBIDDEN); } public static IdentityProvider getIdentityProvider(KeycloakSession session, RealmModel realm, String alias) { @@ -1177,10 +1210,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal private static class ParsedCodeContext { - private ClientSessionCode clientSessionCode; + private ClientSessionCode clientSessionCode; private Response response; - public static ParsedCodeContext clientSessionCode(ClientSessionCode clientSessionCode) { + public static ParsedCodeContext clientSessionCode(ClientSessionCode clientSessionCode) { ParsedCodeContext ctx = new ParsedCodeContext(); ctx.clientSessionCode = clientSessionCode; return ctx; @@ -1192,4 +1225,5 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return ctx; } } + } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 14df1dc222..8f0d39ecce 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -16,6 +16,8 @@ */ package org.keycloak.services.resources; +import org.keycloak.authentication.actiontoken.DefaultActionToken; +import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; @@ -24,20 +26,24 @@ 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.authentication.actiontoken.*; +import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler; 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; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.exceptions.TokenNotActiveException; import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -47,12 +53,10 @@ 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.AuthorizationEndpointBase; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.LoginProtocol.Error; -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; @@ -60,10 +64,13 @@ 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.AuthenticationSessionManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; import org.keycloak.services.util.CacheControlUtil; -import org.keycloak.services.util.CookieHelper; +import org.keycloak.services.util.AuthenticationFlowURLHelper; +import org.keycloak.services.util.BrowserHistoryHelper; +import org.keycloak.sessions.AuthenticationSessionModel; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -72,7 +79,6 @@ import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; -import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; @@ -81,6 +87,10 @@ 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.Map; + +import javax.ws.rs.core.*; +import static org.keycloak.authentication.actiontoken.DefaultActionToken.ACTION_TOKEN_BASIC_CHECKS; /** * @author Stian Thorgersen @@ -89,14 +99,16 @@ public class LoginActionsService { private static final Logger logger = Logger.getLogger(LoginActionsService.class); - public static final String ACTION_COOKIE = "KEYCLOAK_ACTION"; public static final String AUTHENTICATE_PATH = "authenticate"; public static final String REGISTRATION_PATH = "registration"; public static final String RESET_CREDENTIALS_PATH = "reset-credentials"; public static final String REQUIRED_ACTION = "required-action"; public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login"; public static final String POST_BROKER_LOGIN_PATH = "post-broker-login"; - public static final String LAST_PROCESSED_CODE = "last_processed_code"; + + public static final String RESTART_PATH = "restart"; + + public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage"; private RealmModel realm; @@ -133,6 +145,10 @@ public class LoginActionsService { return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "requiredActionPOST"); } + public static UriBuilder actionTokenProcessor(UriInfo uriInfo) { + return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "executeActionToken"); + } + public static UriBuilder registrationFormProcessor(UriInfo uriInfo) { return loginActionsBaseUrl(uriInfo).path(LoginActionsService.class, "processRegister"); } @@ -163,153 +179,45 @@ public class LoginActionsService { } } + private SessionCodeChecks checksForCode(String code, String execution, String flowPath) { + SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, code, execution, flowPath); + res.initialVerify(); + return res; + } - private class Checks { - ClientSessionCode clientCode; - Response response; - ClientSessionCode.ParseResult result; - boolean verifyCode(String code, String requiredAction, ClientSessionCode.ActionType actionType) { - if (!verifyCode(code)) { - return false; - } - if (!clientCode.isValidAction(requiredAction)) { - ClientSessionModel clientSession = clientCode.getClientSession(); - if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(clientSession.getAction())) { - response = redirectToRequiredActions(code); - return false; - } else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) { - response = session.getProvider(LoginFormsProvider.class) - .setSuccess(Messages.ALREADY_LOGGED_IN) - .createInfoPage(); - return false; - } - } - if (!isActionActive(actionType)) return false; - return true; - } + protected URI getLastExecutionUrl(String flowPath, String executionId) { + return new AuthenticationFlowURLHelper(session, realm, uriInfo) + .getLastExecutionUrl(flowPath, executionId); + } - private boolean isValidAction(String requiredAction) { - if (!clientCode.isValidAction(requiredAction)) { - invalidAction(); - return false; - } - return true; + + /** + * protocol independent page for restart of the flow + * + * @return + */ + @Path(RESTART_PATH) + @GET + public Response restartSession() { + event.event(EventType.RESTART_AUTHENTICATION); + SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, clientConnection, session, event, null, null, null); + + AuthenticationSessionModel authSession = checks.initialVerifyAuthSession(); + if (authSession == null) { + return checks.getResponse(); } - private void invalidAction() { - event.client(clientCode.getClientSession().getClient()); - event.error(Errors.INVALID_CODE); - response = ErrorPage.error(session, Messages.INVALID_CODE); + String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW); + if (flowPath == null) { + flowPath = AUTHENTICATE_PATH; } - private boolean isActionActive(ClientSessionCode.ActionType actionType) { - if (!clientCode.isActionActive(actionType)) { - event.client(clientCode.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); - return false; - } - response = ErrorPage.error(session, Messages.EXPIRED_CODE); - return false; - } - return true; - } + AuthenticationProcessor.resetFlow(authSession, flowPath); - public boolean verifyCode(String code) { - if (!checkSsl()) { - event.error(Errors.SSL_REQUIRED); - response = ErrorPage.error(session, Messages.HTTPS_REQUIRED); - return false; - } - if (!realm.isEnabled()) { - event.error(Errors.REALM_DISABLED); - response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED); - return false; - } - result = ClientSessionCode.parseResult(code, session, realm); - clientCode = result.getCode(); - if (clientCode == null) { - if (result.isClientSessionNotFound()) { // timeout - try { - ClientSessionModel clientSession = RestartLoginCookie.restartSession(session, realm, code); - if (clientSession != null) { - event.clone().detail(Details.RESTART_AFTER_TIMEOUT, "true").error(Errors.EXPIRED_CODE); - response = processFlow(null, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), Messages.LOGIN_TIMEOUT, new AuthenticationProcessor()); - return false; - } - } catch (Exception e) { - ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e); - } - } - event.error(Errors.INVALID_CODE); - response = ErrorPage.error(session, Messages.INVALID_CODE); - return false; - } - ClientSessionModel clientSession = clientCode.getClientSession(); - if (clientSession == null) { - event.error(Errors.INVALID_CODE); - response = ErrorPage.error(session, Messages.INVALID_CODE); - return false; - } - event.detail(Details.CODE_ID, clientSession.getId()); - ClientModel client = clientSession.getClient(); - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER); - 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.sessions().removeClientSession(realm, clientSession); - return false; - } - session.getContext().setClient(client); - return true; - } - - public boolean verifyRequiredAction(String code, String executedAction) { - if (!verifyCode(code)) { - return false; - } - if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false; - if (!isActionActive(ClientSessionCode.ActionType.USER)) return false; - - final ClientSessionModel clientSession = clientCode.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); - event.event(EventType.LOGIN); - response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); - return false; - } - - if (!executedAction.equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) { - logger.debug("required action doesn't match current required action"); - clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION); - response = redirectToRequiredActions(code); - return false; - } - return true; - - } + URI redirectUri = getLastExecutionUrl(flowPath, null); + logger.debugf("Flow restart requested. Redirecting to %s", redirectUri); + return Response.status(Response.Status.FOUND).location(redirectUri).build(); } @@ -325,30 +233,23 @@ public class LoginActionsService { @QueryParam("execution") String execution) { event.event(EventType.LOGIN); - ClientSessionModel clientSession = ClientSessionCode.getClientSession(code, session, realm); - if (clientSession != null && code.equals(clientSession.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)) { - return checks.response; - } - - ClientSessionCode clientSessionCode = checks.clientCode; - clientSession = clientSessionCode.getClientSession(); + SessionCodeChecks checks = checksForCode(code, execution, AUTHENTICATE_PATH); + if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { + return checks.getResponse(); } - event.detail(Details.CODE_ID, code); - clientSession.setNote(LAST_PROCESSED_CODE, code); - return processAuthentication(execution, clientSession, null); + AuthenticationSessionModel authSession = checks.getAuthenticationSession(); + boolean actionRequest = checks.isActionRequest(); + + return processAuthentication(actionRequest, execution, authSession, null); } - protected Response processAuthentication(String execution, ClientSessionModel clientSession, String errorMessage) { - return processFlow(execution, clientSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor()); + protected Response processAuthentication(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) { + return processFlow(action, execution, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), errorMessage, new AuthenticationProcessor()); } - protected Response processFlow(String execution, ClientSessionModel clientSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) { - processor.setClientSession(clientSession) + protected Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) { + processor.setAuthenticationSession(authSession) .setFlowPath(flowPath) .setBrowserFlow(true) .setFlowId(flow.getId()) @@ -358,17 +259,34 @@ public class LoginActionsService { .setSession(session) .setUriInfo(uriInfo) .setRequest(request); - if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage)); - - try { - if (execution != null) { - return processor.authenticationAction(execution); - } else { - return processor.authenticate(); - } - } catch (Exception e) { - return processor.handleBrowserException(e); + if (errorMessage != null) { + processor.setForwardedErrorMessage(new FormMessage(null, errorMessage)); } + + // Check the forwarded error message, which was set by previous HTTP request + String forwardedErrorMessage = authSession.getAuthNote(FORWARDED_ERROR_MESSAGE_NOTE); + if (forwardedErrorMessage != null) { + authSession.removeAuthNote(FORWARDED_ERROR_MESSAGE_NOTE); + processor.setForwardedErrorMessage(new FormMessage(null, forwardedErrorMessage)); + } + + + Response response; + try { + if (action) { + response = processor.authenticationAction(execution); + } else { + response = processor.authenticate(); + } + } catch (WebApplicationException e) { + response = e.getResponse(); + authSession = processor.getAuthenticationSession(); + } catch (Exception e) { + response = processor.handleBrowserException(e); + authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow) + } + + return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action); } /** @@ -381,35 +299,25 @@ public class LoginActionsService { @POST public Response authenticateForm(@QueryParam("code") String code, @QueryParam("execution") String execution) { - event.event(EventType.LOGIN); - - ClientSessionModel clientSession = ClientSessionCode.getClientSession(code, session, realm); - if (clientSession != null && code.equals(clientSession.getNote(LAST_PROCESSED_CODE))) { - // Post already processed (refresh) - ignore form post and return next form - request.getFormParameters().clear(); - return authenticate(code, null); - } - - Checks checks = new Checks(); - if (!checks.verifyCode(code, ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { - return checks.response; - } - final ClientSessionCode clientCode = checks.clientCode; - clientSession = clientCode.getClientSession(); - clientSession.setNote(LAST_PROCESSED_CODE, code); - - return processAuthentication(execution, clientSession, null); + return authenticate(code, execution); } @Path(RESET_CREDENTIALS_PATH) @POST public Response resetCredentialsPOST(@QueryParam("code") String code, - @QueryParam("execution") String execution) { + @QueryParam("execution") String execution, + @QueryParam(Constants.KEY) String key) { + if (key != null) { + return handleActionToken(key, execution); + } + + event.event(EventType.RESET_PASSWORD); + return resetCredentials(code, execution); } /** - * 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 token 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. * * @param code @@ -419,80 +327,237 @@ public class LoginActionsService { @Path(RESET_CREDENTIALS_PATH) @GET public Response resetCredentialsGET(@QueryParam("code") String code, - @QueryParam("execution") String execution) { + @QueryParam("execution") String execution) { + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); + // we allow applications to link to reset credentials without going through OAuth or SAML handshakes - // - if (code == null) { + if (authSession == null && code == null) { if (!realm.isResetPasswordAllowed()) { event.event(EventType.RESET_PASSWORD); event.error(Errors.NOT_ALLOWED); return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED); } - // set up the account service as the endpoint to call. - ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); - 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); - String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); - clientSession.setRedirectUri(redirectUri); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); - clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); - clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); - clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); - return processResetCredentials(null, clientSession, null); + authSession = createAuthenticationSessionForClient(); + return processResetCredentials(false, null, authSession); } + + event.event(EventType.RESET_PASSWORD); return resetCredentials(code, execution); } + AuthenticationSessionModel createAuthenticationSessionForClient() + throws UriBuilderException, IllegalArgumentException { + AuthenticationSessionModel authSession; + + // set up the account service as the endpoint to call. + ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true); + authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + //authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); + authSession.setRedirectUri(redirectUri); + authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); + authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri); + authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); + + return authSession; + } + + /** + * @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)) { - return checks.response; + SessionCodeChecks checks = checksForCode(code, execution, RESET_CREDENTIALS_PATH); + if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) { + return checks.getResponse(); } - final ClientSessionCode clientCode = checks.clientCode; - final ClientSessionModel clientSession = clientCode.getClientSession(); + final AuthenticationSessionModel authSession = checks.getAuthenticationSession(); if (!realm.isResetPasswordAllowed()) { - event.client(clientCode.getClientSession().getClient()); event.error(Errors.NOT_ALLOWED); return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED); } - return processResetCredentials(execution, clientSession, null); + return processResetCredentials(checks.isActionRequest(), execution, authSession); } - protected Response processResetCredentials(String execution, ClientSessionModel clientSession, String errorMessage) { - AuthenticationProcessor authProcessor = new AuthenticationProcessor() { + /** + * Handles a given token using the given token handler. If there is any {@link VerificationException} thrown + * in the handler, it is handled automatically here to reduce boilerplate code. + * + * @param key + * @param execution + * @return + */ + @Path("action-token") + @GET + public Response executeActionToken(@QueryParam("key") String key, + @QueryParam("execution") String execution) { + return handleActionToken(key, execution); + } - @Override - protected Response authenticationComplete() { - boolean firstBrokerLoginInProgress = (clientSession.getNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null); - if (firstBrokerLoginInProgress) { + protected Response handleActionToken(String tokenString, String execution) { + T token; + ActionTokenHandler handler; + ActionTokenContext tokenContext; + String eventError = null; + String defaultErrorMessage = null; + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); - UserModel linkingUser = AbstractIdpAuthenticator.getExistingUser(session, realm, clientSession); - if (!linkingUser.getId().equals(clientSession.getAuthenticatedUser().getId())) { - return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, clientSession.getAuthenticatedUser().getUsername(), linkingUser.getUsername()); - } + event.event(EventType.EXECUTE_ACTION_TOKEN); - logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername()); - - return redirectToAfterBrokerLoginEndpoint(clientSession, true); - } else { - return super.authenticationComplete(); - } + // First resolve action token handler + try { + if (tokenString == null) { + throw new ExplainedTokenVerificationException(null, Errors.NOT_ALLOWED, Messages.INVALID_REQUEST); } - }; - return processFlow(execution, clientSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), errorMessage, authProcessor); + TokenVerifier tokenVerifier = TokenVerifier.create(tokenString, DefaultActionToken.class); + DefaultActionToken aToken = tokenVerifier.getToken(); + + event + .detail(Details.TOKEN_ID, aToken.getId()) + .detail(Details.ACTION, aToken.getActionId()) + .user(aToken.getUserId()); + + handler = resolveActionTokenHandler(aToken.getActionId()); + eventError = handler.getDefaultEventError(); + defaultErrorMessage = handler.getDefaultErrorMessage(); + + if (! realm.isEnabled()) { + throw new ExplainedTokenVerificationException(aToken, Errors.REALM_DISABLED, Messages.REALM_NOT_ENABLED); + } + if (! checkSsl()) { + throw new ExplainedTokenVerificationException(aToken, Errors.SSL_REQUIRED, Messages.HTTPS_REQUIRED); + } + + tokenVerifier + .withChecks( + // Token introspection checks + TokenVerifier.IS_ACTIVE, + new TokenVerifier.RealmUrlCheck(Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())), + ACTION_TOKEN_BASIC_CHECKS + ) + + .secretKey(session.keys().getActiveHmacKey(realm).getSecretKey()) + .verify(); + + token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken(); + } catch (TokenNotActiveException ex) { + if (authSession != null) { + event.clone().error(Errors.EXPIRED_CODE); + String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW); + if (flowPath == null) { + flowPath = AUTHENTICATE_PATH; + } + AuthenticationProcessor.resetFlow(authSession, flowPath); + return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT); + } + + return handleActionTokenVerificationException(null, ex, Errors.EXPIRED_CODE, defaultErrorMessage); + } catch (ExplainedTokenVerificationException ex) { + return handleActionTokenVerificationException(null, ex, ex.getErrorEvent(), ex.getMessage()); + } catch (VerificationException ex) { + return handleActionTokenVerificationException(null, ex, eventError, defaultErrorMessage); + } + + // Now proceed with the verification and handle the token + tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow); + + try { + String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token); + + if (tokenAuthSessionId != null) { + // This can happen if the token contains ID but user opens the link in a new browser + LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId); + } + + if (authSession == null) { + authSession = handler.startFreshAuthenticationSession(token, tokenContext); + tokenContext.setAuthenticationSession(authSession, true); + } else if (tokenAuthSessionId == null || + ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) { + // There exists an authentication session but no auth session ID was received in the action token + logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId()); + new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false); + + authSession = handler.startFreshAuthenticationSession(token, tokenContext); + tokenContext.setAuthenticationSession(authSession, true); + } + + initLoginEvent(authSession); + event.event(handler.eventType()); + + LoginActionsServiceChecks.checkIsUserValid(token, tokenContext); + LoginActionsServiceChecks.checkIsClientValid(token, tokenContext); + + session.getContext().setClient(authSession.getClient()); + + TokenVerifier.create(token) + .withChecks(handler.getVerifiers(tokenContext)) + .verify(); + + authSession = tokenContext.getAuthenticationSession(); + event = tokenContext.getEvent(); + event.event(handler.eventType()); + + if (! handler.canUseTokenRepeatedly(token, tokenContext)) { + LoginActionsServiceChecks.checkTokenWasNotUsedYet(token, tokenContext); + authSession.setAuthNote(AuthenticationManager.INVALIDATE_ACTION_TOKEN, token.serializeKey()); + } + + authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId()); + + return handler.handleToken(token, tokenContext); + } catch (ExplainedTokenVerificationException ex) { + return handleActionTokenVerificationException(tokenContext, ex, ex.getErrorEvent(), ex.getMessage()); + } catch (LoginActionsServiceException ex) { + Response response = ex.getResponse(); + return response == null + ? handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage) + : response; + } catch (VerificationException ex) { + return handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage); + } + } + + private ActionTokenHandler resolveActionTokenHandler(String actionId) throws VerificationException { + if (actionId == null) { + throw new VerificationException("Action token operation not set"); + } + ActionTokenHandler handler = session.getProvider(ActionTokenHandler.class, actionId); + + if (handler == null) { + throw new VerificationException("Invalid action token operation"); + } + return handler; + } + + private Response handleActionTokenVerificationException(ActionTokenContext tokenContext, VerificationException ex, String eventError, String errorMessage) { + if (tokenContext != null && tokenContext.getAuthenticationSession() != null) { + new AuthenticationSessionManager(session).removeAuthenticationSession(realm, tokenContext.getAuthenticationSession(), true); + } + + event + .detail(Details.REASON, ex == null ? "" : ex.getMessage()) + .error(eventError == null ? Errors.INVALID_CODE : eventError); + return ErrorPage.error(session, errorMessage == null ? Messages.INVALID_CODE : errorMessage); + } + + protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession) { + AuthenticationProcessor authProcessor = new ResetCredentialsActionTokenHandler.ResetCredsAuthenticationProcessor(); + + return processFlow(actionRequest, execution, authSession, RESET_CREDENTIALS_PATH, realm.getResetCredentialsFlow(), null, authProcessor); } - protected Response processRegistration(String execution, ClientSessionModel clientSession, String errorMessage) { - return processFlow(execution, clientSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor()); + protected Response processRegistration(boolean action, String execution, AuthenticationSessionModel authSession, String errorMessage) { + return processFlow(action, execution, authSession, REGISTRATION_PATH, realm.getRegistrationFlow(), errorMessage, new AuthenticationProcessor()); } @@ -506,24 +571,7 @@ public class LoginActionsService { @GET public Response registerPage(@QueryParam("code") String code, @QueryParam("execution") String execution) { - event.event(EventType.REGISTER); - if (!realm.isRegistrationAllowed()) { - 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)) { - return checks.response; - } - event.detail(Details.CODE_ID, code); - ClientSessionCode clientSessionCode = checks.clientCode; - ClientSessionModel clientSession = clientSessionCode.getClientSession(); - - - AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection); - - return processRegistration(execution, clientSession, null); + return registerRequest(code, execution, false); } @@ -537,20 +585,27 @@ public class LoginActionsService { @POST public Response processRegister(@QueryParam("code") String code, @QueryParam("execution") String execution) { + return registerRequest(code, execution, true); + } + + + private Response registerRequest(String code, String execution, boolean isPostRequest) { event.event(EventType.REGISTER); if (!realm.isRegistrationAllowed()) { 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)) { - return checks.response; + + SessionCodeChecks checks = checksForCode(code, execution, REGISTRATION_PATH); + if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { + return checks.getResponse(); } - ClientSessionCode clientCode = checks.clientCode; - ClientSessionModel clientSession = clientCode.getClientSession(); + AuthenticationSessionModel authSession = checks.getAuthenticationSession(); - return processRegistration(execution, clientSession, null); + AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection); + + return processRegistration(checks.isActionRequest(), execution, authSession, null); } @@ -558,50 +613,51 @@ public class LoginActionsService { @GET public Response firstBrokerLoginGet(@QueryParam("code") String code, @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, true); + return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH); } @Path(FIRST_BROKER_LOGIN_PATH) @POST public Response firstBrokerLoginPost(@QueryParam("code") String code, @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, true); + return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH); } @Path(POST_BROKER_LOGIN_PATH) @GET public Response postBrokerLoginGet(@QueryParam("code") String code, @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, false); + return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH); } @Path(POST_BROKER_LOGIN_PATH) @POST public Response postBrokerLoginPost(@QueryParam("code") String code, @QueryParam("execution") String execution) { - return brokerLoginFlow(code, execution, false); + return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH); } - protected Response brokerLoginFlow(String code, String execution, final boolean firstBrokerLogin) { + protected Response brokerLoginFlow(String code, String execution, String flowPath) { + boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH); + 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)) { - return checks.response; + SessionCodeChecks checks = checksForCode(code, execution, flowPath); + if (!checks.verifyActiveAndValidAction(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { + return checks.getResponse(); } event.detail(Details.CODE_ID, code); - ClientSessionCode clientSessionCode = checks.clientCode; - final ClientSessionModel clientSessionn = clientSessionCode.getClientSession(); + final AuthenticationSessionModel authSession = checks.getAuthenticationSession(); String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT; - SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey); + SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey); if (serializedCtx == null) { ServicesLogger.LOGGER.notFoundSerializedCtxInClientSession(noteKey); throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession.")); } - BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSessionn); + BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, authSession); final String identityProviderAlias = brokerContext.getIdpConfig().getAlias(); String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId(); @@ -623,23 +679,28 @@ public class LoginActionsService { @Override protected Response authenticationComplete() { - if (!firstBrokerLogin) { + if (firstBrokerLogin) { + authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, identityProviderAlias); + } else { String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + identityProviderAlias; - clientSessionn.setNote(authStateNoteKey, "true"); + authSession.setAuthNote(authStateNoteKey, "true"); } - return redirectToAfterBrokerLoginEndpoint(clientSession, firstBrokerLogin); + return redirectToAfterBrokerLoginEndpoint(authSession, firstBrokerLogin); } }; - String flowPath = firstBrokerLogin ? FIRST_BROKER_LOGIN_PATH : POST_BROKER_LOGIN_PATH; - return processFlow(execution, clientSessionn, flowPath, brokerLoginFlow, null, processor); + return processFlow(checks.isActionRequest(), execution, authSession, flowPath, brokerLoginFlow, null, processor); } - private Response redirectToAfterBrokerLoginEndpoint(ClientSessionModel clientSession, boolean firstBrokerLogin) { - ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); - clientSession.setTimestamp(Time.currentTime()); + private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) { + return redirectToAfterBrokerLoginEndpoint(session, realm, uriInfo, authSession, firstBrokerLogin); + } + + public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) { + ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession); + authSession.setTimestamp(Time.currentTime()); URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) : Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ; @@ -648,6 +709,7 @@ public class LoginActionsService { return Response.status(302).location(redirect).build(); } + /** * OAuth grant page. You should not invoked this directly! * @@ -660,27 +722,26 @@ public class LoginActionsService { public Response processConsent(final MultivaluedMap formData) { event.event(EventType.LOGIN); String code = formData.getFirst("code"); - Checks checks = new Checks(); - if (!checks.verifyRequiredAction(code, ClientSessionModel.Action.OAUTH_GRANT.name())) { - return checks.response; + SessionCodeChecks checks = checksForCode(code, null, REQUIRED_ACTION); + if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) { + return checks.getResponse(); } - ClientSessionCode accessCode = checks.clientCode; - ClientSessionModel clientSession = accessCode.getClientSession(); - initEvent(clientSession); + AuthenticationSessionModel authSession = checks.getAuthenticationSession(); - UserSessionModel userSession = clientSession.getUserSession(); - UserModel user = userSession.getUser(); - ClientModel client = clientSession.getClient(); + initLoginEvent(authSession); + + UserModel user = authSession.getAuthenticatedUser(); + ClientModel client = authSession.getClient(); if (formData.containsKey("cancel")) { - LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); + LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol()); protocol.setRealm(realm) .setHttpHeaders(headers) .setUriInfo(uriInfo) .setEventBuilder(event); - Response response = protocol.sendError(clientSession, Error.CONSENT_DENIED); + Response response = protocol.sendError(authSession, Error.CONSENT_DENIED); event.error(Errors.REJECTED_BY_USER); return response; } @@ -690,10 +751,10 @@ public class LoginActionsService { grantedConsent = new UserConsentModel(client); session.users().addConsent(realm, user.getId(), grantedConsent); } - for (RoleModel role : accessCode.getRequestedRoles()) { + for (RoleModel role : ClientSessionCode.getRequestedRoles(authSession, realm)) { grantedConsent.addGrantedRole(role); } - for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) { + for (ProtocolMapperModel protocolMapper : ClientSessionCode.getRequestedProtocolMappers(authSession.getProtocolMappers(), client)) { if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) { grantedConsent.addGrantedProtocolMapper(protocolMapper); } @@ -703,186 +764,82 @@ public class LoginActionsService { event.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED); event.success(); - return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, clientConnection, event); + AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, null, session, realm, clientConnection, event); + return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol()); } - @Path("email-verification") - @GET - public Response emailVerification(@QueryParam("code") String code, @QueryParam("key") String key) { - event.event(EventType.VERIFY_EMAIL); - if (key != null) { - ClientSessionModel clientSession = null; - String keyFromSession = null; - if (code != null) { - clientSession = ClientSessionCode.getClientSession(code, session, realm); - keyFromSession = clientSession != null ? clientSession.getNote(Constants.VERIFY_EMAIL_KEY) : null; - } - - if (!key.equals(keyFromSession)) { - ServicesLogger.LOGGER.invalidKeyForEmailVerification(); - event.error(Errors.INVALID_CODE); - throw new WebApplicationException(ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK)); - } - - clientSession.removeNote(Constants.VERIFY_EMAIL_KEY); - - Checks checks = new Checks(); - if (!checks.verifyCode(code, 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(); - if (!ClientSessionModel.Action.VERIFY_EMAIL.name().equals(clientSession.getNote(AuthenticationManager.CURRENT_REQUIRED_ACTION))) { - ServicesLogger.LOGGER.reqdActionDoesNotMatch(); - event.error(Errors.INVALID_CODE); - throw new WebApplicationException(ErrorPage.error(session, Messages.STALE_VERIFY_EMAIL_LINK)); - } - - UserSessionModel userSession = clientSession.getUserSession(); - UserModel user = userSession.getUser(); - initEvent(clientSession); - event.event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()); - - user.setEmailVerified(true); - - user.removeRequiredAction(RequiredAction.VERIFY_EMAIL); - - event.success(); - - String actionCookieValue = getActionCookie(); - if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { - session.sessions().removeClientSession(realm, clientSession); - return session.getProvider(LoginFormsProvider.class) - .setSuccess(Messages.EMAIL_VERIFIED) - .createInfoPage(); - } - - event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN); - - return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo); - } else { - Checks checks = new Checks(); - if (!checks.verifyCode(code, ClientSessionModel.Action.REQUIRED_ACTIONS.name(), ClientSessionCode.ActionType.USER)) { - return checks.response; - } - ClientSessionCode accessCode = checks.clientCode; - ClientSessionModel clientSession = accessCode.getClientSession(); - UserSessionModel userSession = clientSession.getUserSession(); - initEvent(clientSession); - - createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); - - VerifyEmail.setupKey(clientSession); - - return session.getProvider(LoginFormsProvider.class) - .setClientSessionCode(accessCode.getCode()) - .setClientSession(clientSession) - .setUser(userSession.getUser()) - .createResponse(RequiredAction.VERIFY_EMAIL); - } - } - - /** - * Initiated by admin, not the user on login - * - * @param key - * @return - */ - @Path("execute-actions") - @GET - public Response executeActions(@QueryParam("key") String key) { - event.event(EventType.EXECUTE_ACTIONS); - if (key != null) { - Checks checks = new Checks(); - if (!checks.verifyCode(key, ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) { - return checks.response; - } - ClientSessionModel clientSession = checks.clientCode.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"); - clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true"); - return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo); - } else { - event.error(Errors.INVALID_CODE); - return ErrorPage.error(session, Messages.INVALID_CODE); - } - } - - private String getActionCookie() { - return getActionCookie(headers, realm, uriInfo, clientConnection); - } - - public static String getActionCookie(HttpHeaders headers, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection) { - Cookie cookie = headers.getCookies().get(ACTION_COOKIE); - AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection); - return cookie != null ? cookie.getValue() : null; - } - - public static void createActionCookie(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, String sessionId) { - CookieHelper.addCookie(ACTION_COOKIE, sessionId, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, realm.getSslRequired().isRequired(clientConnection), true); - } - - private void initEvent(ClientSessionModel clientSession) { - UserSessionModel userSession = clientSession.getUserSession(); - - String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); + private void initLoginEvent(AuthenticationSessionModel authSession) { + String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM); if (responseType == null) { responseType = "code"; } - String respMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM); + String respMode = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM); OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType)); - event.event(EventType.LOGIN).client(clientSession.getClient()) - .user(userSession.getUser()) - .session(userSession.getId()) - .detail(Details.CODE_ID, clientSession.getId()) - .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) - .detail(Details.USERNAME, clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME)) - .detail(Details.AUTH_METHOD, userSession.getAuthMethod()) - .detail(Details.USERNAME, userSession.getLoginUsername()) + event.event(EventType.LOGIN).client(authSession.getClient()) + .detail(Details.CODE_ID, authSession.getId()) + .detail(Details.REDIRECT_URI, authSession.getRedirectUri()) + .detail(Details.AUTH_METHOD, authSession.getProtocol()) .detail(Details.RESPONSE_TYPE, responseType) - .detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase()) - .detail(Details.IDENTITY_PROVIDER, userSession.getNote(Details.IDENTITY_PROVIDER)) - .detail(Details.IDENTITY_PROVIDER_USERNAME, userSession.getNote(Details.IDENTITY_PROVIDER_USERNAME)); + .detail(Details.RESPONSE_MODE, responseMode.toString().toLowerCase()); - if (userSession.isRememberMe()) { - event.detail(Details.REMEMBER_ME, "true"); + UserModel authenticatedUser = authSession.getAuthenticatedUser(); + if (authenticatedUser != null) { + event.user(authenticatedUser) + .detail(Details.USERNAME, authenticatedUser.getUsername()); + } + + String attemptedUsername = authSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); + if (attemptedUsername != null) { + event.detail(Details.USERNAME, attemptedUsername); + } + + String rememberMe = authSession.getAuthNote(Details.REMEMBER_ME); + if (rememberMe==null || !rememberMe.equalsIgnoreCase("true")) { + rememberMe = "false"; + } + event.detail(Details.REMEMBER_ME, rememberMe); + + Map userSessionNotes = authSession.getUserSessionNotes(); + String identityProvider = userSessionNotes.get(Details.IDENTITY_PROVIDER); + if (identityProvider != null) { + event.detail(Details.IDENTITY_PROVIDER, identityProvider) + .detail(Details.IDENTITY_PROVIDER_USERNAME, userSessionNotes.get(Details.IDENTITY_PROVIDER_USERNAME)); } } @Path(REQUIRED_ACTION) @POST public Response requiredActionPOST(@QueryParam("code") final String code, - @QueryParam("action") String action) { + @QueryParam("execution") String action) { return processRequireAction(code, action); - - - } @Path(REQUIRED_ACTION) @GET public Response requiredActionGET(@QueryParam("code") final String code, - @QueryParam("action") String action) { + @QueryParam("execution") String action) { return processRequireAction(code, action); } - public Response processRequireAction(final String code, String action) { + private Response processRequireAction(final String code, String action) { + event.event(EventType.CUSTOM_REQUIRED_ACTION); + + SessionCodeChecks checks = checksForCode(code, action, REQUIRED_ACTION); + if (!checks.verifyRequiredAction(action)) { + return checks.getResponse(); + } + + AuthenticationSessionModel authSession = checks.getAuthenticationSession(); + if (!checks.isActionRequest()) { + initLoginEvent(authSession); + event.event(EventType.CUSTOM_REQUIRED_ACTION); + return AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event); + } + + initLoginEvent(authSession); event.event(EventType.CUSTOM_REQUIRED_ACTION); event.detail(Details.CUSTOM_REQUIRED_ACTION, action); - Checks checks = new Checks(); - if (!checks.verifyRequiredAction(code, action)) { - return checks.response; - } - final ClientSessionCode clientCode = checks.clientCode; - final ClientSessionModel clientSession = clientCode.getClientSession(); - - final UserSessionModel userSession = clientSession.getUserSession(); RequiredActionFactory factory = (RequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, action); if (factory == null) { @@ -892,58 +849,46 @@ public class LoginActionsService { } RequiredActionProvider provider = factory.create(session); - initEvent(clientSession); - event.event(EventType.CUSTOM_REQUIRED_ACTION); - - - RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, userSession.getUser(), factory) { + RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) { @Override public void ignore() { throw new RuntimeException("Cannot call ignore within processAction()"); } }; + + Response response; provider.processAction(context); + + if (action != null) { + authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, action); + } + if (context.getStatus() == RequiredActionContext.Status.SUCCESS) { event.clone().success(); - initEvent(clientSession); + initLoginEvent(authSession); event.event(EventType.LOGIN); - clientSession.removeRequiredAction(factory.getId()); - userSession.getUser().removeRequiredAction(factory.getId()); - clientSession.removeNote(AuthenticationManager.CURRENT_REQUIRED_ACTION); + authSession.removeRequiredAction(factory.getId()); + authSession.getAuthenticatedUser().removeRequiredAction(factory.getId()); + authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); - if (AuthenticationManager.isActionRequired(session, userSession, clientSession, clientConnection, request, uriInfo, event)) { - // redirect to a generic code URI so that browser refresh will work - return redirectToRequiredActions(checks.clientCode.getCode()); - } else { - return AuthenticationManager.finishedRequiredActions(session, userSession, clientSession, clientConnection, request, uriInfo, event); - - } - } - if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { - return context.getChallenge(); - } - if (context.getStatus() == RequiredActionContext.Status.FAILURE) { - LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod()); + response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event); + } else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) { + response = context.getChallenge(); + } else if (context.getStatus() == RequiredActionContext.Status.FAILURE) { + LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol()); protocol.setRealm(context.getRealm()) .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) .setUriInfo(context.getUriInfo()) .setEventBuilder(event); event.detail(Details.CUSTOM_REQUIRED_ACTION, action); - Response response = protocol.sendError(context.getClientSession(), Error.CONSENT_DENIED); + response = protocol.sendError(authSession, Error.CONSENT_DENIED); event.error(Errors.REJECTED_BY_USER); - return response; - + } else { + throw new RuntimeException("Unreachable"); } - throw new RuntimeException("Unreachable"); - } - - public Response redirectToRequiredActions(String code) { - URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo) - .path(LoginActionsService.REQUIRED_ACTION) - .queryParam(OAuth2Constants.CODE, code).build(realm.getName()); - return Response.status(302).location(redirect).build(); + return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true); } } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java new file mode 100644 index 0000000000..87eaf20505 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java @@ -0,0 +1,315 @@ +/* + * 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.TokenVerifier.Predicate; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.authentication.actiontoken.DefaultActionToken; +import org.keycloak.authentication.ExplainedVerificationException; +import org.keycloak.authentication.actiontoken.ActionTokenContext; +import org.keycloak.authentication.actiontoken.ExplainedTokenVerificationException; +import org.keycloak.common.VerificationException; +import org.keycloak.events.Errors; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.*; +import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.representations.JsonWebToken; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.CommonClientSessionModel.Action; +import java.util.Objects; +import java.util.function.Consumer; +import org.jboss.logging.Logger; +/** + * + * @author hmlnarik + */ +public class LoginActionsServiceChecks { + + private static final Logger LOG = Logger.getLogger(LoginActionsServiceChecks.class.getName()); + + /** + * This check verifies that user ID (subject) from the token matches + * the one from the authentication session. + */ + public static class AuthenticationSessionUserIdMatchesOneFromToken implements Predicate { + + private final ActionTokenContext context; + + public AuthenticationSessionUserIdMatchesOneFromToken(ActionTokenContext context) { + this.context = context; + } + + @Override + public boolean test(JsonWebToken t) throws VerificationException { + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + + if (authSession == null || authSession.getAuthenticatedUser() == null + || ! Objects.equals(t.getSubject(), authSession.getAuthenticatedUser().getId())) { + throw new ExplainedTokenVerificationException(t, Errors.INVALID_TOKEN, Messages.INVALID_USER); + } + + return true; + } + } + + /** + * Verifies that if authentication session exists and any action is required according to it, then it is + * the expected one. + * + * If there is an action required in the session, furthermore it is not the expected one, and the required + * action is redirection to "required actions", it throws with response performing the redirect to required + * actions. + * @param + */ + public static class IsActionRequired implements Predicate { + + private final ActionTokenContext context; + + private final ClientSessionModel.Action expectedAction; + + public IsActionRequired(ActionTokenContext context, Action expectedAction) { + this.context = context; + this.expectedAction = expectedAction; + } + + @Override + public boolean test(JsonWebToken t) throws VerificationException { + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + + if (authSession != null && ! Objects.equals(authSession.getAction(), this.expectedAction.name())) { + if (Objects.equals(ClientSessionModel.Action.REQUIRED_ACTIONS.name(), authSession.getAction())) { + throw new LoginActionsServiceException( + AuthenticationManager.nextActionAfterAuthentication(context.getSession(), authSession, + context.getClientConnection(), context.getRequest(), context.getUriInfo(), context.getEvent())); + } + throw new ExplainedTokenVerificationException(t, Errors.INVALID_TOKEN, Messages.INVALID_CODE); + } + + return true; + } + } + + /** + * Verifies that the authentication session has not yet been converted to user session, in other words + * that the user has not yet completed authentication and logged in. + */ + public static void checkNotLoggedInYet(ActionTokenContext context, String authSessionId) throws VerificationException { + if (authSessionId == null) { + return; + } + + UserSessionModel userSession = context.getSession().sessions().getUserSession(context.getRealm(), authSessionId); + if (userSession != null) { + LoginFormsProvider loginForm = context.getSession().getProvider(LoginFormsProvider.class) + .setSuccess(Messages.ALREADY_LOGGED_IN); + + ClientModel client = null; + String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT); + if (lastClientUuid != null) { + client = context.getRealm().getClientById(lastClientUuid); + } + + if (client != null) { + context.getSession().getContext().setClient(client); + } else { + loginForm.setAttribute("skipLink", true); + } + + throw new LoginActionsServiceException(loginForm.createInfoPage()); + } + } + + /** + * Verifies whether the user given by ID both exists in the current realm. If yes, + * it optionally also injects the user using the given function (e.g. into session context). + */ + public static void checkIsUserValid(KeycloakSession session, RealmModel realm, String userId, Consumer userSetter) throws VerificationException { + UserModel user = userId == null ? null : session.users().getUserById(userId, realm); + + if (user == null) { + throw new ExplainedVerificationException(Errors.USER_NOT_FOUND, Messages.INVALID_USER); + } + + if (! user.isEnabled()) { + throw new ExplainedVerificationException(Errors.USER_DISABLED, Messages.INVALID_USER); + } + + if (userSetter != null) { + userSetter.accept(user); + } + } + + /** + * Verifies whether the user given by ID both exists in the current realm. If yes, + * it optionally also injects the user using the given function (e.g. into session context). + */ + public static void checkIsUserValid(T token, ActionTokenContext context) throws VerificationException { + try { + checkIsUserValid(context.getSession(), context.getRealm(), token.getUserId(), context.getAuthenticationSession()::setAuthenticatedUser); + } catch (ExplainedVerificationException ex) { + throw new ExplainedTokenVerificationException(token, ex); + } + } + + /** + * Verifies whether the client denoted by client ID in token's {@code iss} ({@code issuedFor}) + * field both exists and is enabled. + */ + public static void checkIsClientValid(KeycloakSession session, ClientModel client) throws VerificationException { + if (client == null) { + throw new ExplainedVerificationException(Errors.CLIENT_NOT_FOUND, Messages.UNKNOWN_LOGIN_REQUESTER); + } + + if (! client.isEnabled()) { + throw new ExplainedVerificationException(Errors.CLIENT_NOT_FOUND, Messages.LOGIN_REQUESTER_NOT_ENABLED); + } + } + + /** + * Verifies whether the client denoted by client ID in token's {@code iss} ({@code issuedFor}) + * field both exists and is enabled. + */ + public static void checkIsClientValid(T token, ActionTokenContext context) throws VerificationException { + String clientId = token.getIssuedFor(); + AuthenticationSessionModel authSession = context.getAuthenticationSession(); + ClientModel client = authSession == null ? null : authSession.getClient(); + + try { + checkIsClientValid(context.getSession(), client); + + if (clientId != null && ! Objects.equals(client.getClientId(), clientId)) { + throw new ExplainedTokenVerificationException(token, Errors.CLIENT_NOT_FOUND, Messages.UNKNOWN_LOGIN_REQUESTER); + } + } catch (ExplainedVerificationException ex) { + throw new ExplainedTokenVerificationException(token, ex); + } + } + + /** + * Verifies whether the given redirect URL, when set, is valid for the given client. + */ + public static class IsRedirectValid implements Predicate { + + private final ActionTokenContext context; + + private final String redirectUri; + + public IsRedirectValid(ActionTokenContext context, String redirectUri) { + this.context = context; + this.redirectUri = redirectUri; + } + + @Override + public boolean test(JsonWebToken t) throws VerificationException { + if (redirectUri == null) { + return true; + } + + ClientModel client = context.getAuthenticationSession().getClient(); + + if (RedirectUtils.verifyRedirectUri(context.getUriInfo(), redirectUri, context.getRealm(), client) == null) { + throw new ExplainedTokenVerificationException(t, Errors.INVALID_REDIRECT_URI, Messages.INVALID_REDIRECT_URI); + } + + return true; + } + } + + /** + * This check verifies that current authentication session is consistent with the one specified in token. + * Examples: + *
      + *
    • 1. Email from administrator with reset e-mail - token does not contain auth session ID
    • + *
    • 2. Email from "verify e-mail" step within flow - token contains auth session ID.
    • + *
    • 3. User clicked the link in an e-mail and gets to a new browser - authentication session cookie is not set
    • + *
    • 4. User clicked the link in an e-mail while having authentication running - authentication session cookie + * is already set in the browser
    • + *
    + * + *
      + *
    • For combinations 1 and 3, 1 and 4, and 2 and 3: Requests next step
    • + *
    • For combination 2 and 4: + *
        + *
      • If the auth session IDs from token and cookie match, pass
      • + *
      • Else if the auth session from cookie was forked and its parent auth session ID + * matches that of token, replaces current auth session with that of parent and passes
      • + *
      • Else requests restart by throwing RestartFlow exception
      • + *
      + *
    • + *
    + * + * When the check passes, it also sets the authentication session in token context accordingly. + * + * @param + */ + public static boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext context, String authSessionIdFromToken) throws VerificationException { + if (authSessionIdFromToken == null) { + return false; + } + + AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession()); + String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm()); + + if (authSessionIdFromCookie == null) { + return false; + } + + AuthenticationSessionModel authSessionFromCookie = context.getSession() + .authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie); + if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session + return false; + } + + if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) { + context.setAuthenticationSession(authSessionFromCookie, false); + return true; + } + + String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM); + if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) { + return false; + } + + AuthenticationSessionModel authSessionFromParent = context.getSession() + .authenticationSessions().getAuthenticationSession(context.getRealm(), parentSessionId); + + // It's the correct browser. Let's remove forked session as we won't continue + // from the login form (browser flow) but from the token's flow + // Don't expire KC_RESTART cookie at this point + asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false); + LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId()); + + // Refresh browser cookie + asm.setAuthSessionCookie(parentSessionId, context.getRealm()); + + context.setAuthenticationSession(authSessionFromParent, false); + context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION)); + + return true; + } + + public static void checkTokenWasNotUsedYet(T token, ActionTokenContext context) throws VerificationException { + ActionTokenStoreProvider actionTokenStore = context.getSession().getProvider(ActionTokenStoreProvider.class); + if (actionTokenStore.get(token) != null) { + throw new ExplainedTokenVerificationException(token, Errors.EXPIRED_CODE, Messages.EXPIRED_ACTION); + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java new file mode 100644 index 0000000000..3e758dfdf2 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceException.java @@ -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; + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java new file mode 100644 index 0000000000..978ad6f26e --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -0,0 +1,388 @@ +/* + * Copyright 2016 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 java.net.URI; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.common.ClientConnection; +import org.keycloak.common.util.ObjectUtil; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.protocol.RestartLoginCookie; +import org.keycloak.services.ErrorPage; +import org.keycloak.services.ServicesLogger; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.util.BrowserHistoryHelper; +import org.keycloak.services.util.AuthenticationFlowURLHelper; +import org.keycloak.sessions.AuthenticationSessionModel; + + +public class SessionCodeChecks { + + private static final Logger logger = Logger.getLogger(SessionCodeChecks.class); + + private AuthenticationSessionModel authSession; + private ClientSessionCode clientCode; + private Response response; + private boolean actionRequest; + + private final RealmModel realm; + private final UriInfo uriInfo; + private final ClientConnection clientConnection; + private final KeycloakSession session; + private final EventBuilder event; + + private final String code; + private final String execution; + private final String flowPath; + + + public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String flowPath) { + this.realm = realm; + this.uriInfo = uriInfo; + this.clientConnection = clientConnection; + this.session = session; + this.event = event; + + this.code = code; + this.execution = execution; + this.flowPath = flowPath; + } + + + public AuthenticationSessionModel getAuthenticationSession() { + return authSession; + } + + + private boolean failed() { + return response != null; + } + + + public Response getResponse() { + return response; + } + + + public ClientSessionCode getClientCode() { + return clientCode; + } + + public boolean isActionRequest() { + return actionRequest; + } + + + private boolean checkSsl() { + if (uriInfo.getBaseUri().getScheme().equals("https")) { + return true; + } else { + return !realm.getSslRequired().isRequired(clientConnection); + } + } + + + public AuthenticationSessionModel initialVerifyAuthSession() { + // Basic realm checks + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + response = ErrorPage.error(session, Messages.HTTPS_REQUIRED); + return null; + } + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED); + return null; + } + + // object retrieve + AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class); + if (authSession != null) { + return authSession; + } + + // See if we are already authenticated and userSession with same ID exists. + String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm); + if (sessionId != null) { + UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); + if (userSession != null) { + + LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class) + .setSuccess(Messages.ALREADY_LOGGED_IN); + + ClientModel client = null; + String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT); + if (lastClientUuid != null) { + client = realm.getClientById(lastClientUuid); + } + + if (client != null) { + session.getContext().setClient(client); + } else { + loginForm.setAttribute("skipLink", true); + } + + response = loginForm.createInfoPage(); + return null; + } + } + + // Otherwise just try to restart from the cookie + response = restartAuthenticationSessionFromCookie(); + return null; + } + + + public boolean initialVerify() { + // Basic realm checks and authenticationSession retrieve + authSession = initialVerifyAuthSession(); + if (authSession == null) { + return false; + } + + // Check cached response from previous action request + response = BrowserHistoryHelper.getInstance().loadSavedResponse(session, authSession); + if (response != null) { + return false; + } + + // Client checks + event.detail(Details.CODE_ID, authSession.getId()); + ClientModel client = authSession.getClient(); + if (client == null) { + event.error(Errors.CLIENT_NOT_FOUND); + response = ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER); + clientCode.removeExpiredClientSession(); + return false; + } + + event.client(client); + session.getContext().setClient(client); + + if (!client.isEnabled()) { + event.error(Errors.CLIENT_DISABLED); + response = ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED); + clientCode.removeExpiredClientSession(); + return false; + } + + + // Check if it's action or not + if (code == null) { + String lastExecFromSession = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); + + // Check if we transitted between flows (eg. clicking "register" on login screen) + if (execution==null && !flowPath.equals(lastFlow)) { + logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", flowPath, lastFlow); + + // Don't allow moving to different flow if I am on requiredActions already + if (ClientSessionModel.Action.AUTHENTICATE.name().equals(authSession.getAction())) { + authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath); + authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + lastExecFromSession = null; + } + } + + if (ObjectUtil.isEqualOrBothNull(execution, lastExecFromSession)) { + // Allow refresh of previous page + clientCode = new ClientSessionCode<>(session, realm, authSession); + actionRequest = false; + return true; + } else { + response = showPageExpired(authSession); + return false; + } + } else { + ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, session, realm, AuthenticationSessionModel.class); + clientCode = result.getCode(); + if (clientCode == null) { + + // In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page + if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) { + String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); + URI redirectUri = getLastExecutionUrl(latestFlowPath, execution); + + logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri); + authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION); + response = Response.status(Response.Status.FOUND).location(redirectUri).build(); + } else { + response = showPageExpired(authSession); + } + return false; + } + + + actionRequest = true; + if (execution != null) { + authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, execution); + } + return true; + } + } + + + public boolean verifyActiveAndValidAction(String expectedAction, ClientSessionCode.ActionType actionType) { + if (failed()) { + return false; + } + + if (!isActionActive(actionType)) { + return false; + } + + if (!clientCode.isValidAction(expectedAction)) { + AuthenticationSessionModel authSession = getAuthenticationSession(); + if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.getAction())) { + logger.debugf("Incorrect action '%s' . User authenticated already.", authSession.getAction()); + response = showPageExpired(authSession); + return false; + } else { + logger.errorf("Bad action. Expected action '%s', current action '%s'", expectedAction, authSession.getAction()); + response = ErrorPage.error(session, Messages.EXPIRED_CODE); + return false; + } + } + + return true; + } + + + private boolean isActionActive(ClientSessionCode.ActionType actionType) { + if (!clientCode.isActionActive(actionType)) { + event.clone().error(Errors.EXPIRED_CODE); + + AuthenticationProcessor.resetFlow(authSession, LoginActionsService.AUTHENTICATE_PATH); + + authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT); + + URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null); + logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri); + response = Response.status(Response.Status.FOUND).location(redirectUri).build(); + return false; + } + return true; + } + + + public boolean verifyRequiredAction(String executedAction) { + if (failed()) { + return false; + } + + if (!clientCode.isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) { + logger.debugf("Expected required action, but session action is '%s' . Showing expired page now.", authSession.getAction()); + event.error(Errors.INVALID_CODE); + + response = showPageExpired(authSession); + + return false; + } + + if (!isActionActive(ClientSessionCode.ActionType.USER)) { + return false; + } + + if (actionRequest) { + String currentRequiredAction = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + if (executedAction == null || !executedAction.equals(currentRequiredAction)) { + logger.debug("required action doesn't match current required action"); + response = redirectToRequiredActions(currentRequiredAction); + return false; + } + } + return true; + } + + + private Response restartAuthenticationSessionFromCookie() { + logger.debug("Authentication session not found. Trying to restart from cookie."); + AuthenticationSessionModel authSession = null; + try { + authSession = RestartLoginCookie.restartSession(session, realm); + } catch (Exception e) { + ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e); + } + + if (authSession != null) { + + event.clone(); + event.detail(Details.RESTART_AFTER_TIMEOUT, "true"); + event.error(Errors.EXPIRED_CODE); + + String warningMessage = Messages.LOGIN_TIMEOUT; + authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, warningMessage); + + String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW); + if (flowPath == null) { + flowPath = LoginActionsService.AUTHENTICATE_PATH; + } + + URI redirectUri = getLastExecutionUrl(flowPath, null); + logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri); + return Response.status(Response.Status.FOUND).location(redirectUri).build(); + } else { + // Finally need to show error as all the fallbacks failed + event.error(Errors.INVALID_CODE); + return ErrorPage.error(session, Messages.INVALID_CODE); + } + } + + + private Response redirectToRequiredActions(String action) { + UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo) + .path(LoginActionsService.REQUIRED_ACTION); + + if (action != null) { + uriBuilder.queryParam("execution", action); + } + URI redirect = uriBuilder.build(realm.getName()); + return Response.status(302).location(redirect).build(); + } + + + private URI getLastExecutionUrl(String flowPath, String executionId) { + return new AuthenticationFlowURLHelper(session, realm, uriInfo) + .getLastExecutionUrl(flowPath, executionId); + } + + + private Response showPageExpired(AuthenticationSessionModel authSession) { + return new AuthenticationFlowURLHelper(session, realm, uriInfo) + .showPageExpired(authSession); + } + +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 89b0a3317c..a92729e2b4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -25,8 +25,8 @@ import org.keycloak.common.Profile; import org.keycloak.common.util.Time; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -492,8 +492,11 @@ public class ClientResource { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession); // Update lastSessionRefresh with the timestamp from clientSession - for (ClientSessionModel clientSession : userSession.getClientSessions()) { - if (client.getId().equals(clientSession.getClient().getId())) { + for (Map.Entry csEntry : userSession.getAuthenticatedClientSessions().entrySet()) { + String clientUuid = csEntry.getKey(); + AuthenticatedClientSessionModel clientSession = csEntry.getValue(); + + if (client.getId().equals(clientUuid)) { rep.setLastAccess(Time.toMillis(clientSession.getTimestamp())); break; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 3259982457..815ee8981b 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken; import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; import org.keycloak.common.util.Time; @@ -33,24 +34,9 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; -import org.keycloak.models.Constants; -import org.keycloak.models.FederatedIdentityModel; -import org.keycloak.models.GroupModel; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.ModelException; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserConsentModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserLoginFailureModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.UserSessionModel; -import org.keycloak.models.credential.PasswordUserCredentialModel; +import org.keycloak.models.*; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.ModelToRepresentation; -import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.provider.ProviderFactory; @@ -62,14 +48,12 @@ import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserSessionRepresentation; import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponseException; -import org.keycloak.services.ServicesLogger; -import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; -import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.models.UserManager; -import org.keycloak.services.managers.UserSessionManager; +import org.keycloak.services.*; +import org.keycloak.services.managers.*; import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.validation.Validation; import org.keycloak.storage.ReadOnlyException; import org.keycloak.utils.ProfileHelper; @@ -83,26 +67,18 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.net.URI; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; +import javax.ws.rs.*; +import javax.ws.rs.core.*; /** * Base resource for managing users @@ -344,7 +320,8 @@ public class UsersResource { } EventBuilder event = new EventBuilder(realm, session, clientConnection); - UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null); + String sessionId = KeycloakModelUtils.generateId(); + UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null); AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection); URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName()); Map result = new HashMap<>(); @@ -396,7 +373,7 @@ public class UsersResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public List getSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) { + public List getOfflineSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) { auth.requireView(); UserModel user = session.users().getUserById(id, realm); @@ -407,19 +384,21 @@ public class UsersResource { if (client == null) { throw new NotFoundException("Client not found"); } - List sessions = new UserSessionManager(session).findOfflineSessions(realm, client, user); + List sessions = new UserSessionManager(session).findOfflineSessions(realm, user); List reps = new ArrayList(); for (UserSessionModel session : sessions) { UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session); // Update lastSessionRefresh with the timestamp from clientSession - for (ClientSessionModel clientSession : session.getClientSessions()) { - if (clientId.equals(clientSession.getClient().getId())) { - rep.setLastAccess(Time.toMillis(clientSession.getTimestamp())); - break; - } + AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessions().get(clientId); + + // Skip if userSession is not for this client + if (clientSession == null) { + continue; } + rep.setLastAccess(clientSession.getTimestamp()); + reps.add(rep); } return reps; @@ -837,7 +816,7 @@ public class UsersResource { @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { List actions = new LinkedList<>(); actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); - return executeActionsEmail(id, redirectUri, clientId, actions); + return executeActionsEmail(id, redirectUri, clientId, null, actions); } @@ -852,6 +831,7 @@ public class UsersResource { * @param id User is * @param redirectUri Redirect uri * @param clientId Client id + * @param lifespan Number of seconds after which the generated token expires * @param actions required actions the user needs to complete * @return */ @@ -861,6 +841,7 @@ public class UsersResource { public Response executeActionsEmail(@PathParam("id") String id, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId, + @QueryParam("lifespan") Integer lifespan, List actions) { auth.requireManage(); @@ -873,25 +854,51 @@ public class UsersResource { return ErrorResponse.error("User email missing", Response.Status.BAD_REQUEST); } - ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId); - for (String action : actions) { - clientSession.addRequiredAction(action); + if (!user.isEnabled()) { + throw new WebApplicationException( + ErrorResponse.error("User is disabled", Response.Status.BAD_REQUEST)); } - if (redirectUri != null) { - clientSession.setNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true"); + if (redirectUri != null && clientId == null) { + throw new WebApplicationException( + ErrorResponse.error("Client id missing", Response.Status.BAD_REQUEST)); } - ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession); - accessCode.setAction(ClientSessionModel.Action.EXECUTE_ACTIONS.name()); + + if (clientId == null) { + clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; + } + + ClientModel client = realm.getClientByClientId(clientId); + if (client == null || !client.isEnabled()) { + throw new WebApplicationException( + ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST)); + } + + String redirect; + if (redirectUri != null) { + redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); + if (redirect == null) { + throw new WebApplicationException( + ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST)); + } + } + + if (lifespan == null) { + lifespan = realm.getActionTokenGeneratedByAdminLifespan(); + } + int expiration = Time.currentTime() + lifespan; + ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, actions, redirectUri, clientId); try { - UriBuilder builder = Urls.executeActionsBuilder(uriInfo.getBaseUri()); - builder.queryParam("key", accessCode.getCode()); + UriBuilder builder = LoginActionsService.actionTokenProcessor(uriInfo); + builder.queryParam("key", token.serialize(session, realm, uriInfo)); String link = builder.build(realm.getName()).toString(); - long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction()); - this.session.getProvider(EmailTemplateProvider.class).setRealm(realm).setUser(user).sendExecuteActions(link, expiration); + this.session.getProvider(EmailTemplateProvider.class) + .setRealm(realm) + .setUser(user) + .sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan)); //audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success(); @@ -922,49 +929,7 @@ public class UsersResource { public Response sendVerifyEmail(@PathParam("id") String id, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) { List actions = new LinkedList<>(); actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name()); - return executeActionsEmail(id, redirectUri, clientId, actions); - } - - private ClientSessionModel createClientSession(UserModel user, String redirectUri, String clientId) { - - if (!user.isEnabled()) { - throw new WebApplicationException( - ErrorResponse.error("User is disabled", Response.Status.BAD_REQUEST)); - } - - if (redirectUri != null && clientId == null) { - throw new WebApplicationException( - ErrorResponse.error("Client id missing", Response.Status.BAD_REQUEST)); - } - - if (clientId == null) { - clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID; - } - - ClientModel client = realm.getClientByClientId(clientId); - if (client == null || !client.isEnabled()) { - throw new WebApplicationException( - ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST)); - } - - String redirect = null; - if (redirectUri != null) { - redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client); - if (redirect == null) { - throw new WebApplicationException( - ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST)); - } - } - - - UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "form", false, null, null); - //audit.session(userSession); - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); - clientSession.setRedirectUri(redirect); - clientSession.setUserSession(userSession); - - return clientSession; + return executeActionsEmail(id, redirectUri, clientId, null, actions); } @GET diff --git a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java index 5935eb08cb..5315be4add 100755 --- a/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java +++ b/services/src/main/java/org/keycloak/services/scheduled/ClearExpiredUserSessions.java @@ -32,6 +32,7 @@ public class ClearExpiredUserSessions implements ScheduledTask { UserSessionProvider sessions = session.sessions(); for (RealmModel realm : session.realms().getRealms()) { sessions.removeExpired(realm); + session.authenticationSessions().removeExpired(realm); } } diff --git a/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java new file mode 100644 index 0000000000..b97963e009 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/util/AuthenticationFlowURLHelper.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016 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.util; + +import java.net.URI; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.AuthorizationEndpointBase; +import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.sessions.AuthenticationSessionModel; + +/** + * @author Marek Posolda + */ +public class AuthenticationFlowURLHelper { + + protected static final Logger logger = Logger.getLogger(AuthenticationFlowURLHelper.class); + + private final KeycloakSession session; + private final RealmModel realm; + private final UriInfo uriInfo; + + public AuthenticationFlowURLHelper(KeycloakSession session, RealmModel realm, UriInfo uriInfo) { + this.session = session; + this.realm = realm; + this.uriInfo = uriInfo; + } + + + public Response showPageExpired(AuthenticationSessionModel authSession) { + URI lastStepUrl = getLastExecutionUrl(authSession); + + logger.debugf("Redirecting to 'page expired' now. Will use last step URL: %s", lastStepUrl); + + return session.getProvider(LoginFormsProvider.class) + .setActionUri(lastStepUrl) + .createLoginExpiredPage(); + } + + + public URI getLastExecutionUrl(String flowPath, String executionId) { + UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo) + .path(flowPath); + + if (executionId != null) { + uriBuilder.queryParam("execution", executionId); + } + return uriBuilder.build(realm.getName()); + } + + + public URI getLastExecutionUrl(AuthenticationSessionModel authSession) { + String executionId = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION); + String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH); + + if (latestFlowPath == null) { + latestFlowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW); + } + + if (latestFlowPath == null) { + latestFlowPath = LoginActionsService.AUTHENTICATE_PATH; + } + + return getLastExecutionUrl(latestFlowPath, executionId); + } + +} diff --git a/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java new file mode 100644 index 0000000000..ef34b16116 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/util/BrowserHistoryHelper.java @@ -0,0 +1,193 @@ +/* + * Copyright 2016 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.util; + +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.theme.BrowserSecurityHeaderSetup; +import org.keycloak.utils.MediaType; + +/** + * The point of this is to improve experience of browser history (back/forward/refresh buttons), but ensure there is no more redirects then necessary. + * + * Ideally we want to: + * - Remove all POST requests from browser history, because browsers don't automatically re-send them when click "back" button. POSTS in history causes unfriendly dialogs and browser "Page is expired" pages. + * + * - Keep the browser URL to match the flow and execution from authentication session. This means that browser refresh works fine and show us the correct form. + * + * - Avoid redirects. This is possible with javascript based approach (JavascriptHistoryReplace). The RedirectAfterPostHelper requires one redirect after POST, but works even on browser without javascript and + * on old browsers where "history.replaceState" is unsupported. + * + * @author Marek Posolda + */ +public abstract class BrowserHistoryHelper { + + protected static final Logger logger = Logger.getLogger(BrowserHistoryHelper.class); + + public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest); + + public abstract Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession); + + + // Always rely on javascript for now + public static BrowserHistoryHelper getInstance() { + return new JavascriptHistoryReplace(); + //return new RedirectAfterPostHelper(); + //return new NoOpHelper(); + } + + + // IMPL + + private static class JavascriptHistoryReplace extends BrowserHistoryHelper { + + private static final Pattern HEAD_END_PATTERN = Pattern.compile(""); + + @Override + public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) { + if (!actionRequest) { + return response; + } + + // For now, handle just status 200 with String body. See if more is needed... + Object entity = response.getEntity(); + if (entity != null && entity instanceof String) { + String responseString = (String) entity; + + URI lastExecutionURL = new AuthenticationFlowURLHelper(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession); + + // Inject javascript for history "replaceState" + String responseWithJavascript = responseWithJavascript(responseString, lastExecutionURL.toString()); + + return Response.fromResponse(response).entity(responseWithJavascript).build(); + } + + + return response; + } + + @Override + public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) { + return null; + } + + + private String responseWithJavascript(String origHtml, String lastExecutionUrl) { + Matcher m = HEAD_END_PATTERN.matcher(origHtml); + + if (m.find()) { + int start = m.start(); + + String javascript = getJavascriptText(lastExecutionUrl); + + return new StringBuilder(origHtml.substring(0, start)) + .append(javascript ) + .append(origHtml.substring(start)) + .toString(); + } else { + return origHtml; + } + } + + private String getJavascriptText(String lastExecutionUrl) { + return new StringBuilder("") + .toString(); + } + + } + + + // This impl is limited ATM. Saved request doesn't save response HTTP headers, so they may not be fully restored.. + private static class RedirectAfterPostHelper extends BrowserHistoryHelper { + + private static final String CACHED_RESPONSE = "cached.response"; + + @Override + public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) { + if (!actionRequest) { + return response; + } + + // For now, handle just status 200 with String body. See if more is needed... + if (response.getStatus() == 200) { + Object entity = response.getEntity(); + if (entity instanceof String) { + String responseString = (String) entity; + authSession.setAuthNote(CACHED_RESPONSE, responseString); + + URI lastExecutionURL = new AuthenticationFlowURLHelper(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession); + + if (logger.isTraceEnabled()) { + logger.tracef("Saved response challenge and redirect to %s", lastExecutionURL); + } + + return Response.status(302).location(lastExecutionURL).build(); + } + } + + return response; + } + + + @Override + public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) { + String savedResponse = authSession.getAuthNote(CACHED_RESPONSE); + if (savedResponse != null) { + authSession.removeAuthNote(CACHED_RESPONSE); + + if (logger.isTraceEnabled()) { + logger.tracef("Restored previously saved request"); + } + + Response.ResponseBuilder builder = Response.status(200).type(MediaType.TEXT_HTML_UTF_8).entity(savedResponse); + BrowserSecurityHeaderSetup.headers(builder, session.getContext().getRealm()); // TODO rather all the headers from the saved response should be added here. + return builder.build(); + } + + return null; + } + + } + + + private static class NoOpHelper extends BrowserHistoryHelper { + + @Override + public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) { + return response; + } + + + @Override + public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) { + return null; + } + + } +} diff --git a/services/src/main/java/org/keycloak/services/util/CookieHelper.java b/services/src/main/java/org/keycloak/services/util/CookieHelper.java index b8d57f978f..2005695b54 100755 --- a/services/src/main/java/org/keycloak/services/util/CookieHelper.java +++ b/services/src/main/java/org/keycloak/services/util/CookieHelper.java @@ -17,11 +17,19 @@ package org.keycloak.services.util; +import java.net.URI; + import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.common.util.ServerCookie; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.AuthenticationManager; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; /** * @author Bill Burke @@ -50,4 +58,10 @@ public class CookieHelper { } + public static String getCookieValue(String name) { + HttpHeaders headers = ResteasyProviderFactory.getContextData(HttpHeaders.class); + Cookie cookie = headers.getCookies().get(name); + return cookie != null ? cookie.getValue() : null; + } + } diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index c6b340fb1b..00be27c8b4 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -26,14 +26,13 @@ import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.common.ClientConnection; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; +import org.keycloak.sessions.AuthenticationSessionModel; import twitter4j.Twitter; import twitter4j.TwitterFactory; import twitter4j.auth.AccessToken; @@ -41,6 +40,7 @@ import twitter4j.auth.RequestToken; import javax.ws.rs.GET; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -48,8 +48,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.net.URI; -import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE; - /** * @author Stian Thorgersen */ @@ -57,6 +55,10 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider { protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class); + + private static final String TWITTER_TOKEN = "twitter_token"; + private static final String TWITTER_TOKENSECRET = "twitter_tokenSecret"; + public TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) { super(session, config); } @@ -75,10 +77,10 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider + + + + + diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java index 27de40e7e7..d03c0b004b 100755 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/forms/PassThroughRegistration.java @@ -52,15 +52,15 @@ public class PassThroughRegistration implements Authenticator, AuthenticatorFact user.setEnabled(true); user.setEmail(email); - context.getClientSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); + context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); context.setUser(user); context.getEvent().user(user); context.getEvent().success(); context.newEvent().event(EventType.LOGIN); - context.getEvent().client(context.getClientSession().getClient().getClientId()) - .detail(Details.REDIRECT_URI, context.getClientSession().getRedirectUri()) - .detail(Details.AUTH_METHOD, context.getClientSession().getAuthMethod()); - String authType = context.getClientSession().getNote(Details.AUTH_TYPE); + context.getEvent().client(context.getAuthenticationSession().getClient().getClientId()) + .detail(Details.REDIRECT_URI, context.getAuthenticationSession().getRedirectUri()) + .detail(Details.AUTH_METHOD, context.getAuthenticationSession().getProtocol()); + String authType = context.getAuthenticationSession().getAuthNote(Details.AUTH_TYPE); if (authType != null) { context.getEvent().detail(Details.AUTH_TYPE, authType); } diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java index a4ae8c2121..b868827f80 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/rest/TestingResourceProvider.java @@ -163,6 +163,7 @@ public class TestingResourceProvider implements RealmResourceProvider { } session.sessions().removeExpired(realm); + session.authenticationSessions().removeExpired(realm); return Response.ok().build(); } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java index c89467466d..83bb19b828 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertow.java @@ -41,8 +41,11 @@ import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.shrinkwrap.descriptor.api.Descriptor; import org.jboss.shrinkwrap.undertow.api.UndertowWebArchive; import org.keycloak.common.util.reflections.Reflections; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.services.filters.KeycloakSessionServletFilter; +import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.resources.KeycloakApplication; import javax.servlet.DispatcherType; @@ -106,6 +109,11 @@ public class KeycloakOnUndertow implements DeployableContainer archive) throws DeploymentException { + if (isRemoteMode()) { + log.infof("Skipped deployment of '%s' as we are in remote mode!", archive.getName()); + return new ProtocolMetaData(); + } + DeploymentInfo di = getDeplotymentInfoFromArchive(archive); ClassLoader parentCl = Thread.currentThread().getContextClassLoader(); @@ -152,7 +160,7 @@ public class KeycloakOnUndertow implements DeployableContainer archive) throws DeploymentException { + if (isRemoteMode()) { + log.infof("Skipped undeployment of '%s' as we are in remote mode!", archive.getName()); + return; + } + Field containerField = Reflections.findDeclaredField(UndertowJaxrsServer.class, "container"); Reflections.setAccessible(containerField); ServletContainer container = (ServletContainer) Reflections.getFieldValue(containerField, undertow); diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java index 14aca1ccab..79666047a4 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowArquillianExtension.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.arquillian.undertow; import org.jboss.arquillian.container.spi.client.container.DeployableContainer; import org.jboss.arquillian.core.spi.LoadableExtension; +import org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer; /** * @@ -12,6 +13,7 @@ public class KeycloakOnUndertowArquillianExtension implements LoadableExtension @Override public void register(ExtensionBuilder builder) { builder.service(DeployableContainer.class, KeycloakOnUndertow.class); + builder.service(DeployableContainer.class, SimpleUndertowLoadBalancerContainer.class); } } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java index 0a519efd75..bdf0ff79ec 100644 --- a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/KeycloakOnUndertowConfiguration.java @@ -19,11 +19,18 @@ package org.keycloak.testsuite.arquillian.undertow; import org.arquillian.undertow.UndertowContainerConfiguration; import org.jboss.arquillian.container.spi.ConfigurationException; +import org.jboss.logging.Logger; public class KeycloakOnUndertowConfiguration extends UndertowContainerConfiguration { + protected static final Logger log = Logger.getLogger(KeycloakOnUndertowConfiguration.class); + private int workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8; private String resourcesHome; + private boolean remoteMode; + private String route; + + private int bindHttpPortOffset = 0; public int getWorkerThreads() { return workerThreads; @@ -41,10 +48,39 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat this.resourcesHome = resourcesHome; } + public int getBindHttpPortOffset() { + return bindHttpPortOffset; + } + + public void setBindHttpPortOffset(int bindHttpPortOffset) { + this.bindHttpPortOffset = bindHttpPortOffset; + } + + public String getRoute() { + return route; + } + + public void setRoute(String route) { + this.route = route; + } + + public boolean isRemoteMode() { + return remoteMode; + } + + public void setRemoteMode(boolean remoteMode) { + this.remoteMode = remoteMode; + } + @Override public void validate() throws ConfigurationException { super.validate(); - + + int basePort = getBindHttpPort(); + int newPort = basePort + bindHttpPortOffset; + setBindHttpPort(newPort); + log.info("KeycloakOnUndertow will listen on port: " + newPort); + // TODO validate workerThreads } diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java new file mode 100644 index 0000000000..86a7e2ed10 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/SetSystemProperty.java @@ -0,0 +1,53 @@ +/* + * Copyright 2016 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.testsuite.arquillian.undertow; + +/** + * @author Marek Posolda + */ +class SetSystemProperty { + + private String name; + private String oldValue; + + public SetSystemProperty(String name, String value) { + this.name = name; + this.oldValue = System.getProperty(name); + + if (value == null) { + if (oldValue != null) { + System.getProperties().remove(name); + } + } else { + System.setProperty(name, value); + } + } + + public void revert() { + String value = System.getProperty(name); + + if (oldValue == null) { + if (value != null) { + System.getProperties().remove(name); + } + } else { + System.setProperty(name, oldValue); + } + } + +} diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java new file mode 100644 index 0000000000..3eda20c8db --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancer.java @@ -0,0 +1,298 @@ +/* + * Copyright 2016 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.testsuite.arquillian.undertow.lb; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import io.undertow.Undertow; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.server.handlers.proxy.ExclusivityChecker; +import io.undertow.server.handlers.proxy.LoadBalancingProxyClient; +import io.undertow.server.handlers.proxy.ProxyCallback; +import io.undertow.server.handlers.proxy.ProxyClient; +import io.undertow.server.handlers.proxy.ProxyConnection; +import io.undertow.server.handlers.proxy.ProxyHandler; +import io.undertow.util.AttachmentKey; +import io.undertow.util.Headers; +import org.jboss.logging.Logger; +import org.keycloak.services.managers.AuthenticationSessionManager; + +/** + * Loadbalancer on embedded undertow. Supports sticky session over "AUTH_SESSION_ID" cookie and failover to different node when sticky node not available. + * Status 503 is returned just if all backend nodes are unavailable. + * + * To configure backend nodes, you can use system property like : -Dkeycloak.nodes="node1=http://localhost:8181,node2=http://localhost:8182" + * + * @author Marek Posolda + */ +public class SimpleUndertowLoadBalancer { + + private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancer.class); + + static final String DEFAULT_NODES = "node1=http://localhost:8181,node2=http://localhost:8182"; + + private final String host; + private final int port; + private final String nodesString; + private Undertow undertow; + + + public static void main(String[] args) throws Exception { + String nodes = System.getProperty("keycloak.nodes", DEFAULT_NODES); + + SimpleUndertowLoadBalancer lb = new SimpleUndertowLoadBalancer("localhost", 8180, nodes); + lb.start(); + + Runtime.getRuntime().addShutdownHook(new Thread() { + + @Override + public void run() { + lb.stop(); + } + + }); + } + + + public SimpleUndertowLoadBalancer(String host, int port, String nodesString) { + this.host = host; + this.port = port; + this.nodesString = nodesString; + log.infof("Keycloak nodes: %s", nodesString); + } + + + public void start() { + Map nodes = parseNodes(nodesString); + try { + HttpHandler proxyHandler = createHandler(nodes); + + undertow = Undertow.builder() + .addHttpListener(port, host) + .setHandler(proxyHandler) + .build(); + undertow.start(); + + log.infof("Loadbalancer started and ready to serve requests on http://%s:%d", host, port); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + public void stop() { + undertow.stop(); + } + + + static Map parseNodes(String nodes) { + String[] nodesArray = nodes.split(","); + Map result = new HashMap<>(); + + for (String nodeStr : nodesArray) { + String[] node = nodeStr.trim().split("="); + if (node.length != 2) { + throw new IllegalArgumentException("Illegal node format in the configuration: " + nodeStr); + } + result.put(node[0].trim(), node[1].trim()); + } + + return result; + } + + + private HttpHandler createHandler(Map backendNodes) throws Exception { + + // TODO: configurable options if needed + String sessionCookieNames = AuthenticationSessionManager.AUTH_SESSION_ID; + int connectionsPerThread = 20; + int problemServerRetry = 5; // In case of unavailable node, we will try to ping him every 5 seconds to check if it's back + int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers + int requestQueueSize = 10; + int cachedConnectionsPerThread = 10; + int connectionIdleTimeout = 60; + int maxRetryAttempts = backendNodes.size() - 1; + + final LoadBalancingProxyClient lb = new CustomLoadBalancingClient(new ExclusivityChecker() { + + @Override + public boolean isExclusivityRequired(HttpServerExchange exchange) { + //we always create a new connection for upgrade requests + return exchange.getRequestHeaders().contains(Headers.UPGRADE); + } + + }, maxRetryAttempts) + .setConnectionsPerThread(connectionsPerThread) + .setMaxQueueSize(requestQueueSize) + .setSoftMaxConnectionsPerThread(cachedConnectionsPerThread) + .setTtl(connectionIdleTimeout) + .setProblemServerRetry(problemServerRetry); + String[] sessionIds = sessionCookieNames.split(","); + for (String id : sessionIds) { + lb.addSessionCookieName(id); + } + + for (Map.Entry node : backendNodes.entrySet()) { + String route = node.getKey(); + URI uri = new URI(node.getValue()); + + lb.addHost(uri, route); + log.infof("Added host: %s, route: %s", uri.toString(), route); + } + + ProxyHandler handler = new ProxyHandler(lb, maxTime, ResponseCodeHandler.HANDLE_404); + return handler; + } + + + private class CustomLoadBalancingClient extends LoadBalancingProxyClient { + + private final int maxRetryAttempts; + + public CustomLoadBalancingClient(ExclusivityChecker checker, int maxRetryAttempts) { + super(checker); + this.maxRetryAttempts = maxRetryAttempts; + } + + + @Override + protected Host selectHost(HttpServerExchange exchange) { + Host host = super.selectHost(exchange); + log.debugf("Selected host: %s, host available: %b", host.getUri().toString(), host.isAvailable()); + exchange.putAttachment(SELECTED_HOST, host); + return host; + } + + + @Override + protected Host findStickyHost(HttpServerExchange exchange) { + Host stickyHost = super.findStickyHost(exchange); + + if (stickyHost != null) { + + if (!stickyHost.isAvailable()) { + log.infof("Sticky host %s not available. Trying different hosts", stickyHost.getUri()); + return null; + } else { + log.infof("Sticky host %s found and looks available", stickyHost.getUri()); + } + } + + return stickyHost; + } + + + @Override + public void getConnection(ProxyTarget target, HttpServerExchange exchange, ProxyCallback callback, long timeout, TimeUnit timeUnit) { + long timeoutMs = timeUnit.toMillis(timeout); + + ProxyCallbackDelegate callbackDelegate = new ProxyCallbackDelegate(this, callback, timeoutMs, maxRetryAttempts); + super.getConnection(target, exchange, callbackDelegate, timeout, timeUnit); + } + + } + + + private static final AttachmentKey SELECTED_HOST = AttachmentKey.create(LoadBalancingProxyClient.Host.class); + private static final AttachmentKey REMAINING_RETRY_ATTEMPTS = AttachmentKey.create(Integer.class); + + + private class ProxyCallbackDelegate implements ProxyCallback { + + private final ProxyClient proxyClient; + private final ProxyCallback delegate; + private final long timeoutMs; + private final int maxRetryAttempts; + + + public ProxyCallbackDelegate(ProxyClient proxyClient, ProxyCallback delegate, long timeoutMs, int maxRetryAttempts) { + this.proxyClient = proxyClient; + this.delegate = delegate; + this.timeoutMs = timeoutMs; + this.maxRetryAttempts = maxRetryAttempts; + } + + + @Override + public void completed(HttpServerExchange exchange, ProxyConnection result) { + LoadBalancingProxyClient.Host host = exchange.getAttachment(SELECTED_HOST); + if (host == null) { + // shouldn't happen + log.error("Host is null!!!"); + } else { + // Host was restored + if (!host.isAvailable()) { + log.infof("Host %s available again", host.getUri()); + host.clearError(); + } + } + + delegate.completed(exchange, result); + } + + + @Override + public void failed(HttpServerExchange exchange) { + final long time = System.currentTimeMillis(); + + Integer remainingAttempts = exchange.getAttachment(REMAINING_RETRY_ATTEMPTS); + if (remainingAttempts == null) { + remainingAttempts = maxRetryAttempts; + } else { + remainingAttempts--; + } + + exchange.putAttachment(REMAINING_RETRY_ATTEMPTS, remainingAttempts); + + log.infof("Failed request to selected host. Remaining attempts: %d", remainingAttempts); + if (remainingAttempts > 0) { + if (timeoutMs > 0 && time > timeoutMs) { + delegate.failed(exchange); + } else { + ProxyClient.ProxyTarget target = proxyClient.findTarget(exchange); + if (target != null) { + final long remaining = timeoutMs > 0 ? timeoutMs - time : -1; + proxyClient.getConnection(target, exchange, this, remaining, TimeUnit.MILLISECONDS); + } else { + couldNotResolveBackend(exchange); // The context was registered when we started, so return 503 + } + } + } else { + couldNotResolveBackend(exchange); + } + } + + + @Override + public void couldNotResolveBackend(HttpServerExchange exchange) { + delegate.couldNotResolveBackend(exchange); + } + + + @Override + public void queuedRequestFailed(HttpServerExchange exchange) { + delegate.queuedRequestFailed(exchange); + } + + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java new file mode 100644 index 0000000000..3a0312c875 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 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.testsuite.arquillian.undertow.lb; + +import org.arquillian.undertow.UndertowContainerConfiguration; +import org.jboss.arquillian.container.spi.ConfigurationException; + +/** + * @author Marek Posolda + */ +public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerConfiguration { + + private String nodes = SimpleUndertowLoadBalancer.DEFAULT_NODES; + + public String getNodes() { + return nodes; + } + + public void setNodes(String nodes) { + this.nodes = nodes; + } + + @Override + public void validate() throws ConfigurationException { + super.validate(); + + try { + SimpleUndertowLoadBalancer.parseNodes(nodes); + } catch (Exception e) { + throw new ConfigurationException(e); + } + } +} diff --git a/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java new file mode 100644 index 0000000000..4b24c15030 --- /dev/null +++ b/testsuite/integration-arquillian/servers/auth-server/undertow/src/main/java/org/keycloak/testsuite/arquillian/undertow/lb/SimpleUndertowLoadBalancerContainer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016 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.testsuite.arquillian.undertow.lb; + +import org.jboss.arquillian.container.spi.client.container.DeployableContainer; +import org.jboss.arquillian.container.spi.client.container.DeploymentException; +import org.jboss.arquillian.container.spi.client.container.LifecycleException; +import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription; +import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData; +import org.jboss.logging.Logger; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.descriptor.api.Descriptor; + +/** + * Arquillian container over {@link SimpleUndertowLoadBalancer} + * + * @author Marek Posolda + */ +public class SimpleUndertowLoadBalancerContainer implements DeployableContainer { + + private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerContainer.class); + + private SimpleUndertowLoadBalancerConfiguration configuration; + private SimpleUndertowLoadBalancer container; + + @Override + public Class getConfigurationClass() { + return SimpleUndertowLoadBalancerConfiguration.class; + } + + @Override + public void setup(SimpleUndertowLoadBalancerConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public void start() throws LifecycleException { + this.container = new SimpleUndertowLoadBalancer(configuration.getBindAddress(), configuration.getBindHttpPort(), configuration.getNodes()); + this.container.start(); + } + + @Override + public void stop() throws LifecycleException { + log.info("Going to stop loadbalancer"); + this.container.stop(); + } + + @Override + public ProtocolDescription getDefaultProtocol() { + return new ProtocolDescription("Servlet 3.1"); + } + + @Override + public ProtocolMetaData deploy(Archive archive) throws DeploymentException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void undeploy(Archive archive) throws DeploymentException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void deploy(Descriptor descriptor) throws DeploymentException { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public void undeploy(Descriptor descriptor) throws DeploymentException { + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java index 45bbc60a1b..f8849b09a6 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkServlet.java @@ -47,7 +47,7 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet { String realm = request.getParameter("realm"); KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); AccessToken token = session.getToken(); - String clientSessionId = token.getClientSession(); + String clientId = token.getAudience()[0]; String nonce = UUID.randomUUID().toString(); MessageDigest md = null; try { @@ -55,7 +55,7 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet { } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } - String input = nonce + token.getSessionState() + clientSessionId + provider; + String input = nonce + token.getSessionState() + clientId + provider; byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8)); String hash = Base64Url.encode(check); request.getSession().setAttribute("hash", hash); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java index e583608f4d..7e7ee6f92c 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java @@ -71,6 +71,8 @@ public class AuthServerTestEnricher { private static final String AUTH_SERVER_CLUSTER_PROPERTY = "auth.server.cluster"; public static final boolean AUTH_SERVER_CLUSTER = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CLUSTER_PROPERTY, "false")); + private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false")); + private static final Boolean START_MIGRATION_CONTAINER = "auto".equals(System.getProperty("migration.mode")) || "manual".equals(System.getProperty("migration.mode")); @@ -112,9 +114,25 @@ public class AuthServerTestEnricher { suiteContext = new SuiteContext(containers); - String authServerFrontend = AUTH_SERVER_CLUSTER - ? "auth-server-balancer-wildfly" // if cluster mode enabled, load-balancer is the frontend - : AUTH_SERVER_CONTAINER; // single-node mode + String authServerFrontend = null; + + if (AUTH_SERVER_CLUSTER) { + // if cluster mode enabled, load-balancer is the frontend + for (ContainerInfo c : containers) { + if (c.getQualifier().startsWith("auth-server-balancer")) { + authServerFrontend = c.getQualifier(); + } + } + + if (authServerFrontend != null) { + log.info("Using frontend container: " + authServerFrontend); + } else { + throw new IllegalStateException("Not found frontend container"); + } + } else { + authServerFrontend = AUTH_SERVER_CONTAINER; // single-node mode + } + String authServerBackend = AUTH_SERVER_CONTAINER + "-backend"; int backends = 0; for (ContainerInfo container : suiteContext.getContainers()) { @@ -130,6 +148,11 @@ public class AuthServerTestEnricher { } } + // Setup with 2 undertow backend nodes and no loadbalancer. +// if (AUTH_SERVER_UNDERTOW_CLUSTER && suiteContext.getAuthServerInfo() == null && !suiteContext.getAuthServerBackendsInfo().isEmpty()) { +// suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0)); +// } + // validate auth server setup if (suiteContext.getAuthServerInfo() == null) { throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", authServerFrontend)); diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java index a7d8706c64..9f3f1968c7 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/page/LoginPasswordUpdatePage.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.page; import org.jboss.arquillian.drone.api.annotation.Drone; +import org.junit.Assert; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -53,6 +54,12 @@ public class LoginPasswordUpdatePage { return driver.getTitle().equals("Update password"); } + public void assertCurrent() { + String name = getClass().getSimpleName(); + Assert.assertTrue("Expected " + name + " but was " + driver.getTitle() + " (" + driver.getCurrentUrl() + ")", + isCurrent()); + } + public void open() { throw new UnsupportedOperationException(); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java index df8f1d07dd..346a0db313 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/InfoPage.java @@ -33,6 +33,9 @@ public class InfoPage extends AbstractPage { @FindBy(className = "instruction") private WebElement infoMessage; + @FindBy(linkText = "« Back to Application") + private WebElement backToApplicationLink; + public String getInfo() { return infoMessage.getText(); } @@ -46,4 +49,8 @@ public class InfoPage extends AbstractPage { throw new UnsupportedOperationException(); } + public void clickBackToApplicationLink() { + backToApplicationLink.click(); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java new file mode 100644 index 0000000000..e3ff938b08 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginExpiredPage.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 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.testsuite.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Marek Posolda + */ +public class LoginExpiredPage extends AbstractPage { + + @FindBy(id = "loginRestartLink") + private WebElement loginRestartLink; + + @FindBy(id = "loginContinueLink") + private WebElement loginContinueLink; + + + public void clickLoginRestartLink() { + loginRestartLink.click(); + } + + public void clickLoginContinueLink() { + loginContinueLink.click(); + } + + + public boolean isCurrent() { + return driver.getTitle().equals("Page has expired"); + } + + public void open() { + throw new UnsupportedOperationException(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java index 810ba84105..0fb07bf2cc 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/RegisterPage.java @@ -54,6 +54,9 @@ public class RegisterPage extends AbstractPage { @FindBy(className = "instruction") private WebElement loginInstructionMessage; + @FindBy(linkText = "« Back to Login") + private WebElement backToLoginLink; + public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) { firstNameInput.clear(); @@ -125,6 +128,10 @@ public class RegisterPage extends AbstractPage { submitButton.click(); } + public void clickBackToLogin() { + backToLoginLink.click(); + } + public String getError() { return loginErrorMessage != null ? loginErrorMessage.getText() : null; } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java index c1869d78aa..a6f42c82f2 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/AdminClientUtil.java @@ -33,6 +33,7 @@ import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider; import org.keycloak.admin.client.Keycloak; import org.keycloak.models.Constants; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.arquillian.SuiteContext; import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN; import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER; @@ -41,7 +42,7 @@ import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY; public class AdminClientUtil { - public static Keycloak createAdminClient(boolean ignoreUnknownProperties) throws Exception { + public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String authServerContextRoot) throws Exception { SSLContext ssl = null; if ("true".equals(System.getProperty("auth.server.ssl.required"))) { File trustore = new File(PROJECT_BUILD_DIRECTORY, "dependency/keystore/keycloak.truststore"); @@ -61,12 +62,12 @@ public class AdminClientUtil { jacksonProvider.setMapper(objectMapper); } - return Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth", + return Keycloak.getInstance(authServerContextRoot + "/auth", MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID, null, ssl, jacksonProvider); } public static Keycloak createAdminClient() throws Exception { - return createAdminClient(false); + return createAdminClient(false, AuthServerTestEnricher.getAuthServerContextRoot()); } private static SSLContext getSSLContextWithTrustore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java index ae7487df99..bc0b7873a5 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/GreenMailRule.java @@ -74,4 +74,38 @@ public class GreenMailRule extends ExternalResource { return greenMail.getReceivedMessages(); } + /** + * Returns the very last received message. When no message is available, returns {@code null}. + * @return see description + */ + public MimeMessage getLastReceivedMessage() { + MimeMessage[] receivedMessages = greenMail.getReceivedMessages(); + return (receivedMessages == null || receivedMessages.length == 0) + ? null + : receivedMessages[receivedMessages.length - 1]; + } + + /** + * Use this method if you are sending email in a different thread from the one you're testing from. + * Block waits for an email to arrive in any mailbox for any user. + * Implementation Detail: No polling wait implementation + * + * @param timeout maximum time in ms to wait for emailCount of messages to arrive before giving up and returning false + * @param emailCount waits for these many emails to arrive before returning + * @return + * @throws InterruptedException + */ + public boolean waitForIncomingEmail(long timeout, int emailCount) throws InterruptedException { + return greenMail.waitForIncomingEmail(timeout, emailCount); + } + + /** + * Does the same thing as Object.wait(long, int) but with a timeout of 5000ms. + * @param emailCount waits for these many emails to arrive before returning + * @return + * @throws InterruptedException + */ + public boolean waitForIncomingEmail(int emailCount) throws InterruptedException { + return greenMail.waitForIncomingEmail(emailCount); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java index 682e745bd7..4c89eaa6bb 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java @@ -41,6 +41,7 @@ import org.keycloak.constants.AdapterConstants; import org.keycloak.jose.jwk.JSONWebKeySet; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.utils.OIDCResponseType; @@ -73,7 +74,7 @@ import java.util.*; */ public class OAuthClient { public static final String SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot(); - public static final String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth"; + public static String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth"; public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app"; private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required")); @@ -89,7 +90,7 @@ public class OAuthClient { private String redirectUri; - private String state; + private StateParamProvider state; private String scope; @@ -162,7 +163,9 @@ public class OAuthClient { realm = "test"; clientId = "test-app"; redirectUri = APP_ROOT + "/auth"; - state = "mystate"; + state = () -> { + return KeycloakModelUtils.generateId(); + }; scope = null; uiLocales = null; clientSessionState = null; @@ -607,6 +610,7 @@ public class OAuthClient { if (redirectUri != null) { b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri); } + String state = this.state.getState(); if (state != null) { b.queryParam(OAuth2Constants.STATE, state); } @@ -692,8 +696,17 @@ public class OAuthClient { return this; } - public OAuthClient state(String state) { - this.state = state; + public OAuthClient stateParamHardcoded(String value) { + this.state = () -> { + return value; + }; + return this; + } + + public OAuthClient stateParamRandom() { + this.state = () -> { + return KeycloakModelUtils.generateId(); + }; return this; } @@ -927,4 +940,12 @@ public class OAuthClient { return publicKeys.get(realm); } + + private interface StateParamProvider { + + String getState(); + + } + + } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index ae1db357dd..1739370891 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -18,26 +18,19 @@ package org.keycloak.testsuite; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; -import org.apache.http.ssl.SSLContexts; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.Time; import org.keycloak.testsuite.arquillian.KcArquillian; import org.keycloak.testsuite.arquillian.TestContext; -import java.io.File; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import javax.net.ssl.SSLContext; + import javax.ws.rs.NotFoundException; import org.jboss.arquillian.container.test.api.RunAsClient; import org.jboss.arquillian.drone.api.annotation.Drone; @@ -53,7 +46,6 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmsResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.models.Constants; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -77,7 +69,6 @@ import org.openqa.selenium.WebDriver; import static org.keycloak.testsuite.admin.Users.setPasswordFor; import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN; import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER; -import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY; /** * @@ -135,7 +126,8 @@ public abstract class AbstractKeycloakTest { public void beforeAbstractKeycloakTest() throws Exception { adminClient = testContext.getAdminClient(); if (adminClient == null) { - adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting()); + String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString(); + adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), authServerContextRoot); testContext.setAdminClient(adminClient); } @@ -147,10 +139,9 @@ public abstract class AbstractKeycloakTest { TestEventsLogger.setDriver(driver); - if (!suiteContext.isAdminPasswordUpdated()) { - log.debug("updating admin password"); + // The backend cluster nodes may not be yet started. Password will be updated later for cluster setup. + if (!AuthServerTestEnricher.AUTH_SERVER_CLUSTER) { updateMasterAdminPassword(); - suiteContext.setAdminPasswordUpdated(true); } if (testContext.getTestRealmReps() == null) { @@ -202,10 +193,16 @@ public abstract class AbstractKeycloakTest { return false; } - private void updateMasterAdminPassword() { - welcomePage.navigateTo(); - if (!welcomePage.isPasswordSet()) { - welcomePage.setPassword("admin", "admin"); + protected void updateMasterAdminPassword() { + if (!suiteContext.isAdminPasswordUpdated()) { + log.debug("updating admin password"); + + welcomePage.navigateTo(); + if (!welcomePage.isPasswordSet()) { + welcomePage.setPassword("admin", "admin"); + } + + suiteContext.setAdminPasswordUpdated(true); } } @@ -236,7 +233,8 @@ public abstract class AbstractKeycloakTest { if (testingClient == null) { testingClient = testContext.getTestingClient(); if (testingClient == null) { - testingClient = KeycloakTestingClient.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth"); + String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString(); + testingClient = KeycloakTestingClient.getInstance(authServerContextRoot + "/auth"); testContext.setTestingClient(testingClient); } } @@ -348,6 +346,10 @@ public abstract class AbstractKeycloakTest { userResource.update(userRepresentation); } + /** + * Sets time offset in seconds that will be added to Time.currentTime() and Time.currentTimeMillis() both for client and server. + * @param offset + */ public void setTimeOffset(int offset) { String response = invokeTimeOffset(offset); resetTimeOffset = offset != 0; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java index 4445de9647..098c05a4a1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -20,6 +20,7 @@ package org.keycloak.testsuite; import org.hamcrest.CoreMatchers; import org.hamcrest.Description; import org.hamcrest.Matcher; +import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.junit.Assert; import org.junit.rules.TestRule; @@ -39,6 +40,7 @@ import javax.ws.rs.core.Response; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.hamcrest.Matchers.is; /** * @author Stian Thorgersen @@ -88,7 +90,7 @@ public class AssertEvents implements TestRule { } public ExpectedEvent expectRequiredAction(EventType event) { - return expectLogin().event(event).removeDetail(Details.CONSENT).session(isUUID()); + return expectLogin().event(event).removeDetail(Details.CONSENT).session(Matchers.isEmptyOrNullString()); } public ExpectedEvent expectLogin() { @@ -175,7 +177,7 @@ public class AssertEvents implements TestRule { private Matcher realmId; private Matcher userId; private Matcher sessionId; - private HashMap> details; + private HashMap> details; public ExpectedEvent realm(Matcher realmId) { this.realmId = realmId; @@ -240,9 +242,9 @@ public class AssertEvents implements TestRule { return detail(key, CoreMatchers.equalTo(value)); } - public ExpectedEvent detail(String key, Matcher matcher) { + public ExpectedEvent detail(String key, Matcher matcher) { if (details == null) { - details = new HashMap>(); + details = new HashMap>(); } details.put(key, matcher); return this; @@ -270,28 +272,28 @@ public class AssertEvents implements TestRule { } public EventRepresentation assertEvent(EventRepresentation actual) { - if (expected.getError() != null && !expected.getType().toString().endsWith("_ERROR")) { + if (expected.getError() != null && ! expected.getType().toString().endsWith("_ERROR")) { expected.setType(expected.getType() + "_ERROR"); } - Assert.assertEquals(expected.getType(), actual.getType()); - Assert.assertThat(actual.getRealmId(), realmId); - Assert.assertEquals(expected.getClientId(), actual.getClientId()); - Assert.assertEquals(expected.getError(), actual.getError()); - Assert.assertEquals(expected.getIpAddress(), actual.getIpAddress()); - Assert.assertThat(actual.getUserId(), userId); - Assert.assertThat(actual.getSessionId(), sessionId); + Assert.assertThat("type", actual.getType(), is(expected.getType())); + Assert.assertThat("realm ID", actual.getRealmId(), is(realmId)); + Assert.assertThat("client ID", actual.getClientId(), is(expected.getClientId())); + Assert.assertThat("error", actual.getError(), is(expected.getError())); + Assert.assertThat("ip address", actual.getIpAddress(), is(expected.getIpAddress())); + Assert.assertThat("user ID", actual.getUserId(), is(userId)); + Assert.assertThat("session ID", actual.getSessionId(), is(sessionId)); if (details == null || details.isEmpty()) { // Assert.assertNull(actual.getDetails()); } else { Assert.assertNotNull(actual.getDetails()); - for (Map.Entry> d : details.entrySet()) { + for (Map.Entry> d : details.entrySet()) { String actualValue = actual.getDetails().get(d.getKey()); if (!actual.getDetails().containsKey(d.getKey())) { Assert.fail(d.getKey() + " missing"); } - Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue()); + Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, is(d.getValue())); } /* for (String k : actual.getDetails().keySet()) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java index dcc4246733..eba81f4db2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -158,7 +158,6 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { @Before public void before() { - oauth.state("mystate"); // keycloak enforces that a state param has been sent by client userId = findUser("test-user@localhost").getId(); // Revert any password policy and user password changes @@ -854,7 +853,6 @@ public class AccountTest extends AbstractTestRealmKeycloakTest { try { OAuthClient oauth2 = new OAuthClient(); oauth2.init(adminClient, driver2); - oauth2.state("mystate"); oauth2.doLogin("view-sessions", "password"); EventRepresentation login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java index 59d277c76c..7fb9692961 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/TrustStoreEmailTest.java @@ -18,17 +18,23 @@ package org.keycloak.testsuite.account; import org.jboss.arquillian.graphene.page.Page; import org.junit.After; +import org.junit.Rule; import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.auth.page.account.AccountManagement; import org.keycloak.testsuite.auth.page.login.OIDCLogin; import org.keycloak.testsuite.auth.page.login.VerifyEmail; import org.keycloak.testsuite.util.MailServerConfiguration; -import org.keycloak.testsuite.util.RealmRepUtil; import org.keycloak.testsuite.util.SslMailServer; import static org.junit.Assert.assertEquals; @@ -54,6 +60,9 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest { @Page private VerifyEmail testRealmVerifyEmailPage; + @Rule + public AssertEvents events = new AssertEvents(this); + @Override public void configureTestRealm(RealmRepresentation testRealm) { log.info("enable verify email and configure smtp server to run with ssl in test realm"); @@ -86,6 +95,15 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest { accountManagement.navigateTo(); testRealmLoginPage.form().login(user.getUsername(), "password"); + EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL) + .user(user.getId()) + .client("account") + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.EMAIL, "test-user@localhost") + .removeDetail(Details.REDIRECT_URI) + .assertEvent(); + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + assertEquals("You need to verify your email address to activate your account.", testRealmVerifyEmailPage.getFeedbackText()); @@ -96,6 +114,23 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest { driver.navigate().to(verifyEmailUrl); + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .user(user.getId()) + .client("account") + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.EMAIL, "test-user@localhost") + .detail(Details.CODE_ID, mailCodeId) + .removeDetail(Details.REDIRECT_URI) + .assertEvent(); + + events.expectLogin() + .client("account") + .user(user.getId()) + .session(mailCodeId) + .detail(Details.USERNAME, "test-user@localhost") + .removeDetail(Details.REDIRECT_URI) + .assertEvent(); + assertCurrentUrlStartsWith(accountManagement); accountManagement.signOut(); testRealmLoginPage.form().login(user.getUsername(), "password"); @@ -103,15 +138,27 @@ public class TrustStoreEmailTest extends AbstractTestRealmKeycloakTest { } @Test - public void verifyEmailWithSslWrongCertificate() { + public void verifyEmailWithSslWrongCertificate() throws Exception { UserRepresentation user = ApiUtil.findUserByUsername(testRealm(), "test-user@localhost"); SslMailServer.startWithSsl(this.getClass().getClassLoader().getResource(SslMailServer.INVALID_KEY).getFile()); accountManagement.navigateTo(); loginPage.form().login(user.getUsername(), "password"); - assertEquals("Failed to send email, please try again later.\n" + - "« Back to Application", - testRealmVerifyEmailPage.getErrorMessage()); + events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL_ERROR) + .error(Errors.EMAIL_SEND_FAILED) + .user(user.getId()) + .client("account") + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.EMAIL, "test-user@localhost") + .removeDetail(Details.REDIRECT_URI) + .assertEvent(); + + // Email wasn't send + Assert.assertNull(SslMailServer.getLastReceivedMessage()); + + // Email wasn't send, but we won't notify end user about that. Admin is aware due to the error in the logs and the SEND_VERIFY_EMAIL_ERROR event. + assertEquals("You need to verify your email address to activate your account.", + testRealmVerifyEmailPage.getFeedbackText()); } } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java index eb719d884b..9fd5c7ac57 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.actions; +import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Before; @@ -25,12 +26,15 @@ import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; +import org.keycloak.models.Constants; +import org.keycloak.models.UserModel; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.auth.page.AuthRealm; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.ErrorPage; @@ -47,6 +51,7 @@ import javax.mail.Multipart; import javax.mail.internet.MimeMessage; import java.io.IOException; +import org.hamcrest.Matchers; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -79,6 +84,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo @Page protected ErrorPage errorPage; + private String testUserId; + @Override public void configureTestRealm(RealmRepresentation testRealm) { testRealm.setVerifyEmail(Boolean.TRUE); @@ -87,14 +94,11 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo @Before public void before() { - oauth.state("mystate"); // have to set this as keycloak validates that state is sent - - ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost"); UserRepresentation user = UserBuilder.create().enabled(true) .username("test-user@localhost") .email("test-user@localhost").build(); - ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + testUserId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); } /** @@ -106,11 +110,11 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo loginPage.open(); loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(verifyEmailPage.isCurrent()); + verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); - MimeMessage message = greenMail.getReceivedMessages()[0]; + MimeMessage message = greenMail.getLastReceivedMessage(); // see testsuite/integration-arquillian/tests/base/src/test/resources/testrealm.json Assert.assertEquals("", message.getHeader("Return-Path")[0]); @@ -124,7 +128,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo loginPage.open(); loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(verifyEmailPage.isCurrent()); + verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); @@ -134,19 +138,21 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost"); EventRepresentation sendEvent = emailEvent.assertEvent(); - String sessionId = sendEvent.getSessionId(); - String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); - Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]); - driver.navigate().to(verificationUrl.trim()); - events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .user(testUserId) + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.EMAIL, "test-user@localhost") + .detail(Details.CODE_ID, mailCodeId) + .assertEvent(); + appPage.assertCurrent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().session(sessionId).detail(Details.CODE_ID, mailCodeId).assertEvent(); + events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent(); } @Test @@ -157,15 +163,13 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo String userId = events.expectRegister("verifyEmail", "email@mail.com").assertEvent().getUserId(); - Assert.assertTrue(verifyEmailPage.isCurrent()); + verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[0]; - EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail("username", "verifyemail").detail("email", "email@mail.com").assertEvent(); - String sessionId = sendEvent.getSessionId(); - + EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).user(userId).detail(Details.USERNAME, "verifyemail").detail("email", "email@mail.com").assertEvent(); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); String verificationUrl = getPasswordResetEmailLink(message); @@ -174,9 +178,14 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectRequiredAction(EventType.VERIFY_EMAIL).user(userId).session(sessionId).detail("username", "verifyemail").detail("email", "email@mail.com").detail(Details.CODE_ID, mailCodeId).assertEvent(); + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .user(userId) + .detail(Details.USERNAME, "verifyemail") + .detail(Details.EMAIL, "email@mail.com") + .detail(Details.CODE_ID, mailCodeId) + .assertEvent(); - events.expectLogin().user(userId).session(sessionId).detail("username", "verifyemail").detail(Details.CODE_ID, mailCodeId).assertEvent(); + events.expectLogin().user(userId).session(mailCodeId).detail(Details.USERNAME, "verifyemail").assertEvent(); } @Test @@ -184,40 +193,95 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo loginPage.open(); loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(verifyEmailPage.isCurrent()); + verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); - EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent(); - String sessionId = sendEvent.getSessionId(); - + EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL) + .detail("email", "test-user@localhost") + .assertEvent(); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); verifyEmailPage.clickResendEmail(); + verifyEmailPage.assertCurrent(); + + events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL) + .detail(Details.CODE_ID, mailCodeId) + .detail("email", "test-user@localhost") + .assertEvent(); Assert.assertEquals(2, greenMail.getReceivedMessages().length); - MimeMessage message = greenMail.getReceivedMessages()[1]; - - events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").assertEvent(sendEvent); - + MimeMessage message = greenMail.getLastReceivedMessage(); String verificationUrl = getPasswordResetEmailLink(message); driver.navigate().to(verificationUrl.trim()); + appPage.assertCurrent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .user(testUserId) + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.EMAIL, "test-user@localhost") + .detail(Details.CODE_ID, mailCodeId) + .assertEvent(); - events.expectLogin().session(sessionId).assertEvent(); + events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent(); } @Test - public void verifyEmailResendFirstInvalidSecondStillValid() throws IOException, MessagingException { + public void verifyEmailResendWithRefreshes() throws IOException, MessagingException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + verifyEmailPage.assertCurrent(); + driver.navigate().refresh(); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + EventRepresentation sendEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL) + .detail("email", "test-user@localhost") + .assertEvent(); + String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); + + verifyEmailPage.clickResendEmail(); + verifyEmailPage.assertCurrent(); + driver.navigate().refresh(); + + events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL) + .detail(Details.CODE_ID, mailCodeId) + .detail("email", "test-user@localhost") + .assertEvent(); + + Assert.assertEquals(2, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getLastReceivedMessage(); + String verificationUrl = getPasswordResetEmailLink(message); + + driver.navigate().to(verificationUrl.trim()); + + appPage.assertCurrent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .user(testUserId) + .detail(Details.USERNAME, "test-user@localhost") + .detail(Details.EMAIL, "test-user@localhost") + .detail(Details.CODE_ID, mailCodeId) + .assertEvent(); + + events.expectLogin().user(testUserId).session(mailCodeId).detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + + @Test + public void verifyEmailResendFirstStillValidEvenWithSecond() throws IOException, MessagingException { + // Email verification can be performed any number of times loginPage.open(); loginPage.login("test-user@localhost", "password"); verifyEmailPage.clickResendEmail(); + verifyEmailPage.assertCurrent(); Assert.assertEquals(2, greenMail.getReceivedMessages().length); @@ -227,8 +291,8 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(verificationUrl1.trim()); - assertTrue(errorPage.isCurrent()); - assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError()); + appPage.assertCurrent(); + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); MimeMessage message2 = greenMail.getReceivedMessages()[1]; @@ -236,7 +300,38 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo driver.navigate().to(verificationUrl2.trim()); - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + infoPage.assertCurrent(); + Assert.assertEquals("You are already logged in.", infoPage.getInfo()); + } + + @Test + public void verifyEmailResendFirstAndSecondStillValid() throws IOException, MessagingException { + // Email verification can be performed any number of times + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + verifyEmailPage.clickResendEmail(); + verifyEmailPage.assertCurrent(); + + Assert.assertEquals(2, greenMail.getReceivedMessages().length); + + MimeMessage message1 = greenMail.getReceivedMessages()[0]; + + String verificationUrl1 = getPasswordResetEmailLink(message1); + + driver.navigate().to(verificationUrl1.trim()); + + appPage.assertCurrent(); + appPage.logout(); + + MimeMessage message2 = greenMail.getReceivedMessages()[1]; + + String verificationUrl2 = getPasswordResetEmailLink(message2); + + driver.navigate().to(verificationUrl2.trim()); + + infoPage.assertCurrent(); + assertEquals("Your email address has been verified.", infoPage.getInfo()); } @Test @@ -244,92 +339,62 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo loginPage.open(); loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(verifyEmailPage.isCurrent()); + verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); - MimeMessage message = greenMail.getReceivedMessages()[0]; + MimeMessage message = greenMail.getLastReceivedMessage(); String verificationUrl = getPasswordResetEmailLink(message); AssertEvents.ExpectedEvent emailEvent = events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost"); EventRepresentation sendEvent = emailEvent.assertEvent(); - String sessionId = sendEvent.getSessionId(); String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID); - Assert.assertEquals(mailCodeId, verificationUrl.split("code=")[1].split("\\&")[0].split("\\.")[1]); - driver.manage().deleteAllCookies(); driver.navigate().to(verificationUrl.trim()); - events.expectRequiredAction(EventType.VERIFY_EMAIL).session(sessionId).detail("email", "test-user@localhost").detail(Details.CODE_ID, mailCodeId).assertEvent(); + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .user(testUserId) + .detail(Details.CODE_ID, Matchers.not(Matchers.is(mailCodeId))) + .client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID) // as authentication sessions are browser-specific, + // the client and redirect_uri is unrelated to + // the "test-app" specified in loginPage.open() + .detail(Details.REDIRECT_URI, Matchers.any(String.class)) + .assertEvent(); - assertTrue(infoPage.isCurrent()); + infoPage.assertCurrent(); assertEquals("Your email address has been verified.", infoPage.getInfo()); loginPage.open(); - - assertTrue(loginPage.isCurrent()); - } - - - @Test - public void verifyInvalidKeyOrCode() throws IOException, MessagingException { - loginPage.open(); - loginPage.login("test-user@localhost", "password"); - - Assert.assertTrue(verifyEmailPage.isCurrent()); - String resendEmailLink = verifyEmailPage.getResendEmailLink(); - String keyInsteadCodeURL = resendEmailLink.replace("code=", "key="); - - events.expectRequiredAction(EventType.SEND_VERIFY_EMAIL).detail("email", "test-user@localhost").assertEvent(); - - driver.navigate().to(keyInsteadCodeURL); - - events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR) - .error(Errors.INVALID_CODE) - .client((String)null) - .user((String)null) - .session((String)null) - .clearDetails() - .assertEvent(); - - String badKeyURL = KeycloakUriBuilder.fromUri(resendEmailLink).replaceQueryParam("key", "foo").build().toString(); - driver.navigate().to(badKeyURL); - - events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR) - .error(Errors.INVALID_CODE) - .client((String)null) - .user((String)null) - .session((String)null) - .clearDetails() - .assertEvent(); + loginPage.assertCurrent(); } @Test - public void verifyEmailBadCode() throws IOException, MessagingException { + public void verifyEmailInvalidKeyInVerficationLink() throws IOException, MessagingException { loginPage.open(); loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(verifyEmailPage.isCurrent()); + verifyEmailPage.assertCurrent(); Assert.assertEquals(1, greenMail.getReceivedMessages().length); - MimeMessage message = greenMail.getReceivedMessages()[0]; + MimeMessage message = greenMail.getLastReceivedMessage(); String verificationUrl = getPasswordResetEmailLink(message); - verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam("code", "foo").build().toString(); + verificationUrl = KeycloakUriBuilder.fromUri(verificationUrl).replaceQueryParam(Constants.KEY, "foo").build().toString(); events.poll(); driver.navigate().to(verificationUrl.trim()); - assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError()); + errorPage.assertCurrent(); + assertEquals("An error occurred, please login again through your application.", errorPage.getError()); - events.expectRequiredAction(EventType.VERIFY_EMAIL_ERROR) + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR) .error(Errors.INVALID_CODE) .client((String)null) .user((String)null) @@ -338,6 +403,80 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo .assertEvent(); } + @Test + public void verifyEmailExpiredCode() throws IOException, MessagingException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + verifyEmailPage.assertCurrent(); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getLastReceivedMessage(); + + String verificationUrl = getPasswordResetEmailLink(message); + + events.poll(); + + try { + setTimeOffset(3600); + + driver.navigate().to(verificationUrl.trim()); + + loginPage.assertCurrent(); + assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); + + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR) + .error(Errors.EXPIRED_CODE) + .client((String)null) + .user(testUserId) + .session((String)null) + .clearDetails() + .detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE) + .assertEvent(); + } finally { + setTimeOffset(0); + } + } + + @Test + public void verifyEmailExpiredCodeAndExpiredSession() throws IOException, MessagingException { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + verifyEmailPage.assertCurrent(); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getLastReceivedMessage(); + + String verificationUrl = getPasswordResetEmailLink(message); + + events.poll(); + + try { + setTimeOffset(3600); + + driver.manage().deleteAllCookies(); + + driver.navigate().to(verificationUrl.trim()); + + errorPage.assertCurrent(); + assertEquals("The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?", errorPage.getError()); + + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR) + .error(Errors.EXPIRED_CODE) + .client((String)null) + .user(testUserId) + .session((String)null) + .clearDetails() + .detail(Details.ACTION, VerifyEmailActionToken.TOKEN_TYPE) + .assertEvent(); + } finally { + setTimeOffset(0); + } + } + public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException { Multipart multipart = (Multipart) message.getContent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java index 433b0b124d..124620a9df 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionMultipleActionsTest.java @@ -63,46 +63,50 @@ public class RequiredActionMultipleActionsTest extends AbstractTestRealmKeycloak loginPage.open(); loginPage.login("test-user@localhost", "password"); - String sessionId = null; + String codeId = null; if (changePasswordPage.isCurrent()) { - sessionId = updatePassword(sessionId); + codeId = updatePassword(codeId); updateProfilePage.assertCurrent(); - updateProfile(sessionId); + updateProfile(codeId); } else if (updateProfilePage.isCurrent()) { - sessionId = updateProfile(sessionId); + codeId = updateProfile(codeId); changePasswordPage.assertCurrent(); - updatePassword(sessionId); + updatePassword(codeId); } else { Assert.fail("Expected to update password and profile before login"); } Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().session(sessionId).assertEvent(); + events.expectLogin().session(codeId).assertEvent(); } - public String updatePassword(String sessionId) { + public String updatePassword(String codeId) { changePasswordPage.changePassword("new-password", "new-password"); AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_PASSWORD); - if (sessionId != null) { - expectedEvent.session(sessionId); + if (codeId != null) { + expectedEvent.detail(Details.CODE_ID, codeId); } - return expectedEvent.assertEvent().getSessionId(); + return expectedEvent.assertEvent().getDetails().get(Details.CODE_ID); } - public String updateProfile(String sessionId) { + public String updateProfile(String codeId) { updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); - AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com"); - if (sessionId != null) { - expectedEvent.session(sessionId); + AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL) + .detail(Details.PREVIOUS_EMAIL, "test-user@localhost") + .detail(Details.UPDATED_EMAIL, "new@email.com"); + if (codeId != null) { + expectedEvent.detail(Details.CODE_ID, codeId); } - sessionId = expectedEvent.assertEvent().getSessionId(); - events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent(); - return sessionId; + codeId = expectedEvent.assertEvent().getDetails().get(Details.CODE_ID); + events.expectRequiredAction(EventType.UPDATE_PROFILE) + .detail(Details.CODE_ID, codeId) + .assertEvent(); + return codeId; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java index d457b0bcad..59f88fe2a0 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionResetPasswordTest.java @@ -59,11 +59,6 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe @Page protected LoginPasswordUpdatePage changePasswordPage; - @Before - public void before() { - oauth.state("mystate"); // have to set this as keycloak validates that state is sent - } - @Test public void tempPassword() throws Exception { @@ -73,11 +68,11 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe changePasswordPage.assertCurrent(); changePasswordPage.changePassword("new-password", "new-password"); - String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); + EventRepresentation loginEvent = events.expectLogin().assertEvent(); oauth.openLogout(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java index f613087706..a06079c0bc 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java @@ -127,11 +127,12 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent().getSessionId(); + String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp").assertEvent() + .getDetails().get(Details.CODE_ID); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setuptotp").assertEvent(); + events.expectLogin().user(userId).session(authSessionId).detail(Details.USERNAME, "setuptotp").assertEvent(); } @Test @@ -145,15 +146,16 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { totpPage.configure(totp.generateTOTP(totpSecret)); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId(); + String authSessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + .getDetails().get(Details.CODE_ID); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - EventRepresentation loginEvent = events.expectLogin().session(sessionId).assertEvent(); + EventRepresentation loginEvent = events.expectLogin().session(authSessionId).assertEvent(); oauth.openLogout(); - events.expectLogout(loginEvent.getSessionId()).assertEvent(); + events.expectLogout(authSessionId).assertEvent(); loginPage.open(); loginPage.login("test-user@localhost", "password"); @@ -229,7 +231,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { totpPage.assertCurrent(); totpPage.configure(totp.generateTOTP(totpPage.getTotpSecret())); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent() + .getDetails().get(Details.CODE_ID); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -260,7 +263,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { TimeBasedOTP timeBased = new TimeBasedOTP(HmacOTP.HMAC_SHA1, 8, 30, 1); totpPage.configure(timeBased.generateTOTP(totpSecret)); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + .getDetails().get(Details.CODE_ID); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -311,7 +315,8 @@ public class RequiredActionTotpSetupTest extends AbstractTestRealmKeycloakTest { HmacOTP otpgen = new HmacOTP(6, HmacOTP.HMAC_SHA1, 1); totpPage.configure(otpgen.generateHOTP(totpSecret, 0)); String uri = driver.getCurrentUrl(); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent().getSessionId(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).assertEvent() + .getDetails().get(Details.CODE_ID); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index 80ee0fee84..9c18070672 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.actions; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Before; @@ -89,12 +90,12 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); - String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId(); - events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent(); + events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PROFILE).assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().session(sessionId).assertEvent(); + events.expectLogin().assertEvent(); // assert user is really updated in persistent store UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); @@ -116,19 +117,17 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.update("New first", "New last", "john-doh@localhost", "new"); - String sessionId = events - .expectLogin() + events.expectLogin() .event(EventType.UPDATE_PROFILE) .detail(Details.USERNAME, "john-doh@localhost") .user(userId) - .session(AssertEvents.isUUID()) + .session(Matchers.nullValue(String.class)) .removeDetail(Details.CONSENT) - .assertEvent() - .getSessionId(); + .assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).session(sessionId).assertEvent(); + events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).assertEvent(); // assert user is really updated in persistent store UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "new"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java index ab5229482b..86066e8ac7 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.actions; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; import org.junit.Before; @@ -86,11 +87,11 @@ public class TermsAndConditionsTest extends AbstractTestRealmKeycloakTest { termsPage.acceptTerms(); - String sessionId = events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent().getSessionId(); + events.expectRequiredAction(EventType.CUSTOM_REQUIRED_ACTION).removeDetail(Details.REDIRECT_URI).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID).assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().session(sessionId).assertEvent(); + events.expectLogin().assertEvent(); // assert user attribute is properly set UserRepresentation user = ActionUtil.findUserWithAdminClient(adminClient, "test-user@localhost"); @@ -123,6 +124,7 @@ public class TermsAndConditionsTest extends AbstractTestRealmKeycloakTest { events.expectLogin().event(EventType.CUSTOM_REQUIRED_ACTION_ERROR).detail(Details.CUSTOM_REQUIRED_ACTION, TermsAndConditions.PROVIDER_ID) .error(Errors.REJECTED_BY_USER) .removeDetail(Details.CONSENT) + .session(Matchers.nullValue(String.class)) .assertEvent(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java index 5c3f1f1e60..f6a8bb247d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java @@ -28,6 +28,8 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.List; +import javax.ws.rs.core.Response; + import org.jboss.arquillian.container.test.api.Deployer; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.BeforeClass; @@ -199,7 +201,8 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda assertFalse(policy.getUsers().isEmpty()); - getAuthorizationResource().policies().user().create(policy); + Response response = getAuthorizationResource().policies().user().create(policy); + response.close(); } protected interface ExceptionRunnable { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java index b37e9e60fb..49158faacf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzFunctionalAdapterTest.java @@ -23,6 +23,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import javax.ws.rs.core.Response; + import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.junit.Test; @@ -289,7 +291,8 @@ public abstract class AbstractServletAuthzFunctionalAdapterTest extends Abstract policy.addClient("admin-cli"); ClientPoliciesResource policyResource = getAuthorizationResource().policies().client(); - policyResource.create(policy); + Response response = policyResource.create(policy); + response.close(); policy = policyResource.findByName(policy.getName()); updatePermissionPolicies("Protected Resource Permission", policy.getName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java index 72e8c46622..750df031f7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractClientInitiatedAccountLinkTest.java @@ -38,10 +38,14 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; +import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.broker.BrokerTestTools; import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginUpdateProfilePage; import org.keycloak.testsuite.pages.UpdateAccountInformationPage; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.WaitUtils; @@ -72,11 +76,17 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer public static final String PARENT_USERNAME = "parent"; @Page - protected UpdateAccountInformationPage profilePage; + protected LoginUpdateProfilePage loginUpdateProfilePage; + + @Page + protected AccountUpdateProfilePage profilePage; @Page private LoginPage loginPage; + @Page + protected ErrorPage errorPage; + public static class ClientApp extends AbstractPageWithInjectedUrl { public static final String DEPLOYMENT_NAME = "client-linking"; @@ -532,6 +542,92 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer } + + @Test + public void testAccountNotLinkedAutomatically() throws Exception { + RealmResource realm = adminClient.realms().realm(CHILD_IDP); + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + // Login to account mgmt first + profilePage.open(CHILD_IDP); + WaitUtils.waitForPageToLoad(driver); + + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + profilePage.assertCurrent(); + + // Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie + UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString()) + .path("nosuch"); + String linkUrl = linkBuilder.clone() + .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN) + .build().toString(); + + navigateTo(linkUrl); + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.clickSocial(PARENT_IDP); + Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); + loginPage.login(PARENT_USERNAME, "password"); + + // Test I was not automatically linked. + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + loginUpdateProfilePage.assertCurrent(); + loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com"); + + errorPage.assertCurrent(); + Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError()); + + logoutAll(); + + // Remove newly created user + String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId(); + getCleanup("child").addUserId(newUserId); + } + + + @Test + public void testAccountLinkingExpired() throws Exception { + RealmResource realm = adminClient.realms().realm(CHILD_IDP); + List links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + // Login to account mgmt first + profilePage.open(CHILD_IDP); + WaitUtils.waitForPageToLoad(driver); + + Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); + loginPage.login("child", "password"); + profilePage.assertCurrent(); + + // Now in another tab, request account linking + UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString()) + .path("link"); + String linkUrl = linkBuilder.clone() + .queryParam("realm", CHILD_IDP) + .queryParam("provider", PARENT_IDP).build().toString(); + navigateTo(linkUrl); + + Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); + + // Logout "child" userSession in the meantime (for example through admin request) + realm.logoutAll(); + + // Finish login on parent. + loginPage.login(PARENT_USERNAME, "password"); + + // Test I was not automatically linked + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + + errorPage.assertCurrent(); + Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError()); + + logoutAll(); + } + private void navigateTo(String uri) { driver.navigate().to(uri); WaitUtils.waitForPageToLoad(driver); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java index cc47020a74..56f21ab1ad 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ApiUtil.java @@ -137,6 +137,13 @@ public class ApiUtil { return realm.users().get(findUserByUsername(realm, username).getId()); } + /** + * Creates a user + * @param realm + * @param user + * @param password + * @return ID of the new user + */ public static String createUserWithAdminClient(RealmResource realm, UserRepresentation user) { Response response = realm.users().create(user); String createdId = getCreatedId(response); @@ -144,6 +151,13 @@ public class ApiUtil { return createdId; } + /** + * Creates a user and sets the password + * @param realm + * @param user + * @param password + * @return ID of the new user + */ public static String createUserAndResetPasswordWithAdminClient(RealmResource realm, UserRepresentation user, String password) { String id = createUserWithAdminClient(realm, user); resetUserPassword(realm.users().get(id), password, false); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java index d821179048..531157daa3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java @@ -46,8 +46,6 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation; import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserFederationMapperRepresentation; -import org.keycloak.representations.idm.UserFederationProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; @@ -64,7 +62,6 @@ import org.keycloak.testsuite.util.FederatedIdentityBuilder; import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.RealmBuilder; -import org.keycloak.testsuite.util.TestCleanup; import org.keycloak.testsuite.util.UserBuilder; import javax.ws.rs.ClientErrorException; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 6a75b333c1..567b2846fa 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -21,9 +21,7 @@ import org.hamcrest.Matchers; import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.test.api.ArquillianResource; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.IdentityProviderResource; @@ -47,6 +45,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.resources.RealmsResource; import org.keycloak.testsuite.page.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.InfoPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.AdminEventPaths; @@ -62,7 +61,6 @@ import org.openqa.selenium.WebDriver; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import javax.ws.rs.ClientErrorException; -import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.IOException; @@ -71,6 +69,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -98,6 +97,9 @@ public class UserTest extends AbstractAdminTest { @Page protected InfoPage infoPage; + @Page + protected ErrorPage errorPage; + @Page protected LoginPage loginPage; @@ -541,7 +543,229 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); - assertTrue(passwordUpdatePage.isCurrent()); + passwordUpdatePage.assertCurrent(); + + passwordUpdatePage.changePassword("new-pass", "new-pass"); + + assertEquals("Your account has been updated.", driver.getTitle()); + + driver.navigate().to(link); + + assertEquals("We're sorry...", driver.getTitle()); + } + + @Test + public void sendResetPasswordEmailSuccessTwoLinks() throws IOException, MessagingException { + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + user.executeActionsEmail(actions); + user.executeActionsEmail(actions); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + + Assert.assertEquals(2, greenMail.getReceivedMessages().length); + + int i = 1; + for (MimeMessage message : greenMail.getReceivedMessages()) { + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); + + passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i); + i++; + + assertEquals("Your account has been updated.", driver.getTitle()); + } + + for (MimeMessage message : greenMail.getReceivedMessages()) { + String link = MailUtils.getPasswordResetEmailLink(message); + driver.navigate().to(link); + errorPage.assertCurrent(); + } + } + + @Test + public void sendResetPasswordEmailSuccessTwoLinksReverse() throws IOException, MessagingException { + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + user.executeActionsEmail(actions); + user.executeActionsEmail(actions); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + + Assert.assertEquals(2, greenMail.getReceivedMessages().length); + + int i = 1; + for (int j = greenMail.getReceivedMessages().length - 1; j >= 0; j--) { + MimeMessage message = greenMail.getReceivedMessages()[j]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); + + passwordUpdatePage.changePassword("new-pass" + i, "new-pass" + i); + i++; + + assertEquals("Your account has been updated.", driver.getTitle()); + } + + for (MimeMessage message : greenMail.getReceivedMessages()) { + String link = MailUtils.getPasswordResetEmailLink(message); + driver.navigate().to(link); + errorPage.assertCurrent(); + } + } + + @Test + public void sendResetPasswordEmailSuccessLinkOpenDoesNotExpireWhenOpenedOnly() throws IOException, MessagingException { + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + user.executeActionsEmail(actions); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); + + driver.manage().deleteAllCookies(); + driver.navigate().to("about:blank"); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); + + passwordUpdatePage.changePassword("new-pass", "new-pass"); + + assertEquals("Your account has been updated.", driver.getTitle()); + } + + @Test + public void sendResetPasswordEmailSuccessTokenShortLifespan() throws IOException, MessagingException { + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + final AtomicInteger originalValue = new AtomicInteger(); + + RealmRepresentation realmRep = realm.toRepresentation(); + originalValue.set(realmRep.getActionTokenGeneratedByAdminLifespan()); + realmRep.setActionTokenGeneratedByAdminLifespan(60); + realm.update(realmRep); + + try { + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + user.executeActionsEmail(actions); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + setTimeOffset(70); + + driver.navigate().to(link); + + errorPage.assertCurrent(); + assertEquals("An error occurred, please login again through your application.", errorPage.getError()); + } finally { + setTimeOffset(0); + + realmRep.setActionTokenGeneratedByAdminLifespan(originalValue.get()); + realm.update(realmRep); + } + } + + @Test + public void sendResetPasswordEmailSuccessWithRecycledAuthSession() throws IOException, MessagingException { + UserRepresentation userRep = new UserRepresentation(); + userRep.setEnabled(true); + userRep.setUsername("user1"); + userRep.setEmail("user1@test.com"); + + String id = createUser(userRep); + + UserResource user = realm.users().get(id); + List actions = new LinkedList<>(); + actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name()); + + // The following block creates a client and requests updating password with redirect to this client. + // After clicking the link (starting a fresh auth session with client), the user goes away and sends the email + // with password reset again - now without the client - and attempts to complete the password reset. + { + ClientRepresentation client = new ClientRepresentation(); + client.setClientId("myclient2"); + client.setRedirectUris(new LinkedList<>()); + client.getRedirectUris().add("http://myclient.com/*"); + client.setName("myclient2"); + client.setEnabled(true); + Response response = realm.clients().create(client); + String createdId = ApiUtil.getCreatedId(response); + assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientResourcePath(createdId), client, ResourceType.CLIENT); + + user.executeActionsEmail("myclient2", "http://myclient.com/home.html", actions); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + } + + user.executeActionsEmail(actions); + assertAdminEvents.assertEvent(realmId, OperationType.ACTION, AdminEventPaths.userResourcePath(id) + "/execute-actions-email", ResourceType.USER); + + Assert.assertEquals(2, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; + + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + + passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); @@ -601,7 +825,7 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); - assertTrue(passwordUpdatePage.isCurrent()); + passwordUpdatePage.assertCurrent(); passwordUpdatePage.changePassword("new-pass", "new-pass"); @@ -673,6 +897,11 @@ public class UserTest extends AbstractAdminTest { driver.navigate().to(link); Assert.assertEquals("Your account has been updated.", infoPage.getInfo()); + + driver.navigate().to("about:blank"); + + driver.navigate().to(link); // It should be possible to use the same action token multiple times + Assert.assertEquals("Your account has been updated.", infoPage.getInfo()); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java index e0a4c53460..41f3890f6c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractPolicyManagementTest.java @@ -28,6 +28,8 @@ import java.util.List; import java.util.Set; import java.util.function.Supplier; +import javax.ws.rs.core.Response; + import org.junit.Before; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; @@ -39,7 +41,6 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.representations.idm.authorization.UserPolicyRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; -import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; @@ -131,7 +132,10 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest resources.add(new ResourceRepresentation("Resource B", scopes)); resources.add(new ResourceRepresentation("Resource C", scopes)); - resources.forEach(resource -> getClient().authorization().resources().create(resource)); + resources.forEach(resource -> { + Response response = getClient().authorization().resources().create(resource); + response.close(); + }); } private void createPolicies(RealmResource realm, ClientResource client) throws IOException { @@ -147,7 +151,8 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest representation.setName(name); representation.addUser(userId); - client.authorization().policies().user().create(representation); + Response response = client.authorization().policies().user().create(representation); + response.close(); } protected ClientResource getClient() { @@ -161,7 +166,7 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest protected RealmResource getRealm() { try { - return AdminClientUtil.createAdminClient().realm("authz-test"); + return adminClient.realm("authz-test"); } catch (Exception cause) { throw new RuntimeException("Failed to create admin client", cause); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java index 87848d911c..d3613da809 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ClientPolicyManagementTest.java @@ -112,6 +112,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest { ClientPoliciesResource policies = authorization.policies().client(); Response response = policies.create(representation); ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class); + response.close(); policies.findById(created.getId()).remove(); @@ -136,6 +137,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest { ClientPoliciesResource policies = authorization.policies().client(); Response response = policies.create(representation); ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class); + response.close(); PolicyResource policy = authorization.policies().policy(created.getId()); PolicyRepresentation genericConfig = policy.toRepresentation(); @@ -152,6 +154,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest { ClientPoliciesResource permissions = authorization.policies().client(); Response response = permissions.create(representation); ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class); + response.close(); ClientPolicyResource permission = permissions.findById(created.getId()); assertRepresentation(representation, permission); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java index e5c128745a..ba0b7c9759 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java @@ -255,6 +255,8 @@ public class RealmTest extends AbstractAdminTest { rep.setSsoSessionIdleTimeout(123); rep.setSsoSessionMaxLifespan(12); rep.setAccessCodeLifespanLogin(1234); + rep.setActionTokenGeneratedByAdminLifespan(2345); + rep.setActionTokenGeneratedByUserLifespan(3456); rep.setRegistrationAllowed(true); rep.setRegistrationEmailAsUsername(true); rep.setEditUsernameAllowed(true); @@ -267,6 +269,8 @@ public class RealmTest extends AbstractAdminTest { assertEquals(123, rep.getSsoSessionIdleTimeout().intValue()); assertEquals(12, rep.getSsoSessionMaxLifespan().intValue()); assertEquals(1234, rep.getAccessCodeLifespanLogin().intValue()); + assertEquals(2345, rep.getActionTokenGeneratedByAdminLifespan().intValue()); + assertEquals(3456, rep.getActionTokenGeneratedByUserLifespan().intValue()); assertEquals(Boolean.TRUE, rep.isRegistrationAllowed()); assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername()); assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed()); @@ -443,6 +447,12 @@ public class RealmTest extends AbstractAdminTest { if (realm.getAccessCodeLifespan() != null) assertEquals(realm.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan()); if (realm.getAccessCodeLifespanUserAction() != null) assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getAccessCodeLifespanUserAction()); + if (realm.getActionTokenGeneratedByAdminLifespan() != null) + assertEquals(realm.getActionTokenGeneratedByAdminLifespan(), storedRealm.getActionTokenGeneratedByAdminLifespan()); + if (realm.getActionTokenGeneratedByUserLifespan() != null) + assertEquals(realm.getActionTokenGeneratedByUserLifespan(), storedRealm.getActionTokenGeneratedByUserLifespan()); + else + assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getActionTokenGeneratedByUserLifespan()); if (realm.getNotBefore() != null) assertEquals(realm.getNotBefore(), storedRealm.getNotBefore()); if (realm.getAccessTokenLifespan() != null) assertEquals(realm.getAccessTokenLifespan(), storedRealm.getAccessTokenLifespan()); if (realm.getAccessTokenLifespanForImplicitFlow() != null) assertEquals(realm.getAccessTokenLifespanForImplicitFlow(), storedRealm.getAccessTokenLifespanForImplicitFlow()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java index 4d5897cf56..450820ed5e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java @@ -50,7 +50,6 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; -import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; @@ -140,7 +139,7 @@ public class ConflictingScopePermissionTest extends AbstractKeycloakTest { } private RealmResource getRealm() throws Exception { - return AdminClientUtil.createAdminClient().realm("authz-test"); + return adminClient.realm("authz-test"); } private ClientResource getClient(RealmResource realm) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java index e2eae3421d..cf54a66a62 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/RequireUmaAuthorizationScopeTest.java @@ -24,6 +24,8 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import javax.ws.rs.core.Response; + import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.AuthorizationResource; @@ -43,7 +45,6 @@ import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; -import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RoleBuilder; @@ -77,14 +78,16 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest { AuthorizationResource authorization = client.authorization(); ResourceRepresentation resource = new ResourceRepresentation("Resource A"); - authorization.resources().create(resource); + Response response = authorization.resources().create(resource); + response.close(); JSPolicyRepresentation policy = new JSPolicyRepresentation(); policy.setName("Default Policy"); policy.setCode("$evaluation.grant();"); - authorization.policies().js().create(policy); + response = authorization.policies().js().create(policy); + response.close(); ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); @@ -92,7 +95,8 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest { permission.addResource(resource.getName()); permission.addPolicy(policy.getName()); - authorization.permissions().resource().create(permission); + response = authorization.permissions().resource().create(permission); + response.close(); } @Test @@ -140,7 +144,7 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest { } private RealmResource getRealm() throws Exception { - return AdminClientUtil.createAdminClient().realm("authz-test"); + return adminClient.realm("authz-test"); } private ClientResource getClient(RealmResource realm) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java index 25abe65b22..d5c9ff2d3c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractClusterTest.java @@ -45,6 +45,12 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest { logFailoverSetup(); } + // Assume that route like "node6" will have corresponding backend container like "auth-server-wildfly-backend6" + protected void setCurrentFailNodeForRoute(String route) { + String routeNumber = route.substring(route.length() - 1); + currentFailNodeIndex = Integer.parseInt(routeNumber) - 1; + } + protected ContainerInfo getCurrentFailNode() { return backendNode(currentFailNodeIndex); } @@ -111,9 +117,13 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest { } protected Keycloak getAdminClientFor(ContainerInfo node) { - return node.equals(suiteContext.getAuthServerInfo()) - ? adminClient // frontend client - : backendAdminClients.get(node); + Keycloak adminClient = backendAdminClients.get(node); + + if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) { + adminClient = this.adminClient; + } + + return adminClient; } @Before diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java new file mode 100644 index 0000000000..aa65e79628 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AbstractFailoverClusterTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 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.testsuite.cluster; + + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.page.AbstractPage; +import org.keycloak.testsuite.page.PageWithLogOutAction; +import org.openqa.selenium.Cookie; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; +import static org.keycloak.testsuite.util.WaitUtils.pause; + +public abstract class AbstractFailoverClusterTest extends AbstractClusterTest { + + public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION"; + + public static final Integer SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("session.cache.owners", "1")); + public static final Integer OFFLINE_SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("offline.session.cache.owners", "1")); + public static final Integer LOGIN_FAILURES_CACHE_OWNERS = Integer.parseInt(System.getProperty("login.failure.cache.owners", "1")); + + public static final Integer REBALANCE_WAIT = Integer.parseInt(System.getProperty("rebalance.wait", "5000")); + + @Override + public void addTestRealms(List testRealms) { + } + + + /** + * failure --> failback --> failure of next node + */ + protected void switchFailedNode() { + assertFalse(controller.isStarted(getCurrentFailNode().getQualifier())); + + failback(); + pause(REBALANCE_WAIT); + + iterateCurrentFailNode(); + + failure(); + pause(REBALANCE_WAIT); + + assertFalse(controller.isStarted(getCurrentFailNode().getQualifier())); + } + + protected Cookie login(AbstractPage targetPage) { + targetPage.navigateTo(); + assertCurrentUrlStartsWith(loginPage); + loginPage.form().login(ADMIN, ADMIN); + assertCurrentUrlStartsWith(targetPage); + Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); + assertNotNull(sessionCookie); + return sessionCookie; + } + + protected void logout(AbstractPage targetPage) { + if (!(targetPage instanceof PageWithLogOutAction)) { + throw new IllegalArgumentException(targetPage.getClass().getSimpleName() + " must implement PageWithLogOutAction interface"); + } + targetPage.navigateTo(); + assertCurrentUrlStartsWith(targetPage); + ((PageWithLogOutAction) targetPage).logOut(); + } + + protected Cookie verifyLoggedIn(AbstractPage targetPage, Cookie sessionCookieForVerification) { + // verify on realm path + masterRealmPage.navigateTo(); + Cookie sessionCookieOnRealmPath = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); + assertNotNull(sessionCookieOnRealmPath); + assertEquals(sessionCookieOnRealmPath.getValue(), sessionCookieForVerification.getValue()); + // verify on target page + targetPage.navigateTo(); + assertCurrentUrlStartsWith(targetPage); + Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); + assertNotNull(sessionCookie); + assertEquals(sessionCookie.getValue(), sessionCookieForVerification.getValue()); + return sessionCookie; + } + + protected void verifyLoggedOut(AbstractPage targetPage) { + // verify on target page + targetPage.navigateTo(); + driver.navigate().refresh(); + assertCurrentUrlStartsWith(loginPage); + Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); + assertNull(sessionCookie); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java new file mode 100644 index 0000000000..c1811f95ff --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/AuthenticationSessionFailoverClusterTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2016 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.testsuite.cluster; + +import java.io.IOException; +import java.util.List; + +import javax.mail.MessagingException; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.managers.AuthenticationSessionManager; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.LoginUpdateProfilePage; +import org.keycloak.testsuite.util.UserBuilder; +import org.openqa.selenium.Cookie; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; +import static org.keycloak.testsuite.util.WaitUtils.pause; + +/** + * @author Marek Posolda + */ +public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverClusterTest { + + private String userId; + + @Page + protected LoginPage loginPage; + + @Page + protected LoginPasswordUpdatePage updatePasswordPage; + + + @Page + protected LoginUpdateProfilePage updateProfilePage; + + @Page + protected AppPage appPage; + + + @Before + public void setup() { + try { + adminClient.realm("test").remove(); + } catch (Exception ignore) { + } + + RealmRepresentation testRealm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + adminClient.realms().create(testRealm); + + UserRepresentation user = UserBuilder.create() + .username("login-test") + .email("login@test.com") + .enabled(true) + .requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString()) + .requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString()) + .build(); + + userId = ApiUtil.createUserAndResetPasswordWithAdminClient(adminClient.realm("test"), user, "password"); + getCleanup().addUserId(userId); + + oauth.clientId("test-app"); + } + + @After + public void after() { + adminClient.realm("test").remove(); + } + + + @Test + public void failoverDuringAuthentication() throws Exception { + + boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= 2; + + log.info("AUTHENTICATION FAILOVER TEST: cluster size = " + getClusterSize() + ", session-cache owners = " + SESSION_CACHE_OWNERS + + " --> Testsing for " + (expectSuccessfulFailover ? "" : "UN") + "SUCCESSFUL session failover."); + + assertEquals(2, getClusterSize()); + + failoverTest(expectSuccessfulFailover); + } + + + protected void failoverTest(boolean expectSuccessfulFailover) throws IOException, MessagingException { + loginPage.open(); + + String cookieValue1 = getAuthSessionCookieValue(); + + // Login and assert on "updatePassword" page + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Route didn't change + Assert.assertEquals(cookieValue1, getAuthSessionCookieValue()); + + log.info("Authentication session cookie: " + cookieValue1); + + setCurrentFailNodeForRoute(cookieValue1); + + failure(); + pause(REBALANCE_WAIT); + logFailoverSetup(); + + // Trigger the action now + updatePasswordPage.changePassword("password", "password"); + + if (expectSuccessfulFailover) { + //Action was successful + updateProfilePage.assertCurrent(); + + String cookieValue2 = getAuthSessionCookieValue(); + + log.info("Authentication session cookie after failover: " + cookieValue2); + + // Cookie was moved to the second node + Assert.assertEquals(cookieValue1.substring(0, 36), cookieValue2.substring(0, 36)); + Assert.assertNotEquals(cookieValue1, cookieValue2); + + } else { + loginPage.assertCurrent(); + String error = loginPage.getError(); + log.info("Failover not successful as expected. Error on login page: " + error); + Assert.assertNotNull(error); + + loginPage.login("login-test", "password"); + updatePasswordPage.changePassword("password", "password"); + } + + + updateProfilePage.assertCurrent(); + + // Successfully update profile and assert user logged + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + private String getAuthSessionCookieValue() { + Cookie authSessionCookie = driver.manage().getCookieNamed(AuthenticationSessionManager.AUTH_SESSION_ID); + Assert.assertNotNull(authSessionCookie); + return authSessionCookie.getValue(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java index ca9417982e..74d9776d2a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cluster/SessionFailoverClusterTest.java @@ -2,38 +2,16 @@ package org.keycloak.testsuite.cluster; import org.junit.Before; import org.junit.Test; -import org.keycloak.representations.idm.RealmRepresentation; -import org.keycloak.testsuite.page.AbstractPage; -import org.keycloak.testsuite.page.PageWithLogOutAction; import org.openqa.selenium.Cookie; -import java.util.List; - import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN; -import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; import static org.keycloak.testsuite.util.WaitUtils.pause; /** * * @author tkyjovsk */ -public class SessionFailoverClusterTest extends AbstractClusterTest { - - public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION"; - - public static final Integer SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("session.cache.owners", "1")); - public static final Integer OFFLINE_SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("offline.session.cache.owners", "1")); - public static final Integer LOGIN_FAILURES_CACHE_OWNERS = Integer.parseInt(System.getProperty("login.failure.cache.owners", "1")); - - public static final Integer REBALANCE_WAIT = Integer.parseInt(System.getProperty("rebalance.wait", "5000")); - - @Override - public void addTestRealms(List testRealms) { - } +public class SessionFailoverClusterTest extends AbstractFailoverClusterTest { @Before public void beforeSessionFailover() { @@ -45,7 +23,7 @@ public class SessionFailoverClusterTest extends AbstractClusterTest { @Test public void sessionFailover() { - boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= getClusterSize(); + boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= 2; log.info("SESSION FAILOVER TEST: cluster size = " + getClusterSize() + ", session-cache owners = " + SESSION_CACHE_OWNERS + " --> Testsing for " + (expectSuccessfulFailover ? "" : "UN") + "SUCCESSFUL session failover."); @@ -91,64 +69,4 @@ public class SessionFailoverClusterTest extends AbstractClusterTest { } - /** - * failure --> failback --> failure of next node - */ - protected void switchFailedNode() { - assertFalse(controller.isStarted(getCurrentFailNode().getQualifier())); - - failback(); - pause(REBALANCE_WAIT); - - iterateCurrentFailNode(); - - failure(); - pause(REBALANCE_WAIT); - - assertFalse(controller.isStarted(getCurrentFailNode().getQualifier())); - } - - protected Cookie login(AbstractPage targetPage) { - targetPage.navigateTo(); - assertCurrentUrlStartsWith(loginPage); - loginPage.form().login(ADMIN, ADMIN); - assertCurrentUrlStartsWith(targetPage); - Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); - assertNotNull(sessionCookie); - return sessionCookie; - } - - protected void logout(AbstractPage targetPage) { - if (!(targetPage instanceof PageWithLogOutAction)) { - throw new IllegalArgumentException(targetPage.getClass().getSimpleName() + " must implement PageWithLogOutAction interface"); - } - targetPage.navigateTo(); - assertCurrentUrlStartsWith(targetPage); - ((PageWithLogOutAction) targetPage).logOut(); - } - - protected Cookie verifyLoggedIn(AbstractPage targetPage, Cookie sessionCookieForVerification) { - // verify on realm path - masterRealmPage.navigateTo(); - Cookie sessionCookieOnRealmPath = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); - assertNotNull(sessionCookieOnRealmPath); - assertEquals(sessionCookieOnRealmPath.getValue(), sessionCookieForVerification.getValue()); - // verify on target page - targetPage.navigateTo(); - assertCurrentUrlStartsWith(targetPage); - Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); - assertNotNull(sessionCookie); - assertEquals(sessionCookie.getValue(), sessionCookieForVerification.getValue()); - return sessionCookie; - } - - protected void verifyLoggedOut(AbstractPage targetPage) { - // verify on target page - targetPage.navigateTo(); - driver.navigate().refresh(); - assertCurrentUrlStartsWith(loginPage); - Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE); - assertNull(sessionCookie); - } - } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java index abda821e1d..37b9d72059 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java @@ -40,6 +40,7 @@ import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.client.params.AuthPolicy; import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.impl.client.DefaultHttpClient; import org.ietf.jgss.GSSCredential; import org.jboss.arquillian.graphene.page.Page; @@ -347,7 +348,10 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest { cleanupApacheHttpClient(); } - DefaultHttpClient httpClient = (DefaultHttpClient) new HttpClientBuilder().build(); + DefaultHttpClient httpClient = (DefaultHttpClient) new HttpClientBuilder() + .disableCookieCache(false) + .build(); + httpClient.getAuthSchemes().register(AuthPolicy.SPNEGO, spnegoSchemeFactory); if (useSpnego) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java index 65e5a3e52e..2d6d3afb5f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java @@ -17,18 +17,23 @@ package org.keycloak.testsuite.federation.kerberos; +import java.net.URI; +import java.net.URL; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.common.constants.KerberosConstants; +import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.KerberosConfig; @@ -148,15 +153,24 @@ public class KerberosStandaloneTest extends AbstractKerberosTest { Response spnegoResponse = spnegoLogin("hnelson", "secret"); String context = spnegoResponse.readEntity(String.class); spnegoResponse.close(); + + Assert.assertTrue(context.contains("Log in to test")); + Pattern pattern = Pattern.compile("action=\"([^\"]+)\""); Matcher m = pattern.matcher(context); Assert.assertTrue(m.find()); String url = m.group(1); - driver.navigate().to(url); - Assert.assertTrue(loginPage.isCurrent()); - loginPage.login("test-user@localhost", "password"); - String pageSource = driver.getPageSource(); - assertAuthenticationSuccess(driver.getCurrentUrl()); + + + // Follow login with HttpClient. Improve if needed + MultivaluedMap params = new javax.ws.rs.core.MultivaluedHashMap<>(); + params.putSingle("username", "test-user@localhost"); + params.putSingle("password", "password"); + Response response = client.target(url).request() + .post(Entity.form(params)); + + URI redirectUri = response.getLocation(); + assertAuthenticationSuccess(redirectUri.toString()); events.clear(); testRealmResource().components().add(kerberosProvider); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java new file mode 100644 index 0000000000..08d826522e --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/BrowserButtonsTest.java @@ -0,0 +1,376 @@ +/* + * Copyright 2016 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.testsuite.forms; + +import java.io.IOException; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.pages.LoginExpiredPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginPasswordResetPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.LoginUpdateProfilePage; +import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.pages.RegisterPage; +import org.keycloak.testsuite.pages.VerifyEmailPage; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.UserBuilder; + +import static org.junit.Assert.assertEquals; + +/** + * Test for browser back/forward/refresh buttons + * + * @author Marek Posolda + */ +public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest { + + private String userId; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Before + public void setup() { + UserRepresentation user = UserBuilder.create() + .username("login-test") + .email("login@test.com") + .enabled(true) + .requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString()) + .requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString()) + .build(); + + userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + expectedMessagesCount = 0; + getCleanup().addUserId(userId); + + oauth.clientId("test-app"); + } + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Page + protected ErrorPage errorPage; + + @Page + protected InfoPage infoPage; + + @Page + protected VerifyEmailPage verifyEmailPage; + + @Page + protected LoginPasswordResetPage resetPasswordPage; + + @Page + protected LoginPasswordUpdatePage updatePasswordPage; + + @Page + protected LoginUpdateProfilePage updateProfilePage; + + @Page + protected LoginExpiredPage loginExpiredPage; + + @Page + protected RegisterPage registerPage; + + @Page + protected OAuthGrantPage grantPage; + + @Rule + public AssertEvents events = new AssertEvents(this); + + private int expectedMessagesCount; + + + // KEYCLOAK-4670 - Flow 1 + @Test + public void invalidLoginAndBackButton() throws IOException, MessagingException { + loginPage.open(); + + loginPage.login("login-test2", "invalid"); + loginPage.assertCurrent(); + + loginPage.login("login-test3", "invalid"); + loginPage.assertCurrent(); + + // Click browser back. Should be still on login page (TODO: Retest with real browsers like FF or Chrome. Maybe they need some additional actions to confirm re-sending POST request ) + driver.navigate().back(); + loginPage.assertCurrent(); + + // Click browser refresh. Should be still on login page + driver.navigate().refresh(); + loginPage.assertCurrent(); + } + + + // KEYCLOAK-4670 - Flow 2 + @Test + public void requiredActionsBackForwardTest() throws IOException, MessagingException { + loginPage.open(); + + // Login and assert on "updatePassword" page + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Update password and assert on "updateProfile" page + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.assertCurrent(); + + // Click browser back. Assert on "Page expired" page + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click browser forward. Assert on "updateProfile" page again + driver.navigate().forward(); + updateProfilePage.assertCurrent(); + + + // Successfully update profile and assert user logged + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + + // KEYCLOAK-4670 - Flow 3 extended + @Test + public void requiredActionsBackAndRefreshTest() throws IOException, MessagingException { + loginPage.open(); + + // Login and assert on "updatePassword" page + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Click browser refresh. Assert still on updatePassword page + driver.navigate().refresh(); + updatePasswordPage.assertCurrent(); + + // Update password and assert on "updateProfile" page + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.assertCurrent(); + + // Click browser back. Assert on "Page expired" page + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click browser refresh. Assert still on "Page expired" page + driver.navigate().refresh(); + loginExpiredPage.assertCurrent(); + + // Click "login restart" and assert on loginPage + loginExpiredPage.clickLoginRestartLink(); + loginPage.assertCurrent(); + + // Login again and assert on "updateProfile" page + loginPage.login("login-test", "password"); + updateProfilePage.assertCurrent(); + + // Click browser back. Assert on "Page expired" page + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click "login continue" and assert on updateProfile page + loginExpiredPage.clickLoginContinueLink(); + updateProfilePage.assertCurrent(); + + // Successfully update profile and assert user logged + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + + // KEYCLOAK-4670 - Flow 4 + @Test + public void consentRefresh() { + oauth.clientId("third-party"); + + // Login and go through required actions + loginPage.open(); + loginPage.login("login-test", "password"); + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + + // Assert on consent screen + grantPage.assertCurrent(); + + // Click browser back. Assert on "page expired" + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click continue login. Assert on consent screen again + loginExpiredPage.clickLoginContinueLink(); + grantPage.assertCurrent(); + + // Click refresh. Assert still on consent screen + driver.navigate().refresh(); + grantPage.assertCurrent(); + + // Confirm consent. Assert authenticated + grantPage.accept(); + appPage.assertCurrent(); + } + + + // KEYCLOAK-4670 - Flow 5 + @Test + public void clickBackButtonAfterReturnFromRegister() throws Exception { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + // Click "Back to login" link on registerPage + registerPage.clickBackToLogin(); + loginPage.assertCurrent(); + + // Click browser "back" button. Should be back on register page + driver.navigate().back(); + registerPage.assertCurrent(); + } + + @Test + public void clickBackButtonFromRegisterPage() { + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + // Click browser "back" button. Should be back on login page + driver.navigate().back(); + loginPage.assertCurrent(); + } + + + @Test + public void backButtonToAuthorizationEndpoint() { + loginPage.open(); + + // Login and assert on "updatePassword" page + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Click browser back. I should be on 'page expired' . URL corresponds to OIDC AuthorizationEndpoint + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click 'restart' link. I should be on login page + loginExpiredPage.clickLoginRestartLink(); + loginPage.assertCurrent(); + } + + + @Test + public void backButtonInResetPasswordFlow() throws Exception { + // Click on "forgot password" and type username + loginPage.open(); + loginPage.resetPassword(); + + resetPasswordPage.assertCurrent(); + + resetPasswordPage.changePassword("login-test"); + + loginPage.assertCurrent(); + assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); + + // Receive email + MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; + + String changePasswordUrl = ResetPasswordTest.getPasswordResetEmailLink(message); + + driver.navigate().to(changePasswordUrl.trim()); + + updatePasswordPage.assertCurrent(); + + // Click browser back. Should be on 'page expired' + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click 'continue' should be on updatePasswordPage + loginExpiredPage.clickLoginContinueLink(); + updatePasswordPage.assertCurrent(); + + // Click browser back. Should be on 'page expired' + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click 'restart' . Should be on login page + loginExpiredPage.clickLoginRestartLink(); + loginPage.assertCurrent(); + + } + + + @Test + public void appInitiatedRegistrationWithBackButton() throws Exception { + // Send request from the application directly to 'registrations' + String appInitiatedRegisterUrl = oauth.getLoginFormUrl(); + appInitiatedRegisterUrl = appInitiatedRegisterUrl.replace("openid-connect/auth", "openid-connect/registrations"); // Should be done better way... + driver.navigate().to(appInitiatedRegisterUrl); + registerPage.assertCurrent(); + + + // Click 'back to login' + registerPage.clickBackToLogin(); + loginPage.assertCurrent(); + + // Login + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Click browser back. Should be on 'page expired' + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click 'continue' should be on updatePasswordPage + loginExpiredPage.clickLoginContinueLink(); + updatePasswordPage.assertCurrent(); + + // Click browser back. Should be on 'page expired' + driver.navigate().back(); + loginExpiredPage.assertCurrent(); + + // Click 'restart' . Check that I was put to the registration page as flow was initiated as registration flow + loginExpiredPage.clickLoginRestartLink(); + registerPage.assertCurrent(); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 63ed003dcb..07006771b3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -23,21 +23,26 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.events.EventType; import org.keycloak.models.BrowserSecurityHeaders; +import org.keycloak.models.Constants; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; +import org.openqa.selenium.NoSuchElementException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; @@ -47,6 +52,7 @@ import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; /** @@ -395,18 +401,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); } - @Test - public void loginTimeout() { - loginPage.open(); - setTimeOffset(1850); - - loginPage.login("login-test", "password"); - - setTimeOffset(0); - - events.expectLogin().clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).user((String) null).session((String) null).error("expired_code").assertEvent().getSessionId(); - } @Test public void loginLoginHint() { @@ -555,11 +550,33 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { } } + + // Login timeout scenarios + // KEYCLOAK-1037 @Test public void loginExpiredCode() { loginPage.open(); setTimeOffset(5000); + // No explicitly call "removeExpired". Hence authSession will still exists, but will be expired + //testingClient.testing().removeExpired("test"); + + loginPage.login("login@test.com", "password"); + loginPage.assertCurrent(); + + Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); + setTimeOffset(0); + + events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() + .assertEvent(); + } + + // KEYCLOAK-1037 + @Test + public void loginExpiredCodeWithExplicitRemoveExpired() { + loginPage.open(); + setTimeOffset(5000); + // Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART testingClient.testing().removeExpired("test"); loginPage.login("login@test.com", "password"); @@ -567,13 +584,68 @@ public class LoginTest extends AbstractTestRealmKeycloakTest { //loginPage.assertCurrent(); loginPage.assertCurrent(); - //Assert.assertEquals("Login timeout. Please login again.", loginPage.getError()); + Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); setTimeOffset(0); - events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails() + events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails() .detail(Details.RESTART_AFTER_TIMEOUT, "true") .client((String) null) .assertEvent(); } + + @Test + public void loginExpiredCodeAndExpiredCookies() { + loginPage.open(); + + driver.manage().deleteAllCookies(); + + // Cookies are expired including KC_RESTART. No way to continue login. Error page must be shown + loginPage.login("login@test.com", "password"); + errorPage.assertCurrent(); + } + + + + @Test + public void openLoginFormWithDifferentApplication() throws Exception { + // Login form shown after redirect from admin console + oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID); + oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console"); + oauth.openLoginForm(); + + // Login form shown after redirect from app + oauth.clientId("test-app"); + oauth.redirectUri(OAuthClient.APP_ROOT + "/auth"); + oauth.openLoginForm(); + + assertTrue(loginPage.isCurrent()); + loginPage.login("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + + @Test + public void openLoginFormAfterExpiredCode() throws Exception { + oauth.openLoginForm(); + + setTimeOffset(5000); + + oauth.openLoginForm(); + + loginPage.assertCurrent(); + try { + String loginError = loginPage.getError(); + Assert.fail("Not expected to have error on loginForm. Error is: " + loginError); + } catch (NoSuchElementException nsee) { + // Expected + } + + loginPage.login("test-user@localhost", "password"); + appPage.assertCurrent(); + + events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java index db66debfc1..7fc3f743d0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/LogoutTest.java @@ -126,7 +126,7 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest { // Check session 1 not logged-in oauth.openLoginForm(); - assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl()); + loginPage.assertCurrent(); // Login session 3 oauth.doLogin("test-user@localhost", "password"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java new file mode 100644 index 0000000000..24a70fd6f8 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/MultipleTabsLoginTest.java @@ -0,0 +1,290 @@ +/* + * Copyright 2016 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.testsuite.forms; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.models.Constants; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.pages.LoginExpiredPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginPasswordResetPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.LoginUpdateProfilePage; +import org.keycloak.testsuite.pages.OAuthGrantPage; +import org.keycloak.testsuite.pages.RegisterPage; +import org.keycloak.testsuite.pages.VerifyEmailPage; +import org.keycloak.testsuite.util.GreenMailRule; +import org.keycloak.testsuite.util.UserBuilder; + +/** + * Tries to simulate testing with multiple browser tabs + * + * @author Marek Posolda + */ +public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest { + + private String userId; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Before + public void setup() { + UserRepresentation user = UserBuilder.create() + .username("login-test") + .email("login@test.com") + .enabled(true) + .requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString()) + .requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString()) + .build(); + + userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + getCleanup().addUserId(userId); + + oauth.clientId("test-app"); + } + + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + + @Page + protected AppPage appPage; + + @Page + protected LoginPage loginPage; + + @Page + protected ErrorPage errorPage; + + @Page + protected InfoPage infoPage; + + @Page + protected VerifyEmailPage verifyEmailPage; + + @Page + protected LoginPasswordResetPage resetPasswordPage; + + @Page + protected LoginPasswordUpdatePage updatePasswordPage; + + @Page + protected LoginUpdateProfilePage updateProfilePage; + + @Page + protected LoginExpiredPage loginExpiredPage; + + @Page + protected RegisterPage registerPage; + + @Page + protected OAuthGrantPage grantPage; + + @Rule + public AssertEvents events = new AssertEvents(this); + + + // Test for scenario when user is logged into JS application in 2 browser tabs. Then click "logout" in tab1 and he is logged-out from both tabs (tab2 is logged-out automatically due to session iframe few seconds later) + // Now both browser tabs show the 1st login screen and we need to make sure that actionURL (code with execution) is valid on both tabs, so user won't have error page when he tries to login from tab1 + @Test + public void openMultipleTabs() { + oauth.openLoginForm(); + loginPage.assertCurrent(); + String actionUrl1 = getActionUrl(driver.getPageSource()); + + oauth.openLoginForm(); + loginPage.assertCurrent(); + String actionUrl2 = getActionUrl(driver.getPageSource()); + + Assert.assertEquals(actionUrl1, actionUrl2); + + } + + + private String getActionUrl(String pageSource) { + return pageSource.split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); + } + + + @Test + public void multipleTabsParallelLoginTest() { + oauth.openLoginForm(); + loginPage.assertCurrent(); + + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + String tab1Url = driver.getCurrentUrl(); + + // Simulate login in different browser tab tab2. I will be on loginPage again. + oauth.openLoginForm(); + loginPage.assertCurrent(); + + // Login in tab2 + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + + // Try to go back to tab 1. We should have ALREADY_LOGGED_IN info page + driver.navigate().to(tab1Url); + infoPage.assertCurrent(); + Assert.assertEquals("You are already logged in.", infoPage.getInfo()); + + infoPage.clickBackToApplicationLink(); + appPage.assertCurrent(); + } + + + @Test + public void expiredAuthenticationAction_currentCodeExpiredExecution() { + // Simulate to open login form in 2 tabs + oauth.openLoginForm(); + loginPage.assertCurrent(); + String actionUrl1 = getActionUrl(driver.getPageSource()); + + // Click "register" in tab2 + loginPage.clickRegister(); + registerPage.assertCurrent(); + + // Simulate going back to tab1 and confirm login form. Page "showExpired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...) + driver.navigate().to(actionUrl1); + loginExpiredPage.assertCurrent(); + + // Click on continue and assert I am on "register" form + loginExpiredPage.clickLoginContinueLink(); + registerPage.assertCurrent(); + + // Finally click "Back to login" and authenticate + registerPage.clickBackToLogin(); + loginPage.assertCurrent(); + + // Login success now + loginPage.login("login-test", "password"); + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + + @Test + public void expiredAuthenticationAction_expiredCodeCurrentExecution() { + // Simulate to open login form in 2 tabs + oauth.openLoginForm(); + loginPage.assertCurrent(); + String actionUrl1 = getActionUrl(driver.getPageSource()); + + loginPage.login("invalid", "invalid"); + loginPage.assertCurrent(); + Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + // Simulate going back to tab1 and confirm login form. Login page with "action expired" message should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...) + driver.navigate().to(actionUrl1); + loginPage.assertCurrent(); + Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError()); + + // Login success now + loginPage.login("login-test", "password"); + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + + @Test + public void expiredAuthenticationAction_expiredCodeExpiredExecution() { + // Open tab1 + oauth.openLoginForm(); + loginPage.assertCurrent(); + String actionUrl1 = getActionUrl(driver.getPageSource()); + + // Authenticate in tab2 + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Simulate going back to tab1 and confirm login form. Page "Page expired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...) + driver.navigate().to(actionUrl1); + loginExpiredPage.assertCurrent(); + + // Finish login + loginExpiredPage.clickLoginContinueLink(); + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + + @Test + public void loginActionWithoutExecution() throws Exception { + oauth.openLoginForm(); + + // Manually remove execution from the URL and try to simulate the request just with "code" parameter + String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); + actionUrl = actionUrl.replaceFirst("&execution=.*", ""); + + driver.navigate().to(actionUrl); + + loginExpiredPage.assertCurrent(); + } + + + // Same like "loginActionWithoutExecution", but AuthenticationSession is in REQUIRED_ACTIONS action + @Test + public void loginActionWithoutExecutionInRequiredActions() throws Exception { + oauth.openLoginForm(); + loginPage.assertCurrent(); + + loginPage.login("login-test", "password"); + updatePasswordPage.assertCurrent(); + + // Manually remove execution from the URL and try to simulate the request just with "code" parameter + String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); + actionUrl = actionUrl.replaceFirst("&execution=.*", ""); + + driver.navigate().to(actionUrl); + + // Back on updatePasswordPage now + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("password", "password"); + updateProfilePage.update("John", "Doe3", "john@doe3.com"); + appPage.assertCurrent(); + } + + + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index 1a68d09b5e..346bbd7f43 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -21,18 +21,17 @@ import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; -import org.keycloak.testsuite.pages.AccountUpdateProfilePage; -import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.*; import org.keycloak.testsuite.pages.AppPage.RequestType; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.RegisterPage; -import org.keycloak.testsuite.util.RealmBuilder; -import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.testsuite.util.*; +import javax.mail.internet.MimeMessage; import static org.jgroups.util.Util.assertTrue; import static org.junit.Assert.assertEquals; @@ -54,9 +53,15 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { @Page protected RegisterPage registerPage; + @Page + protected VerifyEmailPage verifyEmailPage; + @Page protected AccountUpdateProfilePage accountPage; + @Rule + public GreenMailRule greenMail = new GreenMailRule(); + @Override public void configureTestRealm(RealmRepresentation testRealm) { } @@ -295,10 +300,15 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { registerPage.register("firstName", "lastName", "registerUserSuccess@email", "registerUserSuccess", "password", "password"); + appPage.assertCurrent(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); String userId = events.expectRegister("registerUserSuccess", "registerUserSuccess@email").assertEvent().getUserId(); - events.expectLogin().detail("username", "registerusersuccess").user(userId).assertEvent(); + assertUserRegistered(userId, "registerusersuccess", "registerusersuccess@email"); + } + + private void assertUserRegistered(String userId, String username, String email) { + events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent(); UserRepresentation user = getUser(userId); Assert.assertNotNull(user); @@ -306,12 +316,121 @@ public class RegisterTest extends AbstractTestRealmKeycloakTest { // test that timestamp is current with 10s tollerance Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000); // test user info is set from form - assertEquals("registerusersuccess", user.getUsername()); - assertEquals("registerusersuccess@email", user.getEmail()); + assertEquals(username.toLowerCase(), user.getUsername()); + assertEquals(email.toLowerCase(), user.getEmail()); assertEquals("firstName", user.getFirstName()); assertEquals("lastName", user.getLastName()); } + @Test + public void registerUserSuccessWithEmailVerification() throws Exception { + RealmRepresentation realm = testRealm().toRepresentation(); + boolean origVerifyEmail = realm.isVerifyEmail(); + + try { + realm.setVerifyEmail(true); + testRealm().update(realm); + + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerification@email", "registerUserSuccessWithEmailVerification", "password", "password"); + verifyEmailPage.assertCurrent(); + + String userId = events.expectRegister("registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email").assertEvent().getUserId(); + + { + assertTrue("Expecting verify email", greenMail.waitForIncomingEmail(1000, 1)); + + events.expect(EventType.SEND_VERIFY_EMAIL) + .detail(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .user(userId) + .assertEvent(); + + MimeMessage message = greenMail.getLastReceivedMessage(); + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + } + + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .detail(Details.EMAIL, "registerUserSuccessWithEmailVerification@email".toLowerCase()) + .user(userId) + .assertEvent(); + + assertUserRegistered(userId, "registerUserSuccessWithEmailVerification", "registerUserSuccessWithEmailVerification@email"); + + appPage.assertCurrent(); + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // test that timestamp is current with 10s tollerance + // test user info is set from form + } finally { + realm.setVerifyEmail(origVerifyEmail); + testRealm().update(realm); + } + } + + @Test + public void registerUserSuccessWithEmailVerificationWithResend() throws Exception { + RealmRepresentation realm = testRealm().toRepresentation(); + boolean origVerifyEmail = realm.isVerifyEmail(); + try { + realm.setVerifyEmail(true); + testRealm().update(realm); + + loginPage.open(); + loginPage.clickRegister(); + registerPage.assertCurrent(); + + registerPage.register("firstName", "lastName", "registerUserSuccessWithEmailVerificationWithResend@email", "registerUserSuccessWithEmailVerificationWithResend", "password", "password"); + verifyEmailPage.assertCurrent(); + + String userId = events.expectRegister("registerUserSuccessWithEmailVerificationWithResend", "registerUserSuccessWithEmailVerificationWithResend@email").assertEvent().getUserId(); + + { + assertTrue("Expecting verify email", greenMail.waitForIncomingEmail(1000, 1)); + + events.expect(EventType.SEND_VERIFY_EMAIL) + .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase()) + .user(userId) + .assertEvent(); + + verifyEmailPage.clickResendEmail(); + verifyEmailPage.assertCurrent(); + + assertTrue("Expecting second verify email", greenMail.waitForIncomingEmail(1000, 1)); + + events.expect(EventType.SEND_VERIFY_EMAIL) + .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase()) + .user(userId) + .assertEvent(); + + MimeMessage message = greenMail.getLastReceivedMessage(); + String link = MailUtils.getPasswordResetEmailLink(message); + + driver.navigate().to(link); + } + + events.expectRequiredAction(EventType.VERIFY_EMAIL) + .detail(Details.EMAIL, "registerUserSuccessWithEmailVerificationWithResend@email".toLowerCase()) + .user(userId) + .assertEvent(); + + assertUserRegistered(userId, "registerUserSuccessWithEmailVerificationWithResend", "registerUserSuccessWithEmailVerificationWithResend@email"); + + appPage.assertCurrent(); + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // test that timestamp is current with 10s tollerance + // test user info is set from form + } finally { + realm.setVerifyEmail(origVerifyEmail); + testRealm().update(realm); + } + } + @Test public void registerUserUmlats() { loginPage.open(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java index 3a12a76133..04ee91117f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java @@ -16,10 +16,8 @@ */ package org.keycloak.testsuite.forms; +import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken; import org.jboss.arquillian.graphene.page.Page; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventType; @@ -50,8 +48,8 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import org.junit.*; +import static org.junit.Assert.*; /** * @author Stian Thorgersen @@ -74,6 +72,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { .build(); userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password"); + expectedMessagesCount = 0; getCleanup().addUserId(userId); } @@ -104,6 +103,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"; @@ -138,15 +139,15 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.changePassword("resetPassword", "resetPassword"); - String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD) - .detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/") + events.expectRequiredAction(EventType.UPDATE_PASSWORD) + .detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/") .client("account") - .user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId(); + .user(userId).detail(Details.USERNAME, username).assertEvent(); - events.expectLogin().user(userId).detail(Details.USERNAME, username) + String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, username) .detail(Details.REDIRECT_URI, oauth.AUTH_SERVER_ROOT + "/realms/test/account/") .client("account") - .session(sessionId).assertEvent(); + .assertEvent().getSessionId(); oauth.openLogout(); @@ -167,6 +168,40 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { resetPassword("login-test"); } + @Test + public void resetPasswordTwice() throws IOException, MessagingException { + String changePasswordUrl = resetPassword("login-test"); + events.clear(); + + assertSecondPasswordResetFails(changePasswordUrl, null); // KC_RESTART doesn't exists, it was deleted after first successful reset-password flow was finished + } + + @Test + public void resetPasswordTwiceInNewBrowser() throws IOException, MessagingException { + String changePasswordUrl = resetPassword("login-test"); + events.clear(); + + String resetUri = oauth.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials"; + driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path + driver.manage().deleteAllCookies(); + + assertSecondPasswordResetFails(changePasswordUrl, null); + } + + public void assertSecondPasswordResetFails(String changePasswordUrl, String clientId) { + driver.navigate().to(changePasswordUrl.trim()); + + errorPage.assertCurrent(); + assertEquals("Action expired. Please continue with login now.", errorPage.getError()); + + events.expect(EventType.RESET_PASSWORD) + .client("account") + .session((String) null) + .user(userId) + .error(Errors.EXPIRED_CODE) + .assertEvent(); + } + @Test public void resetPasswordWithSpacesInUsername() throws IOException, MessagingException { resetPassword(" login-test "); @@ -174,15 +209,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 +233,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,50 +247,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { .session((String)null) .assertEvent(); - assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - String changePasswordUrl = getPasswordResetEmailLink(message); - - driver.navigate().to(changePasswordUrl.trim()); - - updatePasswordPage.assertCurrent(); - - updatePasswordPage.changePassword("resetPassword", "resetPassword"); - - String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId(); - - assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).session(sessionId).assertEvent(); - - oauth.openLogout(); - - events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent(); - - loginPage.open(); - - loginPage.login("login-test", "resetPassword"); - - events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent(); - - 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(); + assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length); MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; @@ -279,58 +259,72 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.changePassword(password, password); - String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId) - .detail(Details.USERNAME, username).assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, username.trim()).assertEvent(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).detail(Details.USERNAME, username).assertEvent(); + String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, username.trim()).assertEvent().getSessionId(); oauth.openLogout(); events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent(); + + loginPage.open(); + + loginPage.login("login-test", password); + + sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + 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(); + 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).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(); + assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1]; String changePasswordUrl = getPasswordResetEmailLink(message); driver.navigate().to(changePasswordUrl.trim()); + updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword(password, password); - assertTrue(updatePasswordPage.isCurrent()); + updatePasswordPage.assertCurrent(); assertEquals(error, updatePasswordPage.getError()); 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 { + private 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); @@ -358,27 +352,19 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { @Test public void resetPasswordExpiredCode() throws IOException, MessagingException, InterruptedException { + initiateResetPasswordFromResetPasswordPage("login-test"); + + events.expectRequiredAction(EventType.SEND_RESET_PASSWORD) + .session((String)null) + .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); + + assertEquals(1, greenMail.getReceivedMessages().length); + + MimeMessage message = greenMail.getReceivedMessages()[0]; + + String changePasswordUrl = getPasswordResetEmailLink(message); + try { - loginPage.open(); - 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) - .session((String)null) - .user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent(); - - assertEquals(1, greenMail.getReceivedMessages().length); - - MimeMessage message = greenMail.getReceivedMessages()[0]; - - String changePasswordUrl = getPasswordResetEmailLink(message); - setTimeOffset(1800 + 23); driver.navigate().to(changePasswordUrl.trim()); @@ -387,7 +373,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); - events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent(); + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent(); } finally { setTimeOffset(0); } @@ -398,20 +384,12 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { final AtomicInteger originalValue = new AtomicInteger(); RealmRepresentation realmRep = testRealm().toRepresentation(); - originalValue.set(realmRep.getAccessCodeLifespan()); - realmRep.setAccessCodeLifespanUserAction(60); + originalValue.set(realmRep.getActionTokenGeneratedByUserLifespan()); + realmRep.setActionTokenGeneratedByUserLifespan(60); 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) @@ -431,58 +409,53 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); - events.expectRequiredAction(EventType.RESET_PASSWORD).error("expired_code").client("test-app").user((String) null).session((String) null).clearDetails().assertEvent(); + events.expectRequiredAction(EventType.EXECUTE_ACTION_TOKEN_ERROR).error("expired_code").client((String) null).user(userId).session((String) null).clearDetails().detail(Details.ACTION, ResetCredentialsActionToken.TOKEN_TYPE).assertEvent(); } finally { setTimeOffset(0); + + realmRep.setActionTokenGeneratedByUserLifespan(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 +469,31 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { RealmRepresentation realmRep = testRealm().toRepresentation(); Map 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 +506,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); @@ -561,11 +528,11 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { updatePasswordPage.changePassword("resetPasswordWithPasswordPolicy", "resetPasswordWithPasswordPolicy"); - String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").session(sessionId).assertEvent(); + String sessionId = events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId(); oauth.openLogout(); @@ -581,7 +548,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { } @Test - public void resetPasswordWithPasswordHisoryPolicy() throws IOException, MessagingException { + public void resetPasswordWithPasswordHistoryPolicy() throws IOException, MessagingException { //Block passwords that are equal to previous passwords. Default value is 3. setPasswordPolicy("passwordHistory"); @@ -597,13 +564,14 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords."); resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords."); - setTimeOffset(8000000); + setTimeOffset(6000000); resetPassword("login-test", "password3"); resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords."); resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords."); resetPasswordInvalidPassword("login-test", "password3", "Invalid password: must not be equal to any of last 3 passwords."); + setTimeOffset(8000000); resetPassword("login-test", "password"); } finally { setTimeOffset(0); @@ -620,6 +588,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { resetPasswordPage.changePassword(username); + log.info("Should be at login page again."); loginPage.assertCurrent(); assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage()); @@ -638,16 +607,20 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest { String changePasswordUrl = getPasswordResetEmailLink(message); + log.debug("Going to reset password URI."); + driver.navigate().to(resetUri); // This is necessary to delete KC_RESTART cookie that is restricted to /auth/realms/test path + log.debug("Removing cookies."); driver.manage().deleteAllCookies(); + log.debug("Going to URI from e-mail."); driver.navigate().to(changePasswordUrl.trim()); - System.out.println(driver.getPageSource()); +// System.out.println(driver.getPageSource()); updatePasswordPage.assertCurrent(); updatePasswordPage.changePassword("resetPassword", "resetPassword"); - assertTrue(infoPage.isCurrent()); + infoPage.assertCurrent(); assertEquals("Your account has been updated.", infoPage.getInfo()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java index 22f75da2dd..90b8049713 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/SSOTest.java @@ -23,9 +23,12 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.models.UserModel; import org.keycloak.representations.IDToken; import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.drone.Different; @@ -33,6 +36,7 @@ import org.keycloak.testsuite.pages.AccountUpdateProfilePage; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.util.OAuthClient; import org.openqa.selenium.WebDriver; @@ -59,6 +63,9 @@ public class SSOTest extends AbstractTestRealmKeycloakTest { @Page protected AccountUpdateProfilePage profilePage; + @Page + protected LoginPasswordUpdatePage updatePasswordPage; + @Rule public AssertEvents events = new AssertEvents(this); @@ -109,6 +116,7 @@ public class SSOTest extends AbstractTestRealmKeycloakTest { events.clear(); } + @Test public void multipleSessions() { loginPage.open(); @@ -124,7 +132,6 @@ public class SSOTest extends AbstractTestRealmKeycloakTest { OAuthClient oauth2 = new OAuthClient(); oauth2.init(adminClient, driver2); - oauth2.state("mystate"); oauth2.doLogin("test-user@localhost", "password"); EventRepresentation login2 = events.expectLogin().assertEvent(); @@ -158,4 +165,38 @@ public class SSOTest extends AbstractTestRealmKeycloakTest { } } + + @Test + public void loginWithRequiredActionAddedInTheMeantime() { + // SSO login + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); + + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + String sessionId = loginEvent.getSessionId(); + + // Add update-profile required action to user now + UserRepresentation user = testRealm().users().get(loginEvent.getUserId()).toRepresentation(); + user.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PASSWORD.toString()); + testRealm().users().get(loginEvent.getUserId()).update(user); + + // Attempt SSO login. update-password form is shown + oauth.openLoginForm(); + updatePasswordPage.assertCurrent(); + + updatePasswordPage.changePassword("password", "password"); + events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent(); + + assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + loginEvent = events.expectLogin().removeDetail(Details.USERNAME).client("test-app").assertEvent(); + String sessionId2 = loginEvent.getSessionId(); + assertEquals(sessionId, sessionId2); + + + } + } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 92e68cb046..ecf540cc30 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -210,12 +210,7 @@ public class AccessTokenTest extends AbstractKeycloakTest { oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console/nosuch.html"); oauth.openLoginForm(); - String actionUrl = driver.getPageSource().split("action=\"")[1].split("\"")[0].replaceAll("&", "&"); - actionUrl = actionUrl.replaceFirst("&execution=.*", ""); - - String loginPageCode = actionUrl.split("code=")[1].split("&")[0]; - - driver.navigate().to(actionUrl); + String loginPageCode = driver.getPageSource().split("code=")[1].split("&")[0].split("\"")[0]; oauth.fillLoginForm("test-user@localhost", "password"); @@ -452,7 +447,7 @@ public class AccessTokenTest extends AbstractKeycloakTest { Assert.assertEquals(400, response.getStatusCode()); EventRepresentation event = events.poll(); - assertNotNull(event.getDetails().get(Details.CODE_ID)); + assertNull(event.getDetails().get(Details.CODE_ID)); UserManager.realm(adminClient.realm("test")).user(user).removeRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java index 729f0c5626..757799046c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AuthorizationCodeTest.java @@ -16,8 +16,6 @@ */ package org.keycloak.testsuite.oauth; -import org.jboss.arquillian.graphene.page.Page; -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -31,7 +29,6 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.openqa.selenium.By; @@ -61,11 +58,12 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { public void clientConfiguration() { oauth.responseType(OAuth2Constants.CODE); oauth.responseMode(null); + oauth.stateParamRandom(); } @Test public void authorizationRequest() throws IOException { - oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); @@ -100,8 +98,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { public void authorizationValidRedirectUri() throws IOException { ClientManager.realm(adminClient.realm("test")).clientId("test-app").addRedirectUris(oauth.getRedirectUri()); - oauth.state("mystate"); - OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertTrue(response.isRedirected()); @@ -113,7 +109,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { @Test public void authorizationRequestNoState() throws IOException { - oauth.state(null); + oauth.stateParamHardcoded(null); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); @@ -143,7 +139,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { @Test public void authorizationRequestFormPostResponseMode() throws IOException { oauth.responseMode(OIDCResponseMode.FORM_POST.toString().toLowerCase()); - oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk"); + oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk"); oauth.doLoginGrant("test-user@localhost", "password"); String sources = driver.getPageSource(); @@ -159,7 +155,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest { } private void assertCode(String expectedCodeId, String actualCode) { - assertEquals(expectedCodeId, actualCode.split("\\.")[1]); + assertEquals(expectedCodeId, actualCode.split("\\.")[2]); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java index e244f9a379..c5304ff61c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java @@ -16,8 +16,8 @@ */ package org.keycloak.testsuite.oauth; +import org.hamcrest.Matchers; import org.jboss.arquillian.graphene.page.Page; -import org.junit.After; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -155,6 +155,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest { .client(THIRD_PARTY_APP) .error("rejected_by_user") .removeDetail(Details.CONSENT) + .session(Matchers.nullValue(String.class)) .assertEvent(); } @@ -309,6 +310,7 @@ public class OAuthGrantTest extends AbstractKeycloakTest { .client(THIRD_PARTY_APP) .error("rejected_by_user") .removeDetail(Details.CONSENT) + .session(Matchers.nullValue(String.class)) .assertEvent(); oauth.scope("foo-role third-party/bar-role"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java index de83d4e18a..efaf4bdc5a 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java @@ -36,6 +36,7 @@ import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientManager; @@ -204,6 +205,8 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT .removeDetail(Details.CONSENT) .assertEvent(); + Assert.assertTrue(login.equals(accessToken.getPreferredUsername()) || login.equals(accessToken.getEmail())); + assertEquals(accessToken.getSessionState(), refreshToken.getSessionState()); OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 57a2102351..03522d66fd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -304,6 +304,31 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest } + + @Test + public void promptLoginDifferentUser() throws Exception { + String sss = oauth.getLoginFormUrl(); + System.out.println(sss); + + // Login user + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); + IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); + + // Assert need to re-authenticate with prompt=login + driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login"); + + // Authenticate as different user + loginPage.assertCurrent(); + loginPage.login("john-doh@localhost", "password"); + + errorPage.assertCurrent(); + Assert.assertTrue(errorPage.getError().startsWith("You are already authenticated as different user")); + } + // DISPLAY & OTHERS @Test @@ -324,6 +349,8 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest @Test public void requestParamUnsigned() throws Exception { + oauth.stateParamHardcoded("mystate2"); + String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); @@ -344,12 +371,14 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest oauth.request(requestStr); OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); - Assert.assertEquals("mystate", response.getState()); + Assert.assertEquals("mystate2", response.getState()); assertTrue(appPage.isCurrent()); } @Test public void requestUriParamUnsigned() throws Exception { + oauth.stateParamHardcoded("mystate1"); + String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); @@ -367,12 +396,14 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); - Assert.assertEquals("mystate", response.getState()); + Assert.assertEquals("mystate1", response.getState()); assertTrue(appPage.isCurrent()); } @Test public void requestUriParamSigned() throws Exception { + oauth.stateParamHardcoded("mystate3"); + String validRedirectUri = oauth.getRedirectUri(); TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints(); @@ -412,7 +443,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Check signed request_uri will pass OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password"); Assert.assertNotNull(response.getCode()); - Assert.assertEquals("mystate", response.getState()); + Assert.assertEquals("mystate3", response.getState()); assertTrue(appPage.isCurrent()); // Revert requiring signature for client diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index c6da264a53..d0388777c1 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -108,8 +108,9 @@ "connectionsInfinispan": { "default": { "clustered": "${keycloak.connectionsInfinispan.clustered:false}", - "async": "${keycloak.connectionsInfinispan.async:true}", - "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}", + "async": "${keycloak.connectionsInfinispan.async:false}", + "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}", + "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}", "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}", "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}", "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}" diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json index b20eb5da1f..ef6f1053f7 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/admin-test/testrealm.json @@ -9,6 +9,8 @@ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], "defaultRoles": [ "user" ], + "actionTokenGeneratedByAdminLifespan": "147", + "actionTokenGeneratedByUserLifespan": "258", "smtpServer": { "from": "auto@keycloak.org", "host": "localhost", diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index e01a4d5fb4..6bc040f683 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -53,6 +53,7 @@ localhost org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow ${auth.server.http.port} + ${undertow.remote} @@ -90,7 +91,7 @@ ${auth.server.backend1.home} standalone-ha.xml - -Djboss.socket.binding.port-offset=${auth.server.backend1.port.offset} + -Djboss.socket.binding.port-offset=${auth.server.backend1.port.offset} -Djboss.node.name=node1 ${adapter.test.props} ${auth.server.profile} @@ -127,6 +128,43 @@ + + + + + ${auth.server.undertow.cluster} + org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow + localhost + ${auth.server.http.port} + 1 + node1 + ${undertow.remote} + + + + + ${auth.server.undertow.cluster} + org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow + localhost + ${auth.server.http.port} + 2 + node2 + ${undertow.remote} + + + + + + ${auth.server.undertow.cluster} + org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer + localhost + ${auth.server.http.port} + node1=http://localhost:8181,node2=http://localhost:8182 + + + + + ${auth.server.cluster} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js index 07a07a13c6..0fd70d518c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/scripts/client-session-test.js @@ -12,7 +12,7 @@ function authenticate(context) { return; } - if (clientSession.getAuthMethod() != "${authMethod}") { + if (clientSession.getProtocol() != "${authMethod}") { context.failure(AuthenticationFlowError.INVALID_CLIENT_SESSION); return; } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java index 0ebc095802..8c38363db5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java @@ -27,6 +27,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import org.jboss.resteasy.spi.ResteasyDeployment; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; @@ -37,12 +38,15 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.testsuite.util.cli.TestsuiteCLI; import org.keycloak.util.JsonSerialization; +import org.mvel2.util.Make; import javax.servlet.DispatcherType; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; /** @@ -187,6 +191,8 @@ public class KeycloakServer { config.setWorkerThreads(undertowWorkerThreads); } + detectNodeName(config); + final KeycloakServer keycloak = new KeycloakServer(config); keycloak.sysout = true; keycloak.start(); @@ -369,4 +375,24 @@ public class KeycloakServer { return new File(s.toString()); } + + private static void detectNodeName(KeycloakServerConfig config) { + String nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME); + if (nodeName == null) { + // Try to autodetect "jboss.node.name" from the port + Map nodesCfg = new HashMap<>(); + nodesCfg.put(8181, "node1"); + nodesCfg.put(8182, "node2"); + + nodeName = nodesCfg.get(config.getPort()); + if (nodeName != null) { + System.setProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME, nodeName); + } + } + + if (nodeName != null) { + log.infof("Node name: %s", nodeName); + } + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index 46d62b83c2..2da679ec3d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -35,6 +35,7 @@ import org.keycloak.common.util.PemUtils; import org.keycloak.constants.AdapterConstants; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; @@ -69,7 +70,9 @@ public class OAuthClient { private String redirectUri = "http://localhost:8081/app/auth"; - private String state = "mystate"; + private StateParamProvider state = () -> { + return KeycloakModelUtils.generateId(); + }; private String scope; @@ -438,7 +441,7 @@ public class OAuthClient { b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri); } if (state != null) { - b.queryParam(OAuth2Constants.STATE, state); + b.queryParam(OAuth2Constants.STATE, state.getState()); } if(uiLocales != null){ b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales); @@ -509,8 +512,17 @@ public class OAuthClient { return this; } - public OAuthClient state(String state) { - this.state = state; + public OAuthClient stateParamHardcoded(String value) { + this.state = () -> { + return value; + }; + return this; + } + + public OAuthClient stateParamRandom() { + this.state = () -> { + return KeycloakModelUtils.generateId(); + }; return this; } @@ -639,4 +651,10 @@ public class OAuthClient { } } + private interface StateParamProvider { + + String getState(); + + } + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index 474417c218..acf775c313 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -33,12 +33,16 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.pages.IdpConfirmLinkPage; import org.keycloak.testsuite.pages.IdpLinkEmailPage; +import org.keycloak.testsuite.pages.InfoPage; +import org.keycloak.testsuite.pages.LoginExpiredPage; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import javax.mail.internet.MimeMessage; @@ -48,6 +52,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -71,6 +77,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi @WebResource protected LoginPasswordUpdatePage passwordUpdatePage; + @WebResource + protected LoginExpiredPage loginExpiredPage; + /** @@ -291,6 +300,145 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi Assert.assertTrue(user.isEmailVerified()); } + /** + * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email + */ + @Test + public void testLinkAccountByEmailVerificationTwice() throws Exception { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Confirm linking account by email + this.idpLinkEmailPage.assertCurrent(); + Assert.assertThat( + this.idpLinkEmailPage.getMessage(), + is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.") + ); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String linkFromMail = getVerificationEmailLink(message); + + driver.navigate().to(linkFromMail.trim()); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + // Assert user's email is verified now + UserModel user = getFederatedUser(); + Assert.assertTrue(user.isEmailVerified()); + + // Attempt to use the link for the second time + driver.navigate().to(linkFromMail.trim()); + + infoPage.assertCurrent(); + Assert.assertThat(infoPage.getInfo(), is("You are already logged in.")); + + // Log out + driver.navigate().to("http://localhost:8081/test-app/logout"); + + // Go to the same link again + driver.navigate().to(linkFromMail.trim()); + + infoPage.assertCurrent(); + Assert.assertThat(infoPage.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login.")); + } + + /** + * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email + */ + @Test + public void testLinkAccountByEmailVerificationDifferentBrowser() throws Exception, Throwable { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Confirm linking account by email + this.idpLinkEmailPage.assertCurrent(); + Assert.assertThat( + this.idpLinkEmailPage.getMessage(), + is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.") + ); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String linkFromMail = getVerificationEmailLink(message); + + WebRule webRule2 = new WebRule(this); + try { + webRule2.initProperties(); + + WebDriver driver2 = webRule2.getDriver(); + InfoPage infoPage2 = webRule2.getPage(InfoPage.class); + + driver2.navigate().to(linkFromMail.trim()); + + // authenticated, but not redirected to app. Just seeing info page. + infoPage2.assertCurrent(); + Assert.assertThat(infoPage2.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login.")); + } finally { + // Revert everything + webRule2.after(); + } + + this.idpLinkEmailPage.clickContinueFlowLink(); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + // Assert user's email is verified now + UserModel user = getFederatedUser(); + Assert.assertTrue(user.isEmailVerified()); + } + + @Test + public void testLinkAccountByEmailVerificationResendEmail() throws Exception, Throwable { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Confirm linking account by email + this.idpLinkEmailPage.assertCurrent(); + Assert.assertThat( + this.idpLinkEmailPage.getMessage(), + is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.") + ); + + this.idpLinkEmailPage.clickResendEmail(); + + this.idpLinkEmailPage.assertCurrent(); + Assert.assertThat( + this.idpLinkEmailPage.getMessage(), + is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.") + ); + + Assert.assertEquals(2, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String linkFromMail = getVerificationEmailLink(message); + + driver.navigate().to(linkFromMail.trim()); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + // Assert user's email is verified now + UserModel user = getFederatedUser(); + Assert.assertTrue(user.isEmailVerified()); + } + /** * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen) @@ -360,6 +508,101 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi } + /** + * Variation of previous test, which uses browser buttons (back, refresh etc) + */ + @Test + public void testLinkAccountByReauthenticationWithPassword_browserButtons() throws Exception { + // Remove smtp config. The reauthentication by username+password screen will be automatically used then + final Map smtpConfig = new HashMap<>(); + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + smtpConfig.putAll(realmWithBroker.getSmtpConfig()); + realmWithBroker.setSmtpConfig(Collections.emptyMap()); + } + + }, APP_REALM_ID); + + + // Use invalid username for the first time + loginIDP("foo"); + assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + this.loginPage.login("pedroigor", "password"); + + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + + // Click browser 'back' and then 'forward' and then continue + driver.navigate().back(); + Assert.assertTrue(driver.getPageSource().contains("You are already logged in.")); + driver.navigate().forward(); + this.loginExpiredPage.assertCurrent(); + this.loginExpiredPage.clickLoginContinueLink(); + this.idpConfirmLinkPage.assertCurrent(); + + // Click browser 'back' on review profile page + this.idpConfirmLinkPage.clickReviewProfile(); + this.updateProfilePage.assertCurrent(); + driver.navigate().back(); + this.loginExpiredPage.assertCurrent(); + this.loginExpiredPage.clickLoginContinueLink(); + this.updateProfilePage.assertCurrent(); + this.updateProfilePage.update("Pedro", "Igor", "psilva@redhat.com"); + + this.idpConfirmLinkPage.assertCurrent(); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Login screen shown. Username is prefilled and disabled. Registration link and social buttons are not shown + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + Assert.assertEquals("pedroigor", this.loginPage.getUsername()); + Assert.assertFalse(this.loginPage.isUsernameInputEnabled()); + + Assert.assertEquals("Authenticate as pedroigor to link your account with " + getProviderId(), this.loginPage.getInfoMessage()); + + try { + this.loginPage.findSocialButton(getProviderId()); + Assert.fail("Not expected to see social button with " + getProviderId()); + } catch (NoSuchElementException expected) { + } + + try { + this.loginPage.clickRegister(); + Assert.fail("Not expected to see register link"); + } catch (NoSuchElementException expected) { + } + + // Use bad password first + this.loginPage.login("password1"); + Assert.assertEquals("Invalid username or password.", this.loginPage.getError()); + + // Click browser 'back' and then continue + this.driver.navigate().back(); + this.loginExpiredPage.assertCurrent(); + this.loginExpiredPage.clickLoginContinueLink(); + + // Use correct password now + this.loginPage.login("password"); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + + // Restore smtp config + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + realmWithBroker.setSmtpConfig(smtpConfig); + } + + }, APP_REALM_ID); + } + + /** * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen) * and additionally he goes through "forget password" @@ -418,6 +661,96 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi } + /** + * Same like above, but "forget password" link is opened in different browser + */ + @Test + public void testLinkAccountByReauthentication_forgetPassword_differentBrowser() throws Throwable { + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED); + + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + } + + }, APP_REALM_ID); + + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Click "Forget password" on login page. Email sent directly because username is known + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + this.loginPage.resetPassword(); + + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + Assert.assertEquals("You should receive an email shortly with further instructions.", this.loginPage.getSuccessMessage()); + + // Click on link from email + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String linkFromMail = getVerificationEmailLink(message); + + // Simulate 2nd browser + WebRule webRule2 = new WebRule(this); + try { + webRule2.initProperties(); + + WebDriver driver2 = webRule2.getDriver(); + LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class); + InfoPage infoPage2 = webRule2.getPage(InfoPage.class); + + driver2.navigate().to(linkFromMail.trim()); + + // Need to update password now + passwordUpdatePage2.assertCurrent(); + passwordUpdatePage2.changePassword("password", "password"); + + // authenticated, but not redirected to app. Just seeing info page. + infoPage2.assertCurrent(); + Assert.assertEquals("Your account has been updated.", infoPage2.getInfo()); + } finally { + // Revert everything + webRule2.after(); + } + + // User is not yet linked with identity provider. He needs to authenticate again in 1st browser + RealmModel realmWithBroker = getRealm(); + Set federatedIdentities = this.session.users().getFederatedIdentities(this.session.users().getUserByUsername("pedroigor", realmWithBroker), realmWithBroker); + assertEquals(0, federatedIdentities.size()); + + // Continue with 1st browser. Note that the user has already authenticated with brokered IdP in the beginning of this test + // so entering their credentials there is now skipped. + loginToIDPWhenAlreadyLoggedIntoProviderIdP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + this.loginPage.login("password"); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE); + + } + + }, APP_REALM_ID); + } + + protected void assertFederatedUser(String expectedUsername, String expectedEmail, String expectedFederatedUsername) { assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); UserModel federatedUser = getFederatedUser(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index 8efc8c0ba3..297d00a55b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -37,14 +37,7 @@ import org.keycloak.testsuite.MailUtil; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.broker.util.UserSessionStatusServlet; import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus; -import org.keycloak.testsuite.pages.AccountFederatedIdentityPage; -import org.keycloak.testsuite.pages.AccountPasswordPage; -import org.keycloak.testsuite.pages.AccountUpdateProfilePage; -import org.keycloak.testsuite.pages.ErrorPage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.LoginUpdateProfilePage; -import org.keycloak.testsuite.pages.OAuthGrantPage; -import org.keycloak.testsuite.pages.VerifyEmailPage; +import org.keycloak.testsuite.pages.*; import org.keycloak.testsuite.rule.GreenMailRule; import org.keycloak.testsuite.rule.LoggingRule; import org.keycloak.testsuite.rule.WebResource; @@ -61,9 +54,8 @@ import java.net.URI; import java.util.List; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.*; /** * @author pedroigor @@ -115,6 +107,9 @@ public abstract class AbstractIdentityProviderTest { @WebResource protected ErrorPage errorPage; + @WebResource + protected InfoPage infoPage; + protected KeycloakSession session; protected int logoutTimeOffset = 0; @@ -210,18 +205,29 @@ public abstract class AbstractIdentityProviderTest { protected void loginIDP(String username) { driver.navigate().to("http://localhost:8081/test-app"); - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); // choose the identity provider this.loginPage.clickSocial(getProviderId()); String currentUrl = this.driver.getCurrentUrl(); - assertTrue(currentUrl.startsWith("http://localhost:8082/auth/")); + assertThat(currentUrl, startsWith("http://localhost:8082/auth/")); // log in to identity provider this.loginPage.login(username, "password"); doAfterProviderAuthentication(); } + protected void loginToIDPWhenAlreadyLoggedIntoProviderIdP(String username) { + driver.navigate().to("http://localhost:8081/test-app"); + + assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + // choose the identity provider + this.loginPage.clickSocial(getProviderId()); + + doAfterProviderAuthentication(); + } + protected UserModel getFederatedUser() { UserSessionStatus userSessionStatus = retrieveSessionStatus(); IDToken idToken = userSessionStatus.getIdToken(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java index b047595f5d..781714aa76 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java @@ -69,7 +69,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent } @Test - public void testDisabledUser() { + public void testDisabledUser() throws Exception { KeycloakSession session = brokerServerRule.startSession(); setUpdateProfileFirstLogin(session.realms().getRealmByName("realm-with-broker"), IdentityProviderRepresentation.UPFLM_OFF); brokerServerRule.stopSession(session, true); @@ -328,7 +328,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent } @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception { RealmModel realm = getRealm(); realm.setRegistrationEmailAsUsername(true); setUpdateProfileFirstLogin(realm, IdentityProviderRepresentation.UPFLM_OFF); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java index 58c4ca18f3..c8e9d9bb37 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java @@ -116,7 +116,7 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP } @Test - public void testDisabledUser() { + public void testDisabledUser() throws Exception { super.testDisabledUser(); } @@ -156,7 +156,7 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP } @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception { super.testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java index 078666fb43..b2ecd4145b 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java @@ -117,11 +117,9 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro grantPage.assertCurrent(); grantPage.cancel(); - // Assert error page with backToApplication link displayed - errorPage.assertCurrent(); - errorPage.clickBackToApplication(); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + // Assert login page with "You took too long to login..." message + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/login-actions/authenticate")); + Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); } finally { Time.setOffset(0); @@ -143,6 +141,7 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro this.session = brokerServerRule.startSession(); session.sessions().removeExpired(getRealm()); + session.authenticationSessions().removeExpired(getRealm()); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); @@ -151,14 +150,9 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro grantPage.assertCurrent(); grantPage.cancel(); - // Assert error page without backToApplication link (clientSession expired and was removed on the server) - errorPage.assertCurrent(); - try { - errorPage.clickBackToApplication(); - fail("Not expected to have link backToApplication available"); - } catch (NoSuchElementException nsee) { - // Expected; - } + // Assert login page with "You took too long to login..." message + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/login-actions/authenticate")); + Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError()); } finally { Time.setOffset(0); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java index 200b0a7c60..234617b482 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java @@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.KeycloakServer; import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.junit.Test; /** * @author Marek Posolda diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java index 3b83f03955..8afc49b692 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java @@ -128,7 +128,7 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP } @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception { super.testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername(); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java index f4cbc2283b..ba47fca448 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPTestConfiguration.java @@ -38,10 +38,11 @@ public class LDAPTestConfiguration { private String connectionPropertiesLocation; private int sleepTime; - private boolean startEmbeddedLdapLerver = true; + private boolean startEmbeddedLdapServer = true; private Map config; protected static final Map PROP_MAPPINGS = new HashMap(); + protected static final Map DEFAULT_VALUES = new HashMap(); static { @@ -124,9 +125,10 @@ public class LDAPTestConfiguration { config.put(propertyName, value); } - startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty("idm.test.ldap.start.embedded.ldap.server", "true")); + startEmbeddedLdapServer = Boolean.parseBoolean(p.getProperty("idm.test.ldap.start.embedded.ldap.server", "true")); sleepTime = Integer.parseInt(p.getProperty("idm.test.ldap.sleepTime", "1000")); - log.info("Start embedded server: " + startEmbeddedLdapLerver); + config.put("startEmbeddedLdapServer", Boolean.toString(startEmbeddedLdapServer)); + log.info("Start embedded server: " + startEmbeddedLdapServer); log.info("Read config: " + config); } @@ -138,8 +140,8 @@ public class LDAPTestConfiguration { this.connectionPropertiesLocation = connectionPropertiesLocation; } - public boolean isStartEmbeddedLdapLerver() { - return startEmbeddedLdapLerver; + public boolean isStartEmbeddedLdapServer() { + return startEmbeddedLdapServer; } public int getSleepTime() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java index 39b93fb590..086bf257ed 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/storage/ldap/LDAPLegacyImportTest.java @@ -61,8 +61,7 @@ public class LDAPLegacyImportTest { // This test is executed just for the embedded LDAP server private static LDAPRule ldapRule = new LDAPRule((Map ldapConfig) -> { - String connectionURL = ldapConfig.get(LDAPConstants.CONNECTION_URL); - return !"ldap://localhost:10389".equals(connectionURL); + return Boolean.parseBoolean(ldapConfig.get("startEmbeddedLdapServer")); }); private static ComponentModel ldapModel = null; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java new file mode 100644 index 0000000000..db08e810b9 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java @@ -0,0 +1,281 @@ +/* + * Copyright 2016 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.testsuite.model; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserManager; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.ClientManager; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.testsuite.rule.KeycloakRule; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * @author Marek Posolda + */ +public class AuthenticationSessionProviderTest { + + @ClassRule + public static KeycloakRule kc = new KeycloakRule(); + + private KeycloakSession session; + private RealmModel realm; + + @Before + public void before() { + session = kc.startSession(); + realm = session.realms().getRealm("test"); + session.users().addUser(realm, "user1").setEmail("user1@localhost"); + session.users().addUser(realm, "user2").setEmail("user2@localhost"); + } + + @After + public void after() { + resetSession(); + UserModel user1 = session.users().getUserByUsername("user1", realm); + UserModel user2 = session.users().getUserByUsername("user2", realm); + + UserManager um = new UserManager(session); + if (user1 != null) { + um.removeUser(realm, user1); + } + if (user2 != null) { + um.removeUser(realm, user2); + } + kc.stopSession(session, true); + } + + private void resetSession() { + kc.stopSession(session, true); + session = kc.startSession(); + realm = session.realms().getRealm("test"); + } + + @Test + public void testLoginSessionsCRUD() { + ClientModel client1 = realm.getClientByClientId("test-app"); + UserModel user1 = session.users().getUserByUsername("user1", realm); + + AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1); + + authSession.setAction("foo"); + authSession.setTimestamp(100); + + resetSession(); + + // Ensure session is here + authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); + testAuthenticationSession(authSession, client1.getId(), null, "foo"); + Assert.assertEquals(100, authSession.getTimestamp()); + + // Update and commit + authSession.setAction("foo-updated"); + authSession.setTimestamp(200); + authSession.setAuthenticatedUser(session.users().getUserByUsername("user1", realm)); + + resetSession(); + + // Ensure session was updated + authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); + testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated"); + Assert.assertEquals(200, authSession.getTimestamp()); + + // Remove and commit + session.authenticationSessions().removeAuthenticationSession(realm, authSession); + + resetSession(); + + // Ensure session was removed + Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSession.getId())); + + } + + @Test + public void testAuthenticationSessionRestart() { + ClientModel client1 = realm.getClientByClientId("test-app"); + UserModel user1 = session.users().getUserByUsername("user1", realm); + + AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1); + + authSession.setAction("foo"); + authSession.setTimestamp(100); + + authSession.setAuthenticatedUser(user1); + authSession.setAuthNote("foo", "bar"); + authSession.setClientNote("foo2", "bar2"); + authSession.setExecutionStatus("123", CommonClientSessionModel.ExecutionStatus.SUCCESS); + + resetSession(); + + client1 = realm.getClientByClientId("test-app"); + authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); + authSession.restartSession(realm, client1); + + resetSession(); + + authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); + testAuthenticationSession(authSession, client1.getId(), null, null); + Assert.assertTrue(authSession.getTimestamp() > 0); + + Assert.assertTrue(authSession.getClientNotes().isEmpty()); + Assert.assertNull(authSession.getAuthNote("foo2")); + Assert.assertTrue(authSession.getExecutionStatus().isEmpty()); + + } + + + @Test + public void testExpiredAuthSessions() { + try { + realm.setAccessCodeLifespan(10); + realm.setAccessCodeLifespanUserAction(10); + realm.setAccessCodeLifespanLogin(30); + + // Login lifespan is largest + String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + resetSession(); + + Time.setOffset(25); + session.authenticationSessions().removeExpired(realm); + resetSession(); + + assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + + Time.setOffset(35); + session.authenticationSessions().removeExpired(realm); + resetSession(); + + assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + + // User action is largest + realm.setAccessCodeLifespanUserAction(40); + + Time.setOffset(0); + authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + resetSession(); + + Time.setOffset(35); + session.authenticationSessions().removeExpired(realm); + resetSession(); + + assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + + Time.setOffset(45); + session.authenticationSessions().removeExpired(realm); + resetSession(); + + assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + + // Access code is largest + realm.setAccessCodeLifespan(50); + + Time.setOffset(0); + authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + resetSession(); + + Time.setOffset(45); + session.authenticationSessions().removeExpired(realm); + resetSession(); + + assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + + Time.setOffset(55); + session.authenticationSessions().removeExpired(realm); + resetSession(); + + assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + } finally { + Time.setOffset(0); + + realm.setAccessCodeLifespan(60); + realm.setAccessCodeLifespanUserAction(300); + realm.setAccessCodeLifespanLogin(1800); + + } + } + + + @Test + public void testOnRealmRemoved() { + RealmModel fooRealm = session.realms().createRealm("foo-realm"); + ClientModel fooClient = fooRealm.addClient("foo-client"); + + String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + String authSessionId2 = session.authenticationSessions().createAuthenticationSession(fooRealm, fooClient).getId(); + + resetSession(); + + new RealmManager(session).removeRealm(session.realms().getRealmByName("foo-realm")); + + resetSession(); + + AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId); + testAuthenticationSession(authSession, realm.getClientByClientId("test-app").getId(), null, null); + Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2)); + } + + @Test + public void testOnClientRemoved() { + String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + String authSessionId2 = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("third-party")).getId(); + + String testAppClientUUID = realm.getClientByClientId("test-app").getId(); + + resetSession(); + + new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party")); + + resetSession(); + + AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId); + testAuthenticationSession(authSession, testAppClientUUID, null, null); + Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2)); + + // Revert client + realm.addClient("third-party"); + } + + + private void testAuthenticationSession(AuthenticationSessionModel authSession, String expectedClientId, String expectedUserId, String expectedAction) { + Assert.assertEquals(expectedClientId, authSession.getClient().getId()); + + if (expectedUserId == null) { + Assert.assertNull(authSession.getAuthenticatedUser()); + } else { + Assert.assertEquals(expectedUserId, authSession.getAuthenticatedUser().getId()); + } + + if (expectedAction == null) { + Assert.assertNull(authSession.getAction()); + } else { + Assert.assertEquals(expectedAction, authSession.getAction()); + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java index abbf9122c5..68746c0014 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/CacheTest.java @@ -103,7 +103,7 @@ public class CacheTest { user.setFirstName("firstName"); user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); - UserSessionModel userSession = session.sessions().createUserSession(realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null); + UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null); UserModel user2 = userSession.getUser(); user.setLastName("lastName"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java index 7b6de1bb5c..66894f63ba 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClusterSessionCleanerTest.java @@ -75,7 +75,7 @@ public class ClusterSessionCleanerTest { RealmModel realm1 = session1.realms().getRealmByName(REALM_NAME); UserModel user1 = session1.users().getUserByUsername("test-user@localhost", realm1); for (int i=0 ; i<15 ; i++) { - session1.sessions().createUserSession(realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null); + session1.sessions().createUserSession("123", realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null); } session1 = commit(server1, session1); @@ -87,7 +87,7 @@ public class ClusterSessionCleanerTest { Assert.assertEquals(user2.getId(), user1.getId()); for (int i=0 ; i<15 ; i++) { - session2.sessions().createUserSession(realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null); + session2.sessions().createUserSession("456", realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null); } session2 = commit(server2, session2); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java index 2db24addca..8c01e2ceaf 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java @@ -25,14 +25,15 @@ import org.junit.Test; import org.keycloak.cluster.ClusterProvider; import org.keycloak.common.util.Time; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionProvider; import org.keycloak.models.UserSessionProviderFactory; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.models.UserManager; import org.keycloak.services.managers.UserSessionManager; @@ -47,6 +48,7 @@ import java.util.Set; */ public class UserSessionInitializerTest { + @ClassRule public static KeycloakRule kc = new KeycloakRule(); @@ -88,7 +90,7 @@ public class UserSessionInitializerTest { for (UserSessionModel origSession : origSessions) { UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId()); - for (ClientSessionModel clientSession : userSession.getClientSessions()) { + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { sessionManager.createOrUpdateOfflineSession(clientSession, userSession); } } @@ -128,8 +130,8 @@ public class UserSessionInitializerTest { UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, serverStartTime, "test-app"); } - private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); if (userSession != null) clientSession.setUserSession(userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); @@ -140,7 +142,7 @@ public class UserSessionInitializerTest { private UserSessionModel[] createSessions() { UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); Set roles = new HashSet(); roles.add("one"); @@ -153,10 +155,10 @@ public class UserSessionInitializerTest { createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); resetSession(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java index 60d92d9891..8c89046eb2 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java @@ -23,13 +23,14 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; @@ -46,6 +47,7 @@ import java.util.Set; */ public class UserSessionPersisterProviderTest { + @ClassRule public static KeycloakRule kc = new KeycloakRule(); @@ -151,7 +153,7 @@ public class UserSessionPersisterProviderTest { int clientSessionsCount = 0; for (UserSessionModel loadedSession : loadedSessions) { Assert.assertEquals(expectedTime, loadedSession.getLastSessionRefresh()); - for (ClientSessionModel clientSession : loadedSession.getClientSessions()) { + for (AuthenticatedClientSessionModel clientSession : loadedSession.getAuthenticatedClientSessions().values()) { Assert.assertEquals(expectedTime, clientSession.getTimestamp()); clientSessionsCount++; } @@ -183,11 +185,11 @@ public class UserSessionPersisterProviderTest { try { persistedSession.setLastSessionRefresh(Time.currentTime()); persistedSession.setNote("foo", "bar"); - persistedSession.setState(UserSessionModel.State.LOGGING_IN); + persistedSession.setState(UserSessionModel.State.LOGGED_IN); persister.updateUserSession(persistedSession, true); // create new clientSession - ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()), + AuthenticatedClientSessionModel clientSession = createClientSession(realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()), "http://redirect", "state", new HashSet(), new HashSet()); persister.createClientSession(clientSession, true); @@ -198,10 +200,10 @@ public class UserSessionPersisterProviderTest { persistedSession = loadedSessions.get(0); UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app", "third-party"); Assert.assertEquals("bar", persistedSession.getNote("foo")); - Assert.assertEquals(UserSessionModel.State.LOGGING_IN, persistedSession.getState()); + Assert.assertEquals(UserSessionModel.State.LOGGED_IN, persistedSession.getState()); // Remove clientSession - persister.removeClientSession(clientSession.getId(), true); + persister.removeClientSession(userSession.getId(), realm.getClientByClientId("third-party").getId(), true); resetSession(); @@ -228,7 +230,7 @@ public class UserSessionPersisterProviderTest { fooRealm.addClient("foo-app"); session.users().addUser(fooRealm, "user3"); - UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); resetSession(); @@ -262,7 +264,7 @@ public class UserSessionPersisterProviderTest { fooRealm.addClient("bar-app"); session.users().addUser(fooRealm, "user3"); - UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); @@ -358,8 +360,8 @@ public class UserSessionPersisterProviderTest { } - private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); if (userSession != null) clientSession.setUserSession(userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); @@ -370,7 +372,7 @@ public class UserSessionPersisterProviderTest { private UserSessionModel[] createSessions() { UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); Set roles = new HashSet(); roles.add("one"); @@ -383,10 +385,10 @@ public class UserSessionPersisterProviderTest { createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); return sessions; @@ -394,7 +396,7 @@ public class UserSessionPersisterProviderTest { private void persistUserSession(UserSessionModel userSession, boolean offline) { persister.createUserSession(userSession, offline); - for (ClientSessionModel clientSession : userSession.getClientSessions()) { + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { persister.createClientSession(clientSession, offline); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java index fb4b3af81b..106f5258c7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java @@ -24,13 +24,14 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; @@ -41,7 +42,6 @@ import org.keycloak.testsuite.rule.LoggingRule; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -94,29 +94,23 @@ public class UserSessionProviderOfflineTest { resetSession(); - Map offlineSessions = new HashMap<>(); + // Key is userSession ID, values are client UUIDS + Map> offlineSessions = new HashMap<>(); // Persist 3 created userSessions and clientSessions as offline ClientModel testApp = realm.getClientByClientId("test-app"); List userSessions = session.sessions().getUserSessions(realm, testApp); for (UserSessionModel userSession : userSessions) { - offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession)); + offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(userSession)); } resetSession(); // Assert all previously saved offline sessions found - for (Map.Entry entry : offlineSessions.entrySet()) { - Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null); - - UserSessionModel offlineSession = session.sessions().getUserSession(realm, entry.getValue()); - boolean found = false; - for (ClientSessionModel clientSession : offlineSession.getClientSessions()) { - if (clientSession.getId().equals(entry.getKey())) { - found = true; - } - } - Assert.assertTrue(found); + for (Map.Entry> entry : offlineSessions.entrySet()) { + UserSessionModel offlineSession = sessionManager.findOfflineUserSession(realm, entry.getKey()); + Assert.assertNotNull(offlineSession); + Assert.assertEquals(offlineSession.getAuthenticatedClientSessions().keySet(), entry.getValue()); } // Find clients with offline token @@ -174,23 +168,23 @@ public class UserSessionProviderOfflineTest { fooRealm.addClient("foo-app"); session.users().addUser(fooRealm, "user3"); - UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); - ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + AuthenticatedClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); resetSession(); // Persist offline session fooRealm = session.realms().getRealm("foo"); userSession = session.sessions().getUserSession(fooRealm, userSession.getId()); - clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId()); - sessionManager.createOrUpdateOfflineSession(userSession.getClientSessions().get(0), userSession); + createOfflineSessionIncludeClientSessions(userSession); resetSession(); - ClientSessionModel offlineClientSession = sessionManager.findOfflineClientSession(fooRealm, clientSession.getId()); + UserSessionModel offlineUserSession = sessionManager.findOfflineUserSession(fooRealm, userSession.getId()); + Assert.assertEquals(offlineUserSession.getAuthenticatedClientSessions().size(), 1); + AuthenticatedClientSessionModel offlineClientSession = offlineUserSession.getAuthenticatedClientSessions().values().iterator().next(); Assert.assertEquals("foo-app", offlineClientSession.getClient().getClientId()); Assert.assertEquals("user3", offlineClientSession.getUserSession().getUser().getUsername()); - Assert.assertEquals(offlineClientSession.getId(), offlineClientSession.getUserSession().getClientSessions().get(0).getId()); // Remove realm RealmManager realmMgr = new RealmManager(session); @@ -206,7 +200,7 @@ public class UserSessionProviderOfflineTest { // Assert nothing loaded fooRealm = session.realms().getRealm("foo"); - Assert.assertNull(sessionManager.findOfflineClientSession(fooRealm, clientSession.getId())); + Assert.assertNull(sessionManager.findOfflineUserSession(fooRealm, userSession.getId())); Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(fooRealm, fooRealm.getClientByClientId("foo-app"))); // Cleanup @@ -223,7 +217,7 @@ public class UserSessionProviderOfflineTest { fooRealm.addClient("bar-app"); session.users().addUser(fooRealm, "user3"); - UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); @@ -256,8 +250,8 @@ public class UserSessionProviderOfflineTest { // Assert just one bar-app clientSession persisted now offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId()); - Assert.assertEquals(1, offlineSession.getClientSessions().size()); - Assert.assertEquals("bar-app", offlineSession.getClientSessions().get(0).getClient().getClientId()); + Assert.assertEquals(1, offlineSession.getAuthenticatedClientSessions().size()); + Assert.assertEquals("bar-app", offlineSession.getAuthenticatedClientSessions().values().iterator().next().getClient().getClientId()); // Remove bar-app client client = fooRealm.getClientByClientId("bar-app"); @@ -266,8 +260,10 @@ public class UserSessionProviderOfflineTest { resetSession(); // Assert nothing loaded - userSession was removed as well because it was last userSession + realmMgr = new RealmManager(session); + fooRealm = realmMgr.getRealm("foo"); offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId()); - Assert.assertEquals(0, offlineSession.getClientSessions().size()); + Assert.assertEquals(0, offlineSession.getAuthenticatedClientSessions().size()); // Cleanup realmMgr = new RealmManager(session); @@ -282,8 +278,8 @@ public class UserSessionProviderOfflineTest { fooRealm.addClient("foo-app"); session.users().addUser(fooRealm, "user3"); - UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); - ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null); + AuthenticatedClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); resetSession(); @@ -309,7 +305,6 @@ public class UserSessionProviderOfflineTest { // Assert userSession removed as well Assert.assertNull(session.sessions().getOfflineUserSession(fooRealm, userSession.getId())); - Assert.assertNull(session.sessions().getOfflineClientSession(fooRealm, clientSession.getId())); // Cleanup realmMgr = new RealmManager(session); @@ -325,33 +320,26 @@ public class UserSessionProviderOfflineTest { resetSession(); - Map offlineSessions = new HashMap<>(); + // Key is userSessionId, value is set of client UUIDS + Map> offlineSessions = new HashMap<>(); // Persist 3 created userSessions and clientSessions as offline ClientModel testApp = realm.getClientByClientId("test-app"); List userSessions = session.sessions().getUserSessions(realm, testApp); for (UserSessionModel userSession : userSessions) { - offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession)); + offlineSessions.put(userSession.getId(), createOfflineSessionIncludeClientSessions(userSession)); } resetSession(); // Assert all previously saved offline sessions found - for (Map.Entry entry : offlineSessions.entrySet()) { - Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null); + for (Map.Entry> entry : offlineSessions.entrySet()) { + UserSessionModel foundSession = sessionManager.findOfflineUserSession(realm, entry.getKey()); + Assert.assertEquals(foundSession.getAuthenticatedClientSessions().keySet(), entry.getValue()); } UserSessionModel session0 = session.sessions().getOfflineUserSession(realm, origSessions[0].getId()); Assert.assertNotNull(session0); - List clientSessions = new LinkedList<>(); - for (ClientSessionModel clientSession : session0.getClientSessions()) { - clientSessions.add(clientSession.getId()); - Assert.assertNotNull(session.sessions().getOfflineClientSession(realm, clientSession.getId())); - } - - UserSessionModel session1 = session.sessions().getOfflineUserSession(realm, origSessions[1].getId()); - Assert.assertEquals(1, session1.getClientSessions().size()); - ClientSessionModel cls1 = session1.getClientSessions().get(0); // sessions are in persister too Assert.assertEquals(3, persister.getUserSessionsCount(true)); @@ -359,9 +347,6 @@ public class UserSessionProviderOfflineTest { // Set lastSessionRefresh to session[0] to 0 session0.setLastSessionRefresh(0); - // Set timestamp to cls1 to 0 - cls1.setTimestamp(0); - resetSession(); session.sessions().removeExpired(realm); @@ -370,21 +355,8 @@ public class UserSessionProviderOfflineTest { // assert session0 not found now Assert.assertNull(session.sessions().getOfflineUserSession(realm, origSessions[0].getId())); - for (String clientSession : clientSessions) { - Assert.assertNull(session.sessions().getOfflineClientSession(realm, origSessions[0].getId())); - offlineSessions.remove(clientSession); - } - // Assert cls1 not found too - for (Map.Entry entry : offlineSessions.entrySet()) { - String userSessionId = entry.getValue(); - if (userSessionId.equals(session1.getId())) { - Assert.assertFalse(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null); - } else { - Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) != null); - } - } - Assert.assertEquals(1, persister.getUserSessionsCount(true)); + Assert.assertEquals(2, persister.getUserSessionsCount(true)); // Expire everything and assert nothing found Time.setOffset(3000000); @@ -393,8 +365,8 @@ public class UserSessionProviderOfflineTest { resetSession(); - for (Map.Entry entry : offlineSessions.entrySet()) { - Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey()) == null); + for (String userSessionId : offlineSessions.keySet()) { + Assert.assertNull(sessionManager.findOfflineUserSession(realm, userSessionId)); } Assert.assertEquals(0, persister.getUserSessionsCount(true)); @@ -403,18 +375,17 @@ public class UserSessionProviderOfflineTest { } } - private Map createOfflineSessionIncludeClientSessions(UserSessionModel userSession) { - Map offlineSessions = new HashMap<>(); + private Set createOfflineSessionIncludeClientSessions(UserSessionModel userSession) { + Set offlineSessions = new HashSet<>(); - for (ClientSessionModel clientSession : userSession.getClientSessions()) { + for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) { sessionManager.createOrUpdateOfflineSession(clientSession, userSession); - offlineSessions.put(clientSession.getId(), userSession.getId()); + offlineSessions.add(clientSession.getClient().getId()); } return offlineSessions; } - private void resetSession() { kc.stopSession(session, true); session = kc.startSession(); @@ -423,8 +394,8 @@ public class UserSessionProviderOfflineTest { persister = session.getProvider(UserSessionPersisterProvider.class); } - private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { - ClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client); + private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client, userSession); if (userSession != null) clientSession.setUserSession(userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); @@ -435,7 +406,7 @@ public class UserSessionProviderOfflineTest { private UserSessionModel[] createSessions() { UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); Set roles = new HashSet(); roles.add("one"); @@ -448,10 +419,10 @@ public class UserSessionProviderOfflineTest { createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); return sessions; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java index 824200d521..7d6745e702 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java @@ -23,21 +23,23 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.common.util.Time; +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.models.UserManager; import org.keycloak.testsuite.rule.KeycloakRule; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import static org.junit.Assert.assertArrayEquals; @@ -103,22 +105,36 @@ public class UserSessionProviderTest { assertEquals(1000, session.sessions().getUserSession(realm, sessions[0].getId()).getLastSessionRefresh()); } + @Test + public void testRestartSession() { + int started = Time.currentTime(); + UserSessionModel[] sessions = createSessions(); + + Time.setOffset(100); + + UserSessionModel userSession = session.sessions().getUserSession(realm, sessions[0].getId()); + assertSession(userSession, session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party"); + + userSession.restartSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.6", "form", true, null, null); + + resetSession(); + + userSession = session.sessions().getUserSession(realm, sessions[0].getId()); + assertSession(userSession, session.users().getUserByUsername("user2", realm), "127.0.0.6", started + 100, started + 100); + + Time.setOffset(0); + } + @Test public void testCreateClientSession() { UserSessionModel[] sessions = createSessions(); - List clientSessions = session.sessions().getUserSession(realm, sessions[0].getId()).getClientSessions(); + Map clientSessions = session.sessions().getUserSession(realm, sessions[0].getId()).getAuthenticatedClientSessions(); assertEquals(2, clientSessions.size()); - String client1 = realm.getClientByClientId("test-app").getId(); + String clientUUID = realm.getClientByClientId("test-app").getId(); - ClientSessionModel session1; - - if (clientSessions.get(0).getClient().getId().equals(client1)) { - session1 = clientSessions.get(0); - } else { - session1 = clientSessions.get(1); - } + AuthenticatedClientSessionModel session1 = clientSessions.get(clientUUID); assertEquals(null, session1.getAction()); assertEquals(realm.getClientByClientId("test-app").getClientId(), session1.getClient().getClientId()); @@ -137,21 +153,22 @@ public class UserSessionProviderTest { public void testUpdateClientSession() { UserSessionModel[] sessions = createSessions(); - String id = sessions[0].getClientSessions().get(0).getId(); + String userSessionId = sessions[0].getId(); + String clientUUID = realm.getClientByClientId("test-app").getId(); - ClientSessionModel clientSession = session.sessions().getClientSession(realm, id); + AuthenticatedClientSessionModel clientSession = sessions[0].getAuthenticatedClientSessions().get(clientUUID); int time = clientSession.getTimestamp(); assertEquals(null, clientSession.getAction()); - clientSession.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name()); + clientSession.setAction(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name()); clientSession.setTimestamp(time + 10); kc.stopSession(session, true); session = kc.startSession(); - ClientSessionModel updated = session.sessions().getClientSession(realm, id); - assertEquals(ClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction()); + AuthenticatedClientSessionModel updated = session.sessions().getUserSession(realm, userSessionId).getAuthenticatedClientSessions().get(clientUUID); + assertEquals(AuthenticatedClientSessionModel.Action.CODE_TO_TOKEN.name(), updated.getAction()); assertEquals(time + 10, updated.getTimestamp()); } @@ -167,17 +184,12 @@ public class UserSessionProviderTest { public void testRemoveUserSessionsByUser() { UserSessionModel[] sessions = createSessions(); - List clientSessionsRemoved = new LinkedList(); - List clientSessionsKept = new LinkedList(); + Map clientSessionsKept = new HashMap<>(); for (UserSessionModel s : sessions) { s = session.sessions().getUserSession(realm, s.getId()); - for (ClientSessionModel c : s.getClientSessions()) { - if (c.getUserSession().getUser().getUsername().equals("user1")) { - clientSessionsRemoved.add(c.getId()); - } else { - clientSessionsKept.add(c.getId()); - } + if (!s.getUser().getUsername().equals("user1")) { + clientSessionsKept.put(s.getId(), s.getAuthenticatedClientSessions().keySet().size()); } } @@ -185,13 +197,12 @@ public class UserSessionProviderTest { resetSession(); assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); - assertFalse(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); + List userSessions = session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)); + assertFalse(userSessions.isEmpty()); - for (String c : clientSessionsRemoved) { - assertNull(session.sessions().getClientSession(realm, c)); - } - for (String c : clientSessionsKept) { - assertNotNull(session.sessions().getClientSession(realm, c)); + Assert.assertEquals(userSessions.size(), clientSessionsKept.size()); + for (UserSessionModel userSession : userSessions) { + Assert.assertEquals((int) clientSessionsKept.get(userSession.getId()), userSession.getAuthenticatedClientSessions().size()); } } @@ -199,76 +210,47 @@ public class UserSessionProviderTest { public void testRemoveUserSession() { UserSessionModel userSession = createSessions()[0]; - List clientSessionsRemoved = new LinkedList(); - for (ClientSessionModel c : userSession.getClientSessions()) { - clientSessionsRemoved.add(c.getId()); - } - session.sessions().removeUserSession(realm, userSession); resetSession(); assertNull(session.sessions().getUserSession(realm, userSession.getId())); - for (String c : clientSessionsRemoved) { - assertNull(session.sessions().getClientSession(realm, c)); - } } @Test public void testRemoveUserSessionsByRealm() { UserSessionModel[] sessions = createSessions(); - List clientSessions = new LinkedList(); - for (UserSessionModel s : sessions) { - clientSessions.addAll(s.getClientSessions()); - } - session.sessions().removeUserSessions(realm); resetSession(); assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm)).isEmpty()); assertTrue(session.sessions().getUserSessions(realm, session.users().getUserByUsername("user2", realm)).isEmpty()); - - for (ClientSessionModel c : clientSessions) { - assertNull(session.sessions().getClientSession(realm, c.getId())); - } } @Test public void testOnClientRemoved() { UserSessionModel[] sessions = createSessions(); - List clientSessionsRemoved = new LinkedList(); - List clientSessionsKept = new LinkedList(); + String thirdPartyClientUUID = realm.getClientByClientId("third-party").getId(); + + Map> clientSessionsKept = new HashMap<>(); + for (UserSessionModel s : sessions) { + Set clientUUIDS = new HashSet<>(s.getAuthenticatedClientSessions().keySet()); + clientUUIDS.remove(thirdPartyClientUUID); // This client will be later removed, hence his clientSessions too + clientSessionsKept.put(s.getId(), clientUUIDS); + } + + realm.removeClient(thirdPartyClientUUID); + resetSession(); + for (UserSessionModel s : sessions) { s = session.sessions().getUserSession(realm, s.getId()); - for (ClientSessionModel c : s.getClientSessions()) { - if (c.getClient().getClientId().equals("third-party")) { - clientSessionsRemoved.add(c.getId()); - } else { - clientSessionsKept.add(c.getId()); - } - } + Set clientUUIDS = s.getAuthenticatedClientSessions().keySet(); + assertEquals(clientUUIDS, clientSessionsKept.get(s.getId())); } - session.sessions().onClientRemoved(realm, realm.getClientByClientId("third-party")); - resetSession(); - - for (String c : clientSessionsRemoved) { - assertNull(session.sessions().getClientSession(realm, c)); - } - for (String c : clientSessionsKept) { - assertNotNull(session.sessions().getClientSession(realm, c)); - } - - session.sessions().onClientRemoved(realm, realm.getClientByClientId("test-app")); - resetSession(); - - for (String c : clientSessionsRemoved) { - assertNull(session.sessions().getClientSession(realm, c)); - } - for (String c : clientSessionsKept) { - assertNull(session.sessions().getClientSession(realm, c)); - } + // Revert client + realm.addClient("third-party"); } @Test @@ -278,27 +260,25 @@ public class UserSessionProviderTest { try { Set expired = new HashSet(); - Set expiredClientSessions = new HashSet(); Time.setOffset(-(realm.getSsoSessionMaxLifespan() + 1)); - expired.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId()); - expiredClientSessions.add(session.sessions().createClientSession(realm, client).getId()); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + expired.add(userSession.getId()); + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); + Assert.assertEquals(userSession, clientSession.getUserSession()); Time.setOffset(0); - UserSessionModel s = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true, null, null); + UserSessionModel s = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true, null, null); //s.setLastSessionRefresh(Time.currentTime() - (realm.getSsoSessionIdleTimeout() + 1)); s.setLastSessionRefresh(0); expired.add(s.getId()); - ClientSessionModel clSession = session.sessions().createClientSession(realm, client); - clSession.setUserSession(s); - expiredClientSessions.add(clSession.getId()); - Set valid = new HashSet(); Set validClientSessions = new HashSet(); - valid.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId()); - validClientSessions.add(session.sessions().createClientSession(realm, client).getId()); + userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + valid.add(userSession.getId()); + validClientSessions.add(session.sessions().createClientSession(realm, client, userSession).getId()); resetSession(); @@ -308,91 +288,18 @@ public class UserSessionProviderTest { for (String e : expired) { assertNull(session.sessions().getUserSession(realm, e)); } - for (String e : expiredClientSessions) { - assertNull(session.sessions().getClientSession(realm, e)); - } for (String v : valid) { - assertNotNull(session.sessions().getUserSession(realm, v)); - } - for (String e : validClientSessions) { - assertNotNull(session.sessions().getClientSession(realm, e)); + UserSessionModel userSessionLoaded = session.sessions().getUserSession(realm, v); + assertNotNull(userSessionLoaded); + Assert.assertEquals(1, userSessionLoaded.getAuthenticatedClientSessions().size()); + Assert.assertNotNull(userSessionLoaded.getAuthenticatedClientSessions().get(client.getId())); } } finally { Time.setOffset(0); } } - @Test - public void testExpireDetachedClientSessions() { - try { - realm.setAccessCodeLifespan(10); - realm.setAccessCodeLifespanUserAction(10); - realm.setAccessCodeLifespanLogin(30); - - // Login lifespan is largest - String clientSessionId = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")).getId(); - resetSession(); - - Time.setOffset(25); - session.sessions().removeExpired(realm); - resetSession(); - - assertNotNull(session.sessions().getClientSession(clientSessionId)); - - Time.setOffset(35); - session.sessions().removeExpired(realm); - resetSession(); - - assertNull(session.sessions().getClientSession(clientSessionId)); - - // User action is largest - realm.setAccessCodeLifespanUserAction(40); - - Time.setOffset(0); - clientSessionId = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")).getId(); - resetSession(); - - Time.setOffset(35); - session.sessions().removeExpired(realm); - resetSession(); - - assertNotNull(session.sessions().getClientSession(clientSessionId)); - - Time.setOffset(45); - session.sessions().removeExpired(realm); - resetSession(); - - assertNull(session.sessions().getClientSession(clientSessionId)); - - // Access code is largest - realm.setAccessCodeLifespan(50); - - Time.setOffset(0); - clientSessionId = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")).getId(); - resetSession(); - - Time.setOffset(45); - session.sessions().removeExpired(realm); - resetSession(); - - assertNotNull(session.sessions().getClientSession(clientSessionId)); - - Time.setOffset(55); - session.sessions().removeExpired(realm); - resetSession(); - - assertNull(session.sessions().getClientSession(clientSessionId)); - } finally { - Time.setOffset(0); - - realm.setAccessCodeLifespan(60); - realm.setAccessCodeLifespanUserAction(300); - realm.setAccessCodeLifespanLogin(1800); - - } - } - // KEYCLOAK-2508 @Test public void testRemovingExpiredSession() { @@ -425,13 +332,14 @@ public class UserSessionProviderTest { try { for (int i = 0; i < 25; i++) { Time.setOffset(i); - UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null); - ClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app")); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null); + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"), userSession); clientSession.setUserSession(userSession); clientSession.setRedirectUri("http://redirect"); clientSession.setRoles(new HashSet()); clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, "state"); clientSession.setTimestamp(userSession.getStarted()); + userSession.setLastSessionRefresh(userSession.getStarted()); } } finally { Time.setOffset(0); @@ -448,15 +356,111 @@ public class UserSessionProviderTest { @Test public void testCreateAndGetInSameTransaction() { - UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); - ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("test-app"), userSession, "http://redirect", "state", new HashSet(), new HashSet()); + ClientModel client = realm.getClientByClientId("test-app"); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + AuthenticatedClientSessionModel clientSession = createClientSession(client, userSession, "http://redirect", "state", new HashSet(), new HashSet()); - Assert.assertNotNull(session.sessions().getUserSession(realm, userSession.getId())); - Assert.assertNotNull(session.sessions().getClientSession(realm, clientSession.getId())); + UserSessionModel userSessionLoaded = session.sessions().getUserSession(realm, userSession.getId()); + AuthenticatedClientSessionModel clientSessionLoaded = userSessionLoaded.getAuthenticatedClientSessions().get(client.getId()); + Assert.assertNotNull(userSessionLoaded); + Assert.assertNotNull(clientSessionLoaded); - Assert.assertEquals(userSession.getId(), clientSession.getUserSession().getId()); - Assert.assertEquals(1, userSession.getClientSessions().size()); - Assert.assertEquals(clientSession.getId(), userSession.getClientSessions().get(0).getId()); + Assert.assertEquals(userSession.getId(), clientSessionLoaded.getUserSession().getId()); + Assert.assertEquals(1, userSessionLoaded.getAuthenticatedClientSessions().size()); + } + + @Test + public void testAuthenticatedClientSessions() { + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + + ClientModel client1 = realm.getClientByClientId("test-app"); + ClientModel client2 = realm.getClientByClientId("third-party"); + + // Create client1 session + AuthenticatedClientSessionModel clientSession1 = session.sessions().createClientSession(realm, client1, userSession); + clientSession1.setAction("foo1"); + clientSession1.setTimestamp(100); + + // Create client2 session + AuthenticatedClientSessionModel clientSession2 = session.sessions().createClientSession(realm, client2, userSession); + clientSession2.setAction("foo2"); + clientSession2.setTimestamp(200); + + // commit + resetSession(); + + // Ensure sessions are here + userSession = session.sessions().getUserSession(realm, userSession.getId()); + Map clientSessions = userSession.getAuthenticatedClientSessions(); + Assert.assertEquals(2, clientSessions.size()); + testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1", 100); + testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2", 200); + + // Update session1 + clientSessions.get(client1.getId()).setAction("foo1-updated"); + + // commit + resetSession(); + + // Ensure updated + userSession = session.sessions().getUserSession(realm, userSession.getId()); + clientSessions = userSession.getAuthenticatedClientSessions(); + testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100); + + // Rewrite session2 + clientSession2 = session.sessions().createClientSession(realm, client2, userSession); + clientSession2.setAction("foo2-rewrited"); + clientSession2.setTimestamp(300); + + // commit + resetSession(); + + // Ensure updated + userSession = session.sessions().getUserSession(realm, userSession.getId()); + clientSessions = userSession.getAuthenticatedClientSessions(); + Assert.assertEquals(2, clientSessions.size()); + testAuthenticatedClientSession(clientSessions.get(client1.getId()), "test-app", userSession.getId(), "foo1-updated", 100); + testAuthenticatedClientSession(clientSessions.get(client2.getId()), "third-party", userSession.getId(), "foo2-rewrited", 300); + + // remove session + clientSession1 = userSession.getAuthenticatedClientSessions().get(client1.getId()); + clientSession1.setUserSession(null); + + // Commit and ensure removed + resetSession(); + + userSession = session.sessions().getUserSession(realm, userSession.getId()); + clientSessions = userSession.getAuthenticatedClientSessions(); + Assert.assertEquals(1, clientSessions.size()); + Assert.assertNull(clientSessions.get(client1.getId())); + } + + + @Test + public void testFailCreateExistingSession() { + UserSessionModel userSession = session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + + // commit + resetSession(); + + + try { + session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + kc.stopSession(session, true); + Assert.fail("Not expected to successfully create duplicated userSession"); + } catch (IllegalStateException e) { + // Expected + session = kc.startSession(); + } + + } + + + private void testAuthenticatedClientSession(AuthenticatedClientSessionModel clientSession, String expectedClientId, String expectedUserSessionId, String expectedAction, int expectedTimestamp) { + Assert.assertEquals(expectedClientId, clientSession.getClient().getClientId()); + Assert.assertEquals(expectedUserSessionId, clientSession.getUserSession().getId()); + Assert.assertEquals(expectedAction, clientSession.getAction()); + Assert.assertEquals(expectedTimestamp, clientSession.getTimestamp()); } private void assertPaginatedSession(RealmModel realm, ClientModel client, int start, int max, int expectedSize) { @@ -545,9 +549,8 @@ public class UserSessionProviderTest { assertNotNull(session.sessions().getUserLoginFailure(realm, "user2")); } - private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - if (userSession != null) clientSession.setUserSession(userSession); + private AuthenticatedClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set roles, Set protocolMappers) { + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, client, userSession); clientSession.setRedirectUri(redirect); if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state); if (roles != null) clientSession.setRoles(roles); @@ -557,7 +560,7 @@ public class UserSessionProviderTest { private UserSessionModel[] createSessions() { UserSessionModel[] sessions = new UserSessionModel[3]; - sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); + sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null); Set roles = new HashSet(); roles.add("one"); @@ -570,10 +573,10 @@ public class UserSessionProviderTest { createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers); createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); + sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet(), new HashSet()); - sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); + sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null); createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet(), new HashSet()); resetSession(); @@ -613,9 +616,14 @@ public class UserSessionProviderTest { assertTrue(session.getStarted() >= started - 1 && session.getStarted() <= started + 1); assertTrue(session.getLastSessionRefresh() >= lastRefresh - 1 && session.getLastSessionRefresh() <= lastRefresh + 1); - String[] actualClients = new String[session.getClientSessions().size()]; - for (int i = 0; i < actualClients.length; i++) { - actualClients[i] = session.getClientSessions().get(i).getClient().getClientId(); + String[] actualClients = new String[session.getAuthenticatedClientSessions().size()]; + int i = 0; + for (Map.Entry entry : session.getAuthenticatedClientSessions().entrySet()) { + String clientUUID = entry.getKey(); + AuthenticatedClientSessionModel clientSession = entry.getValue(); + Assert.assertEquals(clientUUID, clientSession.getClient().getId()); + actualClients[i] = clientSession.getClient().getClientId(); + i++; } Arrays.sort(clients); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java index 8ed8461070..e6bb66004b 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java @@ -28,11 +28,25 @@ public class IdpLinkEmailPage extends AbstractPage { @FindBy(id = "instruction1") private WebElement message; + @FindBy(linkText = "Click here") + private WebElement resendEmailLink; + + @FindBy(linkText = "Click here") // Actually same link like "resendEmailLink" + private WebElement continueFlowLink; + @Override public boolean isCurrent() { return driver.getTitle().startsWith("Link "); } + public void clickResendEmail() { + resendEmailLink.click(); + } + + public void clickContinueFlowLink() { + continueFlowLink.click(); + } + @Override public void open() throws Exception { throw new UnsupportedOperationException(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginExpiredPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginExpiredPage.java new file mode 100644 index 0000000000..e3ff938b08 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginExpiredPage.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 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.testsuite.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Marek Posolda + */ +public class LoginExpiredPage extends AbstractPage { + + @FindBy(id = "loginRestartLink") + private WebElement loginRestartLink; + + @FindBy(id = "loginContinueLink") + private WebElement loginContinueLink; + + + public void clickLoginRestartLink() { + loginRestartLink.click(); + } + + public void clickLoginContinueLink() { + loginContinueLink.click(); + } + + + public boolean isCurrent() { + return driver.getTitle().equals("Page has expired"); + } + + public void open() { + throw new UnsupportedOperationException(); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java index 050bcf3ece..2904ef854e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java @@ -21,7 +21,6 @@ import org.keycloak.Config; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; -import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.ApplicationServlet; @@ -90,24 +89,6 @@ public class KeycloakRule extends AbstractKeycloakRule { stopSession(session, true); } - public ClientSessionCode verifyCode(String code) { - KeycloakSession session = startSession(); - try { - RealmModel realm = session.realms().getRealm("test"); - try { - ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); - if (accessCode == null) { - Assert.fail("Invalid code"); - } - return accessCode; - } catch (Throwable t) { - throw new AssertionError("Failed to parse code", t); - } - } finally { - stopSession(session, false); - } - } - public abstract static class KeycloakSetup { protected KeycloakSession session; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java index 6dbc938ec2..7b217b66f9 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/LDAPRule.java @@ -88,7 +88,7 @@ public class LDAPRule implements TestRule { return true; } - if (ldapTestConfiguration.isStartEmbeddedLdapLerver()) { + if (ldapTestConfiguration.isStartEmbeddedLdapServer()) { ldapEmbeddedServer = createServer(); ldapEmbeddedServer.init(); ldapEmbeddedServer.start(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java index 2cea40a7cd..0d93d19a21 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java @@ -40,10 +40,14 @@ public class WebRule extends ExternalResource { this.test = test; } - @Override - public void before() throws Throwable { + public void initProperties() { driver = createWebDriver(); oauth = new OAuthClient(driver); + } + + @Override + public void before() throws Throwable { + initProperties(); initWebResources(test); } @@ -58,6 +62,7 @@ public class WebRule extends ExternalResource { HtmlUnitDriver d = new HtmlUnitDriver(); d.getWebClient().getOptions().setJavaScriptEnabled(true); d.getWebClient().getOptions().setCssEnabled(false); + d.getWebClient().getOptions().setTimeout(1000000); driver = d; } else if (browser.equals("chrome")) { driver = new ChromeDriver(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java index f94cea05d6..7aea03e5e3 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/AbstractOfflineCacheCommand.java @@ -47,9 +47,9 @@ public abstract class AbstractOfflineCacheCommand extends AbstractCommand { } protected String toString(UserSessionEntity userSession) { - int clientSessionsSize = userSession.getClientSessions()==null ? 0 : userSession.getClientSessions().size(); + int clientSessionsSize = userSession.getAuthenticatedClientSessions()==null ? 0 : userSession.getAuthenticatedClientSessions().size(); return "ID: " + userSession.getId() + ", realm: " + userSession.getRealm() + ", lastAccessTime: " + Time.toDate(userSession.getLastSessionRefresh()) + - ", clientSessions: " + clientSessionsSize; + ", authenticatedClientSessions: " + clientSessionsSize; } protected abstract void doRunCacheCommand(KeycloakSession session, Cache cache); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java index b2ede3ecae..c8b6771049 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/util/cli/PersistSessionsCommand.java @@ -17,8 +17,11 @@ package org.keycloak.testsuite.util.cli; +import java.util.LinkedList; +import java.util.List; + +import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; -import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionTask; import org.keycloak.models.RealmModel; @@ -27,8 +30,6 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.session.UserSessionPersisterProvider; import org.keycloak.models.utils.KeycloakModelUtils; -import java.util.LinkedList; -import java.util.List; /** * @author Marek Posolda @@ -65,9 +66,9 @@ public class PersistSessionsCommand extends AbstractCommand { }); } + private void createSessionsBatch(final int countInThisBatch) { final List userSessionIds = new LinkedList<>(); - final List clientSessionIds = new LinkedList<>(); KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { @@ -79,13 +80,11 @@ public class PersistSessionsCommand extends AbstractCommand { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); for (int i = 0; i < countInThisBatch; i++) { - UserSessionModel userSession = session.sessions().createUserSession(realm, john, "john-doh@localhost", "127.0.0.2", "form", true, null, null); - ClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp); - clientSession.setUserSession(userSession); + UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, john, "john-doh@localhost", "127.0.0.2", "form", true, null, null); + AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp, userSession); clientSession.setRedirectUri("http://redirect"); clientSession.setNote("foo", "bar-" + i); userSessionIds.add(userSession.getId()); - clientSessionIds.add(clientSession.getId()); } } @@ -100,6 +99,7 @@ public class PersistSessionsCommand extends AbstractCommand { @Override public void run(KeycloakSession session) { RealmModel realm = session.realms().getRealmByName("master"); + ClientModel testApp = realm.getClientByClientId("security-admin-console"); UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); int counter = 0; @@ -107,17 +107,12 @@ public class PersistSessionsCommand extends AbstractCommand { counter++; UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId); persister.createUserSession(userSession, true); + + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(testApp.getId()); + persister.createClientSession(clientSession, true); } log.infof("%d user sessions persisted. Continue", counter); - - counter = 0; - for (String clientSessionId : clientSessionIds) { - counter++; - ClientSessionModel clientSession = session.sessions().getClientSession(realm, clientSessionId); - persister.createClientSession(clientSession, true); - } - log.infof("%d client sessions persisted. Continue", counter); } }); diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index c463347de9..fc695d420e 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -90,7 +90,7 @@ "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}", "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}", "remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}", - "remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}", + "remoteStoreHost": "${keycloak.connectionsjen neInfinispan.remoteStoreHost:localhost}", "remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}" } }, diff --git a/testsuite/integration/src/test/resources/log4j.properties b/testsuite/integration/src/test/resources/log4j.properties index cac26aebae..2c6e8849ca 100755 --- a/testsuite/integration/src/test/resources/log4j.properties +++ b/testsuite/integration/src/test/resources/log4j.properties @@ -21,7 +21,9 @@ log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n -log4j.logger.org.keycloak=info +# For debug, run KeycloakServer with -Dkeycloak.logging.level=debug +keycloak.logging.level=info +log4j.logger.org.keycloak=${keycloak.logging.level} # Enable to view events @@ -80,4 +82,8 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error #log4j.logger.org.apache.http.impl.conn=debug # Enable to view details from identity provider authenticator -# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace \ No newline at end of file +log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace +log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace +log4j.logger.org.keycloak.broker=trace + +# log4j.logger.io.undertow=trace diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 8b776c6c5f..f129e459e5 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -108,6 +108,10 @@ access-token-lifespan=Access Token Lifespan access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout. access-token-lifespan-for-implicit-flow=Access Token Lifespan For Implicit Flow access-token-lifespan-for-implicit-flow.tooltip=Max time before an access token issued during OpenID Connect Implicit Flow is expired. This value is recommended to be shorter than SSO timeout. There is no possibility to refresh token during implicit flow, that's why there is separate timeout different to 'Access Token Lifespan'. +action-token-generated-by-admin-lifespan=Default Admin Action Token Lifespan +action-token-generated-by-admin-lifespan.tooltip=Max time before an action token generated via admin interface is expired. This value is recommended to be long to allow admins send e-mails for users that are currently offline. The default timeout can be overridden right before issuing the token. +action-token-generated-by-user-lifespan=User Action Token Lifespan +action-token-generated-by-user-lifespan.tooltip=Max time before an action token generated via user action (e.g. e-mail verification) is expired. This value is recommended to be short because it is expected that the user would react to self-created action token quickly. client-login-timeout=Client login timeout client-login-timeout.tooltip=Max time an client has to finish the access token protocol. This should normally be 1 minute. login-timeout=Login timeout @@ -1292,6 +1296,8 @@ credential-types=Credential Types manage-user-password=Manage Password disable-credentials=Disable Credentials credential-reset-actions=Credential Reset +credential-reset-actions-timeout=Token validity +credential-reset-actions-timeout.tooltip=Max time before the action token allowing execution of given actions is expired. ldap-mappers=LDAP Mappers create-ldap-mapper=Create LDAP mapper diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index bcc655fbcc..c658bb548f 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1044,6 +1044,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan); $scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin); $scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction); + $scope.realm.actionTokenGeneratedByAdminLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan); + $scope.realm.actionTokenGeneratedByUserLifespan = TimeUnit2.asUnit(realm.actionTokenGeneratedByUserLifespan); var oldCopy = angular.copy($scope.realm); $scope.changed = false; @@ -1063,6 +1065,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds(); $scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds(); $scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds(); + $scope.realm.actionTokenGeneratedByAdminLifespan = $scope.realm.actionTokenGeneratedByAdminLifespan.toSeconds(); + $scope.realm.actionTokenGeneratedByUserLifespan = $scope.realm.actionTokenGeneratedByUserLifespan.toSeconds(); Realm.update($scope.realm, function () { $route.reload(); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 13d83436b7..580d66175d 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -482,7 +482,7 @@ module.controller('UserDetailCtrl', function($scope, realm, user, BruteForceUser } }); -module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, RequiredActions, User, UserExecuteActionsEmail, UserCredentials, Notifications, Dialog) { +module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, RequiredActions, User, UserExecuteActionsEmail, UserCredentials, Notifications, Dialog, TimeUnit2) { console.log('UserCredentialsCtrl'); $scope.realm = realm; @@ -548,6 +548,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R }; $scope.emailActions = []; + $scope.emailActionsTimeout = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan); $scope.disableableCredentialTypes = []; $scope.sendExecuteActionsEmail = function() { @@ -556,7 +557,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R return; } Dialog.confirm('Send Email', 'Are you sure you want to send email to user?', function() { - UserExecuteActionsEmail.update({ realm: realm.realm, userId: user.id }, $scope.emailActions, function() { + UserExecuteActionsEmail.update({ realm: realm.realm, userId: user.id, lifespan: $scope.emailActionsTimeout.toSeconds() }, $scope.emailActions, function() { Notifications.success("Email sent to user"); $scope.emailActions = []; }, function() { diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js index fe09ebbe28..e850b3bc4b 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -496,7 +496,8 @@ module.factory('UserCredentials', function($resource) { module.factory('UserExecuteActionsEmail', function($resource) { return $resource(authUrl + '/admin/realms/:realm/users/:userId/execute-actions-email', { realm : '@realm', - userId : '@userId' + userId : '@userId', + lifespan : '@lifespan', }, { update : { method : 'PUT' diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html index f0f9e58fb4..f508716538 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html @@ -141,6 +141,40 @@ +
    + + +
    + + +
    + + {{:: 'action-token-generated-by-user-lifespan.tooltip' | translate}} + +
    + +
    + + +
    + + +
    + + {{:: 'action-token-generated-by-admin-lifespan.tooltip' | translate}} + +
    +
    diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html index d213fdd442..9f3512ee0a 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-credentials.html @@ -75,6 +75,20 @@
    {{:: 'credentials.reset-actions.tooltip' | translate}}
    +
    + + +
    + + +
    + {{:: 'credential-reset-actions-timeout.tooltip' | translate}} +
    diff --git a/themes/src/main/resources/theme/base/login/bypass_kerberos.ftl b/themes/src/main/resources/theme/base/login/bypass_kerberos.ftl deleted file mode 100755 index d87163c1a1..0000000000 --- a/themes/src/main/resources/theme/base/login/bypass_kerberos.ftl +++ /dev/null @@ -1,25 +0,0 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout displayMessage=false; section> - <#if section = "title"> - ${msg("kerberosNotConfiguredTitle")} - <#elseif section = "header"> - ${msg("kerberosNotConfigured")} - <#elseif section = "form"> -
    - -

    ${msg("bypassKerberosDetail")}

    -
    -
    -
    -
    - -
    -
    - <#if client?? && client.baseUrl?has_content> -

    ${msg("backToApplication")}

    - -
    -
    -
    - - \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl index 5dc29f1c11..1dbd43d50c 100644 --- a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl +++ b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl @@ -9,7 +9,10 @@ ${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}

    - ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")} + ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")} +

    +

    + ${msg("emailLinkIdp4")} ${msg("doClickHere")} ${msg("emailLinkIdp5")}

    \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/login-page-expired.ftl b/themes/src/main/resources/theme/base/login/login-page-expired.ftl new file mode 100644 index 0000000000..f58c25db33 --- /dev/null +++ b/themes/src/main/resources/theme/base/login/login-page-expired.ftl @@ -0,0 +1,13 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("pageExpiredTitle")} + <#elseif section = "header"> + ${msg("pageExpiredTitle")} + <#elseif section = "form"> +

    + ${msg("pageExpiredMsg1")} ${msg("doClickHere")} . + ${msg("pageExpiredMsg2")} ${msg("doClickHere")} . +

    + + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/login-verify-email.ftl b/themes/src/main/resources/theme/base/login/login-verify-email.ftl index 13963512fc..53caaa3802 100755 --- a/themes/src/main/resources/theme/base/login/login-verify-email.ftl +++ b/themes/src/main/resources/theme/base/login/login-verify-email.ftl @@ -9,7 +9,7 @@ ${msg("emailVerifyInstruction1")}

    - ${msg("emailVerifyInstruction2")} ${msg("doClickHere")} ${msg("emailVerifyInstruction3")} + ${msg("emailVerifyInstruction2")} ${msg("doClickHere")} ${msg("emailVerifyInstruction3")}

    \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/login/messages/messages_en.properties b/themes/src/main/resources/theme/base/login/messages/messages_en.properties index 6c168aeff8..cf262361b7 100755 --- a/themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -83,6 +83,8 @@ emailLinkIdpTitle=Link {0} emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you. emailLinkIdp2=Haven''t received a verification code in your email? emailLinkIdp3=to re-send the email. +emailLinkIdp4=If you already verified the email in different browser +emailLinkIdp5=to continue. backToLogin=« Back to Login @@ -90,6 +92,10 @@ emailInstruction=Enter your username or email address and we will send you instr copyCodeInstruction=Please copy this code and paste it into your application: +pageExpiredTitle=Page has expired +pageExpiredMsg1=To restart the login process +pageExpiredMsg2=To continue the login process + personalInfo=Personal Info: role_admin=Admin role_realm-admin=Realm Admin @@ -123,6 +129,7 @@ invalidEmailMessage=Invalid email address. accountDisabledMessage=Account is disabled, contact admin. accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later. expiredCodeMessage=Login timeout. Please login again. +expiredActionMessage=Action expired. Please continue with login now. missingFirstNameMessage=Please specify first name. missingLastNameMessage=Please specify last name. @@ -203,12 +210,13 @@ sessionNotActiveMessage=Session not active. invalidCodeMessage=An error occurred, please login again through your application. identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider identityProviderNotFoundMessage=Could not find an identity provider with the identifier. -identityProviderLinkSuccess=Your account was successfully linked with {0} account {1} . +identityProviderLinkSuccess=You successfully verified your email. Please go back to your original browser and continue there with the login. staleCodeMessage=This page is no longer valid, please go back to your application and login again realmSupportsNoCredentialsMessage=Realm does not support any credential type. identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with. emailVerifiedMessage=Your email address has been verified. staleEmailVerificationLink=The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email? +identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user. locale_ca=Catal\u00E0 locale_de=Deutsch @@ -229,5 +237,7 @@ clientNotFoundMessage=Client not found. clientDisabledMessage=Client disabled. invalidParameterMessage=Invalid parameter\: {0} alreadyLoggedIn=You are already logged in. +differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first. +brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid. p3pPolicy=CP="This is not a P3P policy!" diff --git a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java index 96078ffe41..d83cd189ea 100755 --- a/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java +++ b/wildfly/server-subsystem/src/main/java/org/keycloak/subsystem/server/extension/KeycloakServerDeploymentProcessor.java @@ -41,12 +41,12 @@ import java.util.List; public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcessor { private static final String[] CACHES = new String[] { - "realms", "users","sessions","offlineSessions","loginFailures","work","authorization","keys" + "realms", "users","sessions","authenticationSessions","offlineSessions","loginFailures","work","authorization","keys","actionTokens" }; // This param name is defined again in Keycloak Services class // org.keycloak.services.resources.KeycloakApplication. We have this value in - // two places to avoid dependency between Keycloak Subsystem and Keyclaok Services module. + // two places to avoid dependency between Keycloak Subsystem and Keycloak Services module. public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config"; @Override diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml index fb832fa23a..818626a8fb 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml @@ -32,6 +32,7 @@ + @@ -42,6 +43,10 @@ + + + + @@ -97,6 +102,7 @@ + @@ -107,6 +113,10 @@ + + + + diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml index 95bcffd9f3..a76162b83a 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan2.xml @@ -32,6 +32,7 @@ + @@ -42,6 +43,10 @@ + + + + @@ -100,6 +105,7 @@ + @@ -110,6 +116,10 @@ + + + +