> it = checks.iterator(); it.hasNext();) {
+ if (it.next().getClass() == checkClass) {
+ it.remove();
+ }
+ }
+ }
+
+ private void removeCheck(Predicate super T> check) {
+ checks.remove(check);
+ }
+
+ private > TokenVerifier replaceCheck(Class extends Predicate>> checkClass, boolean active, P predicate) {
+ removeCheck(checkClass);
+ if (active) {
+ checks.add(predicate);
+ }
+ return this;
+ }
+
+ private > TokenVerifier replaceCheck(Predicate super T> 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 super T>... 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 super T> 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 super T>... predicates) {
+ return new Predicate() {
+ @Override
+ public boolean test(T t) throws VerificationException {
+ for (Predicate super T> predicate : predicates) {
+ try {
+ if (predicate.test(t)) {
+ return true;
+ }
+
+ LOG.finer("[alternative] predicate failed: " + predicate);
+ } catch (VerificationException ex) {
+ LOG.log(Level.FINER, "[alternative] predicate " + predicate + " failed.", ex);
+ }
+ }
+
+ return false;
+ }
+ };
+ }
}
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