KEYCLOAK-4628 Single-use cache + its functionality incorporated into reset password token. Utilize single-use cache for relevant actions in execute-actions token
This commit is contained in:
parent
db8b733610
commit
b8262a9f02
63 changed files with 1243 additions and 222 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -474,6 +474,30 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setAccessCodeLifespanLogin(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActionTokenGeneratedByAdminLifespan() {
|
||||
if (isUpdated()) return updated.getActionTokenGeneratedByAdminLifespan();
|
||||
return cached.getActionTokenGeneratedByAdminLifespan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActionTokenGeneratedByAdminLifespan(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setActionTokenGeneratedByAdminLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActionTokenGeneratedByUserLifespan() {
|
||||
if (isUpdated()) return updated.getActionTokenGeneratedByUserLifespan();
|
||||
return cached.getActionTokenGeneratedByUserLifespan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setActionTokenGeneratedByUserLifespan(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setActionTokenGeneratedByUserLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RequiredCredentialModel> getRequiredCredentials() {
|
||||
if (isUpdated()) return updated.getRequiredCredentials();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.models.sessions.infinispan.InfinispanActionTokenStoreProviderFactory
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
}
|
|
@ -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> {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
@ -95,6 +97,9 @@ public class UserTest extends AbstractAdminTest {
|
|||
@Page
|
||||
protected InfoPage infoPage;
|
||||
|
||||
@Page
|
||||
protected ErrorPage errorPage;
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -141,6 +141,40 @@
|
|||
</kc-tooltip>
|
||||
</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>
|
||||
|
|
|
@ -75,6 +75,20 @@
|
|||
</div>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
Loading…
Reference in a new issue