keycloak-aplcache
Changes
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 18(+18 -0)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java 5(+5 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/AddInvalidatedActionTokenEvent.java 50(+50 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java 12(+12 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RemoveActionTokensSpecificEvent.java 42(+42 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java 104(+104 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenValueEntity.java 76(+76 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProvider.java 98(+98 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanActionTokenStoreProviderFactory.java 100(+100 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java 2(+2 -0)
model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanKeycloakTransaction.java 136(+79 -57)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory 1(+1 -0)
server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java 27(+27 -0)
server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java 2(+0 -2)
services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java 17(+13 -4)
services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java 41(+37 -4)
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java 18(+2 -16)
services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java 27(+25 -2)
services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java 8(+2 -6)
services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java 21(+2 -19)
services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java 10(+6 -4)
services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java 8(+2 -6)
services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java 4(+2 -2)
services/src/main/java/org/keycloak/authentication/authenticators/resetcred/ResetCredentialEmail.java 9(+3 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 2(+0 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java 10(+10 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 61(+55 -6)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java 30(+14 -16)
Details
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 b14e55b..670e1d8 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<String> getDefaultRoles() {
return defaultRoles;
}
diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl
index 7c0b3c1..d78ff75 100755
--- a/distribution/demo-dist/src/main/xslt/standalone.xsl
+++ b/distribution/demo-dist/src/main/xslt/standalone.xsl
@@ -93,6 +93,7 @@
<local-cache name="offlineSessions"/>
<local-cache name="loginFailures"/>
<local-cache name="authorization"/>
+ <local-cache name="actionTokens"/>
<local-cache name="work"/>
<local-cache name="keys">
<eviction max-entries="1000" strategy="LRU"/>
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 b8a7e89..5ce3122 100644
--- a/distribution/server-overlay/src/main/cli/keycloak-install-base.cli
+++ b/distribution/server-overlay/src/main/cli/keycloak-install-base.cli
@@ -15,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 e092374..4710eb8 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
@@ -16,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/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 0f63129..4bd7801 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
@@ -120,6 +120,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
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);
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
} catch (Exception e) {
@@ -220,6 +221,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);
}
private Configuration getRevisionCacheConfig(long maxEntries) {
@@ -270,4 +274,18 @@ 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();
+ }
+
}
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 6d8b7f4..ba9a31b 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
@@ -40,6 +40,11 @@ public interface InfinispanConnectionProvider extends Provider {
String WORK_CACHE_NAME = "work";
String AUTHORIZATION_CACHE_NAME = "authorization";
+ 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;
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 0000000..37a1a21
--- /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/entities/CachedRealm.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedRealm.java
index fef0486..3668d97 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<RequiredCredentialModel> getRequiredCredentials() {
return requiredCredentials;
}
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 8350f0d..0bed826 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
@@ -475,6 +475,30 @@ public class RealmAdapter implements CachedRealmModel {
}
@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<RequiredCredentialModel> getRequiredCredentials() {
if (isUpdated()) return updated.getRequiredCredentials();
return cached.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 0000000..0a4d858
--- /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/sessions/infinispan/entities/ActionTokenReducedKey.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ActionTokenReducedKey.java
new file mode 100644
index 0000000..173c434
--- /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<ActionTokenReducedKey> {
+
+ @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 0000000..7c0f663
--- /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<String, String> notes;
+
+ public ActionTokenValueEntity(Map<String, String> notes) {
+ this.notes = notes == null ? Collections.EMPTY_MAP : new HashMap<>(notes);
+ }
+
+ @Override
+ public Map<String, String> getNotes() {
+ return Collections.unmodifiableMap(notes);
+ }
+
+ @Override
+ public String getNote(String name) {
+ return notes.get(name);
+ }
+
+ public static class ExternalizerImpl implements Externalizer<ActionTokenValueEntity> {
+
+ 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<String, String> notes = notesEmpty ? Collections.EMPTY_MAP : (Map<String, String>) input.readObject();
+
+ return new ActionTokenValueEntity(notes);
+ }
+ }
+}
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 0000000..127879a
--- /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<ActionTokenReducedKey, ActionTokenValueEntity> actionKeyCache;
+ private final InfinispanKeycloakTransaction tx;
+ private final KeycloakSession session;
+
+ public InfinispanActionTokenStoreProvider(KeycloakSession session, Cache<ActionTokenReducedKey, ActionTokenValueEntity> 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<String, String> 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 0000000..a8c5e38
--- /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<ActionTokenReducedKey, ActionTokenValueEntity> 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/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index aa6ede3..83e970d 100644
--- 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
@@ -42,6 +42,8 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
+ public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
+
@Override
public void init(Config.Scope config) {
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
index 4ac40af..5471184 100644
--- 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
@@ -16,11 +16,14 @@
*/
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.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import org.infinispan.Cache;
import org.jboss.logging.Logger;
@@ -32,12 +35,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
private final static Logger log = Logger.getLogger(InfinispanKeycloakTransaction.class);
public enum CacheOperation {
- ADD, REMOVE, REPLACE, ADD_IF_ABSENT // ADD_IF_ABSENT throws an exception if there is existing value
+ 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<Object, CacheTask> tasks = new HashMap<>();
+ private final Map<Object, CacheTask> tasks = new LinkedHashMap<>();
@Override
public void begin() {
@@ -80,7 +83,28 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
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));
+ tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
+ @Override
+ public void execute() {
+ decorateCache(cache).put(key, value);
+ }
+ });
+ }
+ }
+
+ public <K, V> void put(Cache<K, V> 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<V>(value) {
+ @Override
+ public void execute() {
+ decorateCache(cache).put(key, value, lifespan, lifespanUnit);
+ }
+ });
}
}
@@ -91,7 +115,15 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
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_IF_ABSENT, key, value));
+ tasks.put(taskKey, new CacheTaskWithValue<V>(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);
+ }
+ }
+ });
}
}
@@ -101,40 +133,47 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
Object taskKey = getTaskKey(cache, key);
CacheTask current = tasks.get(taskKey);
if (current != null) {
- switch (current.operation) {
- case ADD:
- case ADD_IF_ABSENT:
- case REPLACE:
- current.value = value;
- return;
- case REMOVE:
- return;
+ if (current instanceof CacheTaskWithValue) {
+ ((CacheTaskWithValue<V>) current).setValue(value);
}
} else {
- tasks.put(taskKey, new CacheTask<>(cache, CacheOperation.REPLACE, key, value));
+ tasks.put(taskKey, new CacheTaskWithValue<V>(value) {
+ @Override
+ public void execute() {
+ decorateCache(cache).replace(key, value);
+ }
+ });
+ }
+ }
+
+ public <K, V> 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 <K, V> void remove(Cache<K, V> cache, K 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));
+ tasks.put(taskKey, () -> decorateCache(cache).remove(key));
}
// This is for possibility to lookup for session by id, which was created in this transaction
public <K, V> V get(Cache<K, V> cache, K key) {
Object taskKey = getTaskKey(cache, key);
- CacheTask<K, V> current = tasks.get(taskKey);
+ CacheTask<V> current = tasks.get(taskKey);
if (current != null) {
- switch (current.operation) {
- case ADD:
- case ADD_IF_ABSENT:
- case REPLACE:
- return current.value;
- case REMOVE:
- return null;
+ if (current instanceof CacheTaskWithValue) {
+ return ((CacheTaskWithValue<V>) current).getValue();
}
+ return null;
}
// Should we have per-transaction cache for lookups?
@@ -151,46 +190,29 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
}
}
- public static class CacheTask<K, V> {
- private final Cache<K, V> cache;
- private final CacheOperation operation;
- private final K key;
- private V value;
+ public interface CacheTask<V> {
+ void execute();
+ }
- public CacheTask(Cache<K, V> cache, CacheOperation operation, K key, V value) {
- this.cache = cache;
- this.operation = operation;
- this.key = key;
+ public abstract class CacheTaskWithValue<V> implements CacheTask<V> {
+ protected V value;
+
+ public CacheTaskWithValue(V value) {
this.value = value;
}
- public void execute() {
- log.tracev("Executing cache operation: {0} on {1}", operation, key);
-
- switch (operation) {
- case ADD:
- decorateCache().put(key, value);
- break;
- case REMOVE:
- decorateCache().remove(key);
- break;
- case REPLACE:
- decorateCache().replace(key, value);
- break;
- case ADD_IF_ABSENT:
- V existing = cache.putIfAbsent(key, value);
- if (existing != null) {
- throw new IllegalStateException("IllegalState. There is already existing value in cache for key " + key);
- }
- break;
- }
+ public V getValue() {
+ return value;
}
-
- // Ignore return values. Should have better performance within cluster / cross-dc env
- private Cache<K, V> decorateCache() {
- return cache.getAdvancedCache()
- .withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_REMOTE_LOOKUP);
+ public void setValue(V value) {
+ this.value = value;
}
}
+
+ // Ignore return values. Should have better performance within cluster / cross-dc env
+ private static <K, V> Cache<K, V> decorateCache(Cache<K, V> 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/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory
new file mode 100644
index 0000000..4100ecc
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory
@@ -0,0 +1 @@
+org.keycloak.models.sessions.infinispan.InfinispanActionTokenStoreProviderFactory
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 499a008..6ee1074 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/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 58aa424..b3b4db2 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -512,6 +512,26 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
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/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 0000000..cf9d7d0
--- /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 0000000..ba01cb6
--- /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<String,String> 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/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index dc8bff5..f6484d6 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<RequiredCredentialModel> getRequiredCredentials();
void addRequiredCredential(String cred);
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 76dc582..517b5f4 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<RequiredActionPro
* @return
*/
String getDisplayText();
+
+ /**
+ * Flag indicating whether the execution of the required action by the same circumstances
+ * (e.g. by one and the same action token) should only be permitted once.
+ * @return
+ */
+ default boolean isOneTimeAction() {
+ return false;
+ }
}
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 0000000..4e4a8db
--- /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<String, String> 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 0000000..26d086d
--- /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<ActionTokenStoreProvider> {
+
+}
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 0000000..66ee518
--- /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<? extends Provider> getProviderClass() {
+ return ActionTokenStoreProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return ActionTokenStoreProviderFactory.class;
+ }
+
+}
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 fb2c727..43e8ef8 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
@@ -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());
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 b427477..1c90b0f 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/sessions/AuthenticationSessionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
index c8758ca..b182458 100644
--- a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
@@ -23,6 +23,4 @@ import org.keycloak.provider.ProviderFactory;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface AuthenticationSessionProviderFactory extends ProviderFactory<AuthenticationSessionProvider> {
- // TODO:hmlnarik: move this constant out of an interface into a more appropriate class
- public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
}
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 b046e7d..c692d32 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
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
index e8a9b02..52d94d9 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/AbstractActionTokenHander.java
@@ -17,13 +17,11 @@
package org.keycloak.authentication.actiontoken;
import org.keycloak.Config.Scope;
-import org.keycloak.authentication.actiontoken.ActionTokenHandler;
-import org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
-import org.keycloak.representations.JsonWebToken;
-import org.keycloak.services.messages.Messages;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.sessions.AuthenticationSessionModel;
/**
*
@@ -92,4 +90,15 @@ public abstract class AbstractActionTokenHander<T extends DefaultActionToken> im
return token == null ? null : token.getAuthenticationSessionId();
}
+ @Override
+ public AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext) {
+ AuthenticationSessionModel authSession = tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
+ authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
+ return authSession;
+ }
+
+ @Override
+ public boolean canUseTokenRepeatedly(T token, ActionTokenContext<T> tokenContext) {
+ return true;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
index 4368a74..f8d02d3 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
@@ -17,20 +17,18 @@
package org.keycloak.authentication.actiontoken;
import org.keycloak.TokenVerifier.Predicate;
-import org.keycloak.authentication.AuthenticationProcessor;
-import org.keycloak.common.VerificationException;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.provider.Provider;
import org.keycloak.representations.JsonWebToken;
-import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
/**
* Handler of the action token.
*
+ * @param <T> Class implementing the action token
+ *
* @author hmlnarik
*/
public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
@@ -42,7 +40,6 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
* @param token
* @param tokenContext
* @return
- * @throws VerificationException
*/
Response handleToken(T token, ActionTokenContext<T> tokenContext);
@@ -96,10 +93,12 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
* @param tokenContext
* @return
*/
- default AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext) {
- AuthenticationSessionModel authSession = tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
- authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
- return authSession;
- }
+ AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> 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<T> tokenContext);
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
index 0f21a44..ba44880 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionToken.java
@@ -22,9 +22,7 @@ import org.keycloak.common.VerificationException;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSBuilder;
-import org.keycloak.models.KeyManager;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
+import org.keycloak.models.*;
import org.keycloak.services.Urls;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -37,12 +35,11 @@ import javax.ws.rs.core.UriInfo;
*
* @author hmlnarik
*/
-public class DefaultActionToken extends DefaultActionTokenKey {
+public class DefaultActionToken extends DefaultActionTokenKey implements ActionTokenValueModel {
- public static final String JSON_FIELD_ACTION_VERIFICATION_NONCE = "nonce";
public static final String JSON_FIELD_AUTHENTICATION_SESSION_ID = "asid";
- public static Predicate<DefaultActionToken> ACTION_TOKEN_BASIC_CHECKS = t -> {
+ public static final Predicate<DefaultActionToken> ACTION_TOKEN_BASIC_CHECKS = t -> {
if (t.getActionVerificationNonce() == null) {
throw new VerificationException("Nonce not present.");
}
@@ -53,15 +50,8 @@ public class DefaultActionToken extends DefaultActionTokenKey {
/**
* Single-use random value used for verification whether the relevant action is allowed.
*/
- @JsonProperty(value = JSON_FIELD_ACTION_VERIFICATION_NONCE, required = true)
- private UUID actionVerificationNonce;
-
public DefaultActionToken() {
- super(null, null);
- }
-
- public DefaultActionToken(String userId, String actionId, int expirationInSecs) {
- this(userId, actionId, expirationInSecs, UUID.randomUUID());
+ super(null, null, 0, null);
}
/**
@@ -72,11 +62,20 @@ public class DefaultActionToken extends DefaultActionTokenKey {
* @param actionVerificationNonce
*/
protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
- super(userId, actionId);
- this.actionVerificationNonce = actionVerificationNonce == null ? UUID.randomUUID() : actionVerificationNonce;
- expiration = absoluteExpirationInSecs;
+ 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() {
@@ -88,11 +87,8 @@ public class DefaultActionToken extends DefaultActionTokenKey {
setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
}
- public UUID getActionVerificationNonce() {
- return actionVerificationNonce;
- }
-
@JsonIgnore
+ @Override
public Map<String, String> getNotes() {
Map<String, String> res = new HashMap<>();
if (getAuthenticationSessionId() != null) {
@@ -101,6 +97,7 @@ public class DefaultActionToken extends DefaultActionTokenKey {
return res;
}
+ @Override
public String getNote(String name) {
Object res = getOtherClaims().get(name);
return res instanceof String ? (String) res : null;
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
index a5440a9..b41681f 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
@@ -16,31 +16,64 @@
*/
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 {
+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 DefaultActionTokenKey(String userId, String actionId) {
- subject = userId;
- type = actionId;
+ 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/execactions/ExecuteActionsActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java
index 4be6f86..7c32e2d 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionToken.java
@@ -16,13 +16,10 @@
*/
package org.keycloak.authentication.actiontoken.execactions;
-import org.keycloak.TokenVerifier;
import org.keycloak.authentication.actiontoken.DefaultActionToken;
-import org.keycloak.common.VerificationException;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.LinkedList;
import java.util.List;
-import java.util.UUID;
/**
*
@@ -34,15 +31,14 @@ public class ExecuteActionsActionToken extends DefaultActionToken {
private static final String JSON_FIELD_REQUIRED_ACTIONS = "rqac";
private static final String JSON_FIELD_REDIRECT_URI = "reduri";
- public ExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, List<String> requiredActions, String redirectUri, String clientId) {
- super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
+ public ExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, List<String> 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() {
- super(null, TOKEN_TYPE, -1, null);
}
@JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
@@ -72,14 +68,4 @@ public class ExecuteActionsActionToken extends DefaultActionToken {
setOtherClaims(JSON_FIELD_REDIRECT_URI, redirectUri);
}
}
-
- /**
- * Returns a {@code ExecuteActionsActionToken} instance decoded from the given string. If decoding fails, returns {@code null}
- *
- * @param actionTokenString
- * @return
- */
- public static ExecuteActionsActionToken deserialize(String actionTokenString) throws VerificationException {
- return TokenVerifier.create(actionTokenString, ExecuteActionsActionToken.class).getToken();
- }
}
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
index 010c517..9993ab7 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
@@ -17,15 +17,18 @@
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.UserModel;
+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;
/**
@@ -48,7 +51,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
public Predicate<? super ExecuteActionsActionToken>[] getVerifiers(ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
return TokenUtils.predicates(
TokenUtils.checkThat(
- // either redirect URI is not specified or must be valid for the cllient
+ // 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,
@@ -81,4 +84,24 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
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<ExecuteActionsActionToken> 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
index ea705ed..7776634 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -16,9 +16,7 @@
*/
package org.keycloak.authentication.actiontoken.idpverifyemail;
-import org.keycloak.authentication.actiontoken.verifyemail.*;
import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.UUID;
import org.keycloak.authentication.actiontoken.DefaultActionToken;
/**
@@ -39,16 +37,14 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
private String identityProviderAlias;
- public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId,
+ public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId,
String identityProviderUsername, String identityProviderAlias) {
- super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
- setAuthenticationSessionId(authenticationSessionId);
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
this.identityProviderUsername = identityProviderUsername;
this.identityProviderAlias = identityProviderAlias;
}
private IdpVerifyAccountLinkActionToken() {
- super(null, TOKEN_TYPE, -1, null);
}
public String getIdentityProviderUsername() {
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
index 67fb452..6cd0458 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionToken.java
@@ -16,8 +16,6 @@
*/
package org.keycloak.authentication.actiontoken.resetcred;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.UUID;
import org.keycloak.authentication.actiontoken.DefaultActionToken;
/**
@@ -28,26 +26,11 @@ import org.keycloak.authentication.actiontoken.DefaultActionToken;
public class ResetCredentialsActionToken extends DefaultActionToken {
public static final String TOKEN_TYPE = "reset-credentials";
- private static final String JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP = "lcpt";
- @JsonProperty(value = JSON_FIELD_LAST_CHANGE_PASSWORD_TIMESTAMP)
- private Long lastChangedPasswordTimestamp;
-
- public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId, Long lastChangedPasswordTimestamp) {
- super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
- setAuthenticationSessionId(authenticationSessionId);
- this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
+ public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
}
private ResetCredentialsActionToken() {
- super(null, TOKEN_TYPE, -1, null);
- }
-
- public Long getLastChangedPasswordTimestamp() {
- return lastChangedPasswordTimestamp;
- }
-
- public final void setLastChangedPasswordTimestamp(Long lastChangedPasswordTimestamp) {
- this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
}
}
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
index 3417431..0f08bd3 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
@@ -25,7 +25,6 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.services.ErrorPage;
-import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.LoginActionsServiceChecks.IsActionRequired;
@@ -55,9 +54,7 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
return new Predicate[] {
TokenUtils.checkThat(tokenContext.getRealm()::isResetPasswordAllowed, Errors.NOT_ALLOWED, Messages.RESET_CREDENTIAL_NOT_ALLOWED),
- new IsActionRequired(tokenContext, Action.AUTHENTICATE),
-
-// singleUseCheck, // TODO:hmlnarik - fix with single-use cache
+ new IsActionRequired(tokenContext, Action.AUTHENTICATE)
};
}
@@ -74,6 +71,11 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
);
}
+ @Override
+ public boolean canUseTokenRepeatedly(ResetCredentialsActionToken token, ActionTokenContext tokenContext) {
+ return false;
+ }
+
public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor {
@Override
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
index 72e460c..656c518 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionToken.java
@@ -17,7 +17,6 @@
package org.keycloak.authentication.actiontoken.verifyemail;
import com.fasterxml.jackson.annotation.JsonProperty;
-import java.util.UUID;
import org.keycloak.authentication.actiontoken.DefaultActionToken;
/**
@@ -34,15 +33,12 @@ public class VerifyEmailActionToken extends DefaultActionToken {
@JsonProperty(value = JSON_FIELD_EMAIL)
private String email;
- public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId,
- String email) {
- super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
- setAuthenticationSessionId(authenticationSessionId);
+ public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
this.email = email;
}
private VerifyEmailActionToken() {
- super(null, TOKEN_TYPE, -1, null);
}
public String getEmail() {
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 d8b9b30..11d9b91 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
@@ -108,7 +108,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
UriInfo uriInfo = session.getContext().getUri();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
- int validityInSecs = realm.getAccessCodeLifespanUserAction();
+ int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan();
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
@@ -120,7 +120,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
.removeDetail(Details.AUTH_TYPE);
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
- existingUser.getId(), absoluteExpirationInSecs, null, authSession.getId(),
+ existingUser.getId(), absoluteExpirationInSecs, authSession.getId(),
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
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 4d9b42e..4ac9bff 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
@@ -85,15 +85,11 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
return;
}
- int validityInSecs = context.getRealm().getAccessCodeLifespanUserAction();
+ int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan();
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
- KeycloakSession keycloakSession = context.getSession();
- Long lastCreatedPassword = getLastChangedTimestamp(keycloakSession, context.getRealm(), user);
-
// We send the secret in the email in a link as a query param.
- ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs,
- null, authenticationSession.getId(), lastCreatedPassword);
+ ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId());
String link = UriBuilder
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
.build()
@@ -101,6 +97,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
try {
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
+
event.clone().event(EventType.SEND_RESET_PASSWORD)
.user(user)
.detail(Details.USERNAME, username)
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 5a0e5bf..6c7f745 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdatePassword.java
@@ -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 829c705..de7a078 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 ae56970..baa3c4e 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -32,7 +32,6 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.*;
-import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.Urls;
import org.keycloak.services.validation.Validation;
@@ -132,20 +131,20 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
RealmModel realm = session.getContext().getRealm();
UriInfo uriInfo = session.getContext().getUri();
- int validityInSecs = realm.getAccessCodeLifespanUserAction();
+ int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
-// ExecuteActionsActionToken token = new ExecuteActionsActionToken(user.getId(), absoluteExpirationInSecs, null,
-// Collections.singletonList(UserModel.RequiredAction.VERIFY_EMAIL.name()),
-// null, null);
-// token.setAuthenticationSessionId(authenticationSession.getId());
- VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, null, authSession.getId(), user.getEmail());
+ 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);
+ session
+ .getProvider(EmailTemplateProvider.class)
+ .setRealm(realm)
+ .setUser(user)
+ .sendVerifyEmail(link, expirationInMinutes);
event.success();
} catch (EmailException e) {
logger.error("Failed to send verification email", e);
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 6e7a917..7ac2dee 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -26,6 +26,7 @@ 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;
@@ -37,23 +38,10 @@ 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.AuthenticatedClientSessionModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.ClientTemplateModel;
-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;
-import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ServicesLogger;
@@ -77,11 +65,7 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.security.PublicKey;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
/**
* Stateless object that manages authentication
@@ -92,6 +76,7 @@ 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";
@@ -522,6 +507,16 @@ public class AuthenticationManager {
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);
+ }
+ }
+
if (authSession.getAuthNote(END_AFTER_REQUIRED_ACTIONS) != null) {
LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ACCOUNT_UPDATED);
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 6f9c2c0..815ee89 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
@@ -816,7 +816,7 @@ public class UsersResource {
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
List<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
- return executeActionsEmail(id, redirectUri, clientId, actions);
+ return executeActionsEmail(id, redirectUri, clientId, null, actions);
}
@@ -831,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
*/
@@ -840,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<String> actions) {
auth.requireManage();
@@ -881,9 +883,11 @@ public class UsersResource {
}
}
- long relativeExpiration = realm.getAccessCodeLifespanUserAction();
- int expiration = Time.currentTime() + realm.getAccessCodeLifespanUserAction();
- ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, UUID.randomUUID(), actions, redirectUri, clientId);
+ if (lifespan == null) {
+ lifespan = realm.getActionTokenGeneratedByAdminLifespan();
+ }
+ int expiration = Time.currentTime() + lifespan;
+ ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, actions, redirectUri, clientId);
try {
UriBuilder builder = LoginActionsService.actionTokenProcessor(uriInfo);
@@ -894,7 +898,7 @@ public class UsersResource {
this.session.getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(user)
- .sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(relativeExpiration));
+ .sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan));
//audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success();
@@ -925,7 +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<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name());
- return executeActionsEmail(id, redirectUri, clientId, actions);
+ return executeActionsEmail(id, redirectUri, clientId, null, actions);
}
@GET
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 758b7a1..2a07253 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -504,8 +504,12 @@ public class LoginActionsService {
authSession = tokenContext.getAuthenticationSession();
event = tokenContext.getEvent();
+ event.event(handler.eventType());
- initLoginEvent(authSession);
+ 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());
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
index 6d42d25..87eaf20 100644
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
@@ -304,4 +304,12 @@ public class LoginActionsServiceChecks {
return true;
}
+
+ public static <T extends DefaultActionToken> void checkTokenWasNotUsedYet(T token, ActionTokenContext<T> 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/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 b31cdfc..9fd5c7a 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
@@ -358,8 +358,6 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
events.expectRequiredAction(EventType.VERIFY_EMAIL)
.user(testUserId)
- .detail(Details.USERNAME, "test-user@localhost")
- .detail(Details.EMAIL, "test-user@localhost")
.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
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 e5c1287..ba0b7c9 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/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index f34c320..3d99124 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
@@ -45,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;
@@ -68,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;
@@ -96,6 +98,9 @@ public class UserTest extends AbstractAdminTest {
protected InfoPage infoPage;
@Page
+ protected ErrorPage errorPage;
+
+ @Page
protected LoginPage loginPage;
public String createUser() {
@@ -546,8 +551,49 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
-// TODO:hmlnarik - return back once single-use cache would be implemented
-// assertEquals("We're sorry...", driver.getTitle());
+ assertEquals("We're sorry...", 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<String> 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
@@ -608,8 +654,7 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
-// TODO:hmlnarik - return back once single-use cache would be implemented
-// assertEquals("We're sorry...", driver.getTitle());
+ assertEquals("We're sorry...", driver.getTitle());
}
@Test
@@ -674,8 +719,7 @@ public class UserTest extends AbstractAdminTest {
driver.navigate().to(link);
-// TODO:hmlnarik - return back once single-use cache would be implemented
-// assertEquals("We're sorry...", driver.getTitle());
+ assertEquals("We're sorry...", driver.getTitle());
}
@@ -734,6 +778,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/forms/ResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index f322c52..04ee911 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
@@ -189,19 +189,17 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
public void assertSecondPasswordResetFails(String changePasswordUrl, String clientId) {
- // TODO:hmlnarik uncomment when single-use cache is implemented
-// driver.navigate().to(changePasswordUrl.trim());
-//
-// errorPage.assertCurrent();
-// assertEquals("An error occurred, please login again through your application.", errorPage.getError());
-//
-// events.expect(EventType.RESET_PASSWORD)
-// .client((String) null)
-// .session((String) null)
-// .user(userId)
-// .detail(Details.USERNAME, "login-test")
-// .error(Errors.EXPIRED_CODE)
-// .assertEvent();
+ 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
@@ -386,8 +384,8 @@ 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 {
@@ -415,7 +413,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
} finally {
setTimeOffset(0);
- realmRep.setAccessCodeLifespanUserAction(originalValue.get());
+ realmRep.setActionTokenGeneratedByUserLifespan(originalValue.get());
testRealm().update(realmRep);
}
}
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 b20eb5d..ef6f105 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/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 8b776c6..f129e45 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 bcc655f..c658bb5 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 13d8343..2c3c34f 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.emailActionsLifespan.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 fe09ebb..e850b3b 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 f0f9e58..f508716 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
@@ -142,6 +142,40 @@
</div>
<div class="form-group">
+ <label class="col-md-2 control-label" for="actionTokenGeneratedByUserLifespan" class="two-lines">{{:: 'action-token-generated-by-user-lifespan' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.actionTokenGeneratedByUserLifespan.time"
+ id="actionTokenGeneratedByUserLifespan" name="actionTokenGeneratedByUserLifespan">
+ <select class="form-control" name="actionTokenGeneratedByUserLifespanUnit" data-ng-model="realm.actionTokenGeneratedByUserLifespan.unit">
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>
+ {{:: 'action-token-generated-by-user-lifespan.tooltip' | translate}}
+ </kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="actionTokenGeneratedByAdminLifespan" class="two-lines">{{:: 'action-token-generated-by-admin-lifespan' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="realm.actionTokenGeneratedByAdminLifespan.time"
+ id="actionTokenGeneratedByAdminLifespan" name="actionTokenGeneratedByAdminLifespan">
+ <select class="form-control" name="actionTokenGeneratedByAdminLifespanUnit" data-ng-model="realm.actionTokenGeneratedByAdminLifespan.unit">
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>
+ {{:: 'action-token-generated-by-admin-lifespan.tooltip' | translate}}
+ </kc-tooltip>
+ </div>
+
+ <div class="form-group">
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
<button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
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 d213fdd..9f3512e 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
@@ -76,6 +76,20 @@
<kc-tooltip>{{:: 'credentials.reset-actions.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActionsEmailTimeout">{{:: 'credential-reset-actions-timeout' | translate}}</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="emailActionsTimeout.time"
+ id="reqActionsEmailTimeout" name="reqActionsEmailTimeout">
+ <select class="form-control" name="reqActionsEmailTimeoutUnit" data-ng-model="emailActionsTimeout.unit">
+ <option value="Minutes">{{:: 'minutes' | translate}}</option>
+ <option value="Hours">{{:: 'hours' | translate}}</option>
+ <option value="Days">{{:: 'days' | translate}}</option>
+ </select>
+ </div>
+ <kc-tooltip>{{:: 'credential-reset-actions-timeout.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
<label class="col-md-2 control-label" for="reqActionsEmail">{{:: 'reset-actions-email' | translate}}</label>
<div class="col-md-6">
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 8567376..d83cd18 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","authenticationSessions","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 83f7654..0d3b4aa 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
@@ -43,6 +43,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
<local-cache name="default">
@@ -109,6 +113,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
<transport lock-timeout="60000"/>
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 5e706dc..a76162b 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
@@ -43,6 +43,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
<local-cache name="default">
@@ -112,6 +116,10 @@
<eviction max-entries="1000" strategy="LRU"/>
<expiration max-idle="3600000" />
</local-cache>
+ <local-cache name="actionTokens">
+ <eviction max-entries="-1" strategy="NONE"/>
+ <expiration max-idle="-1" interval="300000"/>
+ </local-cache>
</cache-container>
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
<transport lock-timeout="60000"/>