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 accessCodeLifespan;
|
||||||
protected Integer accessCodeLifespanUserAction;
|
protected Integer accessCodeLifespanUserAction;
|
||||||
protected Integer accessCodeLifespanLogin;
|
protected Integer accessCodeLifespanLogin;
|
||||||
|
protected Integer actionTokenGeneratedByAdminLifespan;
|
||||||
|
protected Integer actionTokenGeneratedByUserLifespan;
|
||||||
protected Boolean enabled;
|
protected Boolean enabled;
|
||||||
protected String sslRequired;
|
protected String sslRequired;
|
||||||
@Deprecated
|
@Deprecated
|
||||||
|
@ -338,6 +340,22 @@ public class RealmRepresentation {
|
||||||
this.accessCodeLifespanLogin = accessCodeLifespanLogin;
|
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() {
|
public List<String> getDefaultRoles() {
|
||||||
return defaultRoles;
|
return defaultRoles;
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,7 @@
|
||||||
<local-cache name="offlineSessions"/>
|
<local-cache name="offlineSessions"/>
|
||||||
<local-cache name="loginFailures"/>
|
<local-cache name="loginFailures"/>
|
||||||
<local-cache name="authorization"/>
|
<local-cache name="authorization"/>
|
||||||
|
<local-cache name="actionTokens"/>
|
||||||
<local-cache name="work"/>
|
<local-cache name="work"/>
|
||||||
<local-cache name="keys">
|
<local-cache name="keys">
|
||||||
<eviction max-entries="1000" strategy="LRU"/>
|
<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: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/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=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)
|
/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: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/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=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)
|
/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.AUTHORIZATION_CACHE_NAME, true);
|
||||||
cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true);
|
cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true);
|
||||||
cacheManager.getCache(InfinispanConnectionProvider.KEYS_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);
|
logger.debugv("Using container managed Infinispan cache container, lookup={1}", cacheContainerLookup);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -220,6 +221,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
|
|
||||||
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig());
|
cacheManager.defineConfiguration(InfinispanConnectionProvider.KEYS_CACHE_NAME, getKeysCacheConfig());
|
||||||
cacheManager.getCache(InfinispanConnectionProvider.KEYS_CACHE_NAME, true);
|
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) {
|
private Configuration getRevisionCacheConfig(long maxEntries) {
|
||||||
|
@ -270,4 +274,18 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
|
||||||
return cb.build();
|
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 WORK_CACHE_NAME = "work";
|
||||||
String AUTHORIZATION_CACHE_NAME = "authorization";
|
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";
|
String KEYS_CACHE_NAME = "keys";
|
||||||
int KEYS_CACHE_DEFAULT_MAX = 1000;
|
int KEYS_CACHE_DEFAULT_MAX = 1000;
|
||||||
int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
|
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);
|
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
|
@Override
|
||||||
public List<RequiredCredentialModel> getRequiredCredentials() {
|
public List<RequiredCredentialModel> getRequiredCredentials() {
|
||||||
if (isUpdated()) return updated.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 accessCodeLifespan;
|
||||||
protected int accessCodeLifespanUserAction;
|
protected int accessCodeLifespanUserAction;
|
||||||
protected int accessCodeLifespanLogin;
|
protected int accessCodeLifespanLogin;
|
||||||
|
protected int actionTokenGeneratedByAdminLifespan;
|
||||||
|
protected int actionTokenGeneratedByUserLifespan;
|
||||||
protected int notBefore;
|
protected int notBefore;
|
||||||
protected PasswordPolicy passwordPolicy;
|
protected PasswordPolicy passwordPolicy;
|
||||||
protected OTPPolicy otpPolicy;
|
protected OTPPolicy otpPolicy;
|
||||||
|
@ -175,6 +177,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
accessCodeLifespan = model.getAccessCodeLifespan();
|
accessCodeLifespan = model.getAccessCodeLifespan();
|
||||||
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
|
accessCodeLifespanUserAction = model.getAccessCodeLifespanUserAction();
|
||||||
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
|
accessCodeLifespanLogin = model.getAccessCodeLifespanLogin();
|
||||||
|
actionTokenGeneratedByAdminLifespan = model.getActionTokenGeneratedByAdminLifespan();
|
||||||
|
actionTokenGeneratedByUserLifespan = model.getActionTokenGeneratedByUserLifespan();
|
||||||
notBefore = model.getNotBefore();
|
notBefore = model.getNotBefore();
|
||||||
passwordPolicy = model.getPasswordPolicy();
|
passwordPolicy = model.getPasswordPolicy();
|
||||||
otpPolicy = model.getOTPPolicy();
|
otpPolicy = model.getOTPPolicy();
|
||||||
|
@ -399,6 +403,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
||||||
return accessCodeLifespanLogin;
|
return accessCodeLifespanLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getActionTokenGeneratedByAdminLifespan() {
|
||||||
|
return actionTokenGeneratedByAdminLifespan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getActionTokenGeneratedByUserLifespan() {
|
||||||
|
return actionTokenGeneratedByUserLifespan;
|
||||||
|
}
|
||||||
|
|
||||||
public List<RequiredCredentialModel> getRequiredCredentials() {
|
public List<RequiredCredentialModel> getRequiredCredentials() {
|
||||||
return requiredCredentials;
|
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;
|
private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
|
||||||
|
|
||||||
|
public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.models.sessions.infinispan;
|
package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
|
import org.keycloak.cluster.ClusterEvent;
|
||||||
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.infinispan.context.Flag;
|
import org.infinispan.context.Flag;
|
||||||
import org.keycloak.models.KeycloakTransaction;
|
import org.keycloak.models.KeycloakTransaction;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
|
@ -32,12 +35,12 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
private final static Logger log = Logger.getLogger(InfinispanKeycloakTransaction.class);
|
private final static Logger log = Logger.getLogger(InfinispanKeycloakTransaction.class);
|
||||||
|
|
||||||
public enum CacheOperation {
|
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 active;
|
||||||
private boolean rollback;
|
private boolean rollback;
|
||||||
private final Map<Object, CacheTask> tasks = new HashMap<>();
|
private final Map<Object, CacheTask> tasks = new LinkedHashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void begin() {
|
public void begin() {
|
||||||
|
@ -80,7 +83,28 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
if (tasks.containsKey(taskKey)) {
|
if (tasks.containsKey(taskKey)) {
|
||||||
throw new IllegalStateException("Can't add session: task in progress for session");
|
throw new IllegalStateException("Can't add session: task in progress for session");
|
||||||
} else {
|
} 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)) {
|
if (tasks.containsKey(taskKey)) {
|
||||||
throw new IllegalStateException("Can't add session: task in progress for session");
|
throw new IllegalStateException("Can't add session: task in progress for session");
|
||||||
} else {
|
} 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);
|
Object taskKey = getTaskKey(cache, key);
|
||||||
CacheTask current = tasks.get(taskKey);
|
CacheTask current = tasks.get(taskKey);
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
switch (current.operation) {
|
if (current instanceof CacheTaskWithValue) {
|
||||||
case ADD:
|
((CacheTaskWithValue<V>) current).setValue(value);
|
||||||
case ADD_IF_ABSENT:
|
|
||||||
case REPLACE:
|
|
||||||
current.value = value;
|
|
||||||
return;
|
|
||||||
case REMOVE:
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
public <K, V> void remove(Cache<K, V> cache, K key) {
|
||||||
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
|
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
|
||||||
|
|
||||||
Object taskKey = getTaskKey(cache, 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
|
// 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) {
|
public <K, V> V get(Cache<K, V> cache, K key) {
|
||||||
Object taskKey = getTaskKey(cache, key);
|
Object taskKey = getTaskKey(cache, key);
|
||||||
CacheTask<K, V> current = tasks.get(taskKey);
|
CacheTask<V> current = tasks.get(taskKey);
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
switch (current.operation) {
|
if (current instanceof CacheTaskWithValue) {
|
||||||
case ADD:
|
return ((CacheTaskWithValue<V>) current).getValue();
|
||||||
case ADD_IF_ABSENT:
|
|
||||||
case REPLACE:
|
|
||||||
return current.value;
|
|
||||||
case REMOVE:
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should we have per-transaction cache for lookups?
|
// Should we have per-transaction cache for lookups?
|
||||||
|
@ -151,46 +190,29 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CacheTask<K, V> {
|
public interface CacheTask<V> {
|
||||||
private final Cache<K, V> cache;
|
void execute();
|
||||||
private final CacheOperation operation;
|
}
|
||||||
private final K key;
|
|
||||||
private V value;
|
|
||||||
|
|
||||||
public CacheTask(Cache<K, V> cache, CacheOperation operation, K key, V value) {
|
public abstract class CacheTaskWithValue<V> implements CacheTask<V> {
|
||||||
this.cache = cache;
|
protected V value;
|
||||||
this.operation = operation;
|
|
||||||
this.key = key;
|
public CacheTaskWithValue(V value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execute() {
|
public V getValue() {
|
||||||
log.tracev("Executing cache operation: {0} on {1}", operation, key);
|
return value;
|
||||||
|
|
||||||
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 void setValue(V value) {
|
||||||
// Ignore return values. Should have better performance within cluster / cross-dc env
|
this.value = value;
|
||||||
private Cache<K, V> decorateCache() {
|
|
||||||
return cache.getAdvancedCache()
|
|
||||||
.withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_REMOTE_LOOKUP);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
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) {
|
protected RequiredCredentialModel initRequiredCredentialModel(String type) {
|
||||||
RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
|
RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
|
|
|
@ -26,4 +26,8 @@ public interface RealmAttributes {
|
||||||
|
|
||||||
String DISPLAY_NAME_HTML = "displayNameHtml";
|
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
|
* @return
|
||||||
*/
|
*/
|
||||||
String getDisplayText();
|
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.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
||||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||||
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
||||||
|
rep.setActionTokenGeneratedByAdminLifespan(realm.getActionTokenGeneratedByAdminLifespan());
|
||||||
|
rep.setActionTokenGeneratedByUserLifespan(realm.getActionTokenGeneratedByUserLifespan());
|
||||||
rep.setSmtpServer(new HashMap<>(realm.getSmtpConfig()));
|
rep.setSmtpServer(new HashMap<>(realm.getSmtpConfig()));
|
||||||
rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders());
|
rep.setBrowserSecurityHeaders(realm.getBrowserSecurityHeaders());
|
||||||
rep.setAccountTheme(realm.getAccountTheme());
|
rep.setAccountTheme(realm.getAccountTheme());
|
||||||
|
|
|
@ -189,6 +189,14 @@ public class RepresentationToModel {
|
||||||
newRealm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
|
newRealm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
|
||||||
else newRealm.setAccessCodeLifespanLogin(1800);
|
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)
|
if (rep.getSslRequired() != null)
|
||||||
newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
|
newRealm.setSslRequired(SslRequired.valueOf(rep.getSslRequired().toUpperCase()));
|
||||||
if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
|
if (rep.isRegistrationAllowed() != null) newRealm.setRegistrationAllowed(rep.isRegistrationAllowed());
|
||||||
|
@ -812,6 +820,10 @@ public class RepresentationToModel {
|
||||||
realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
|
realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
|
||||||
if (rep.getAccessCodeLifespanLogin() != null)
|
if (rep.getAccessCodeLifespanLogin() != null)
|
||||||
realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
|
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.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
|
||||||
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
|
||||||
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public interface AuthenticationSessionProviderFactory extends ProviderFactory<AuthenticationSessionProvider> {
|
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.UserStorageProviderSpi
|
||||||
org.keycloak.storage.federated.UserFederatedStorageProviderSpi
|
org.keycloak.storage.federated.UserFederatedStorageProviderSpi
|
||||||
org.keycloak.models.RealmSpi
|
org.keycloak.models.RealmSpi
|
||||||
|
org.keycloak.models.ActionTokenStoreSpi
|
||||||
org.keycloak.models.UserSessionSpi
|
org.keycloak.models.UserSessionSpi
|
||||||
org.keycloak.models.UserSpi
|
org.keycloak.models.UserSpi
|
||||||
org.keycloak.models.session.UserSessionPersisterSpi
|
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);
|
void setAccessCodeLifespanLogin(int seconds);
|
||||||
|
|
||||||
|
int getActionTokenGeneratedByAdminLifespan();
|
||||||
|
void setActionTokenGeneratedByAdminLifespan(int seconds);
|
||||||
|
|
||||||
|
int getActionTokenGeneratedByUserLifespan();
|
||||||
|
void setActionTokenGeneratedByUserLifespan(int seconds);
|
||||||
|
|
||||||
List<RequiredCredentialModel> getRequiredCredentials();
|
List<RequiredCredentialModel> getRequiredCredentials();
|
||||||
|
|
||||||
void addRequiredCredential(String cred);
|
void addRequiredCredential(String cred);
|
||||||
|
|
|
@ -17,13 +17,11 @@
|
||||||
package org.keycloak.authentication.actiontoken;
|
package org.keycloak.authentication.actiontoken;
|
||||||
|
|
||||||
import org.keycloak.Config.Scope;
|
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.events.EventType;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -92,4 +90,15 @@ public abstract class AbstractActionTokenHander<T extends DefaultActionToken> im
|
||||||
return token == null ? null : token.getAuthenticationSessionId();
|
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;
|
package org.keycloak.authentication.actiontoken;
|
||||||
|
|
||||||
import org.keycloak.TokenVerifier.Predicate;
|
import org.keycloak.TokenVerifier.Predicate;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
|
||||||
import org.keycloak.common.VerificationException;
|
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler of the action token.
|
* Handler of the action token.
|
||||||
*
|
*
|
||||||
|
* @param <T> Class implementing the action token
|
||||||
|
*
|
||||||
* @author hmlnarik
|
* @author hmlnarik
|
||||||
*/
|
*/
|
||||||
public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
||||||
|
@ -42,7 +40,6 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
||||||
* @param token
|
* @param token
|
||||||
* @param tokenContext
|
* @param tokenContext
|
||||||
* @return
|
* @return
|
||||||
* @throws VerificationException
|
|
||||||
*/
|
*/
|
||||||
Response handleToken(T token, ActionTokenContext<T> tokenContext);
|
Response handleToken(T token, ActionTokenContext<T> tokenContext);
|
||||||
|
|
||||||
|
@ -96,10 +93,12 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
||||||
* @param tokenContext
|
* @param tokenContext
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
default AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext) {
|
AuthenticationSessionModel startFreshAuthenticationSession(T token, ActionTokenContext<T> tokenContext);
|
||||||
AuthenticationSessionModel authSession = tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
|
|
||||||
authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
|
|
||||||
return authSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.common.util.Time;
|
||||||
import org.keycloak.jose.jws.JWSBuilder;
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
import org.keycloak.models.KeyManager;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
@ -37,12 +35,11 @@ import javax.ws.rs.core.UriInfo;
|
||||||
*
|
*
|
||||||
* @author hmlnarik
|
* @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 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) {
|
if (t.getActionVerificationNonce() == null) {
|
||||||
throw new VerificationException("Nonce not present.");
|
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.
|
* 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() {
|
public DefaultActionToken() {
|
||||||
super(null, null);
|
super(null, null, 0, null);
|
||||||
}
|
|
||||||
|
|
||||||
public DefaultActionToken(String userId, String actionId, int expirationInSecs) {
|
|
||||||
this(userId, actionId, expirationInSecs, UUID.randomUUID());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,11 +62,20 @@ public class DefaultActionToken extends DefaultActionTokenKey {
|
||||||
* @param actionVerificationNonce
|
* @param actionVerificationNonce
|
||||||
*/
|
*/
|
||||||
protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
|
protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce) {
|
||||||
super(userId, actionId);
|
super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
|
||||||
this.actionVerificationNonce = actionVerificationNonce == null ? UUID.randomUUID() : actionVerificationNonce;
|
|
||||||
expiration = absoluteExpirationInSecs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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)
|
@JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
|
||||||
public String getAuthenticationSessionId() {
|
public String getAuthenticationSessionId() {
|
||||||
|
@ -88,11 +87,8 @@ public class DefaultActionToken extends DefaultActionTokenKey {
|
||||||
setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
|
setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID getActionVerificationNonce() {
|
|
||||||
return actionVerificationNonce;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
@Override
|
||||||
public Map<String, String> getNotes() {
|
public Map<String, String> getNotes() {
|
||||||
Map<String, String> res = new HashMap<>();
|
Map<String, String> res = new HashMap<>();
|
||||||
if (getAuthenticationSessionId() != null) {
|
if (getAuthenticationSessionId() != null) {
|
||||||
|
@ -101,6 +97,7 @@ public class DefaultActionToken extends DefaultActionTokenKey {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getNote(String name) {
|
public String getNote(String name) {
|
||||||
Object res = getOtherClaims().get(name);
|
Object res = getOtherClaims().get(name);
|
||||||
return res instanceof String ? (String) res : null;
|
return res instanceof String ? (String) res : null;
|
||||||
|
|
|
@ -16,31 +16,64 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authentication.actiontoken;
|
package org.keycloak.authentication.actiontoken;
|
||||||
|
|
||||||
|
import org.keycloak.models.ActionTokenKeyModel;
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author hmlnarik
|
* @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 */
|
/** The authenticationSession note with ID of the user authenticated via the action token */
|
||||||
public static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER";
|
public static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER";
|
||||||
|
|
||||||
public DefaultActionTokenKey(String userId, String actionId) {
|
public static final String JSON_FIELD_ACTION_VERIFICATION_NONCE = "nonce";
|
||||||
subject = userId;
|
|
||||||
type = actionId;
|
@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
|
@JsonIgnore
|
||||||
|
@Override
|
||||||
public String getUserId() {
|
public String getUserId() {
|
||||||
return getSubject();
|
return getSubject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
|
@Override
|
||||||
public String getActionId() {
|
public String getActionId() {
|
||||||
return getType();
|
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;
|
package org.keycloak.authentication.actiontoken.execactions;
|
||||||
|
|
||||||
import org.keycloak.TokenVerifier;
|
|
||||||
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
||||||
import org.keycloak.common.VerificationException;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
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_REQUIRED_ACTIONS = "rqac";
|
||||||
private static final String JSON_FIELD_REDIRECT_URI = "reduri";
|
private static final String JSON_FIELD_REDIRECT_URI = "reduri";
|
||||||
|
|
||||||
public ExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, List<String> requiredActions, String redirectUri, String clientId) {
|
public ExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, List<String> requiredActions, String redirectUri, String clientId) {
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);
|
||||||
setRequiredActions(requiredActions == null ? new LinkedList<>() : new LinkedList<>(requiredActions));
|
setRequiredActions(requiredActions == null ? new LinkedList<>() : new LinkedList<>(requiredActions));
|
||||||
setRedirectUri(redirectUri);
|
setRedirectUri(redirectUri);
|
||||||
this.issuedFor = clientId;
|
this.issuedFor = clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExecuteActionsActionToken() {
|
private ExecuteActionsActionToken() {
|
||||||
super(null, TOKEN_TYPE, -1, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
|
@JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
|
||||||
|
@ -72,14 +68,4 @@ public class ExecuteActionsActionToken extends DefaultActionToken {
|
||||||
setOtherClaims(JSON_FIELD_REDIRECT_URI, redirectUri);
|
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;
|
package org.keycloak.authentication.actiontoken.execactions;
|
||||||
|
|
||||||
import org.keycloak.TokenVerifier.Predicate;
|
import org.keycloak.TokenVerifier.Predicate;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
import org.keycloak.authentication.actiontoken.*;
|
import org.keycloak.authentication.actiontoken.*;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventType;
|
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.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import java.util.Objects;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +51,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
|
||||||
public Predicate<? super ExecuteActionsActionToken>[] getVerifiers(ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
|
public Predicate<? super ExecuteActionsActionToken>[] getVerifiers(ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
|
||||||
return TokenUtils.predicates(
|
return TokenUtils.predicates(
|
||||||
TokenUtils.checkThat(
|
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
|
t -> t.getRedirectUri() == null
|
||||||
|| RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), t.getRedirectUri(),
|
|| RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), t.getRedirectUri(),
|
||||||
tokenContext.getRealm(), tokenContext.getAuthenticationSession().getClient()) != null,
|
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());
|
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);
|
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;
|
package org.keycloak.authentication.actiontoken.idpverifyemail;
|
||||||
|
|
||||||
import org.keycloak.authentication.actiontoken.verifyemail.*;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import java.util.UUID;
|
|
||||||
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,16 +37,14 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
|
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
|
||||||
private String identityProviderAlias;
|
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) {
|
String identityProviderUsername, String identityProviderAlias) {
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||||
setAuthenticationSessionId(authenticationSessionId);
|
|
||||||
this.identityProviderUsername = identityProviderUsername;
|
this.identityProviderUsername = identityProviderUsername;
|
||||||
this.identityProviderAlias = identityProviderAlias;
|
this.identityProviderAlias = identityProviderAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IdpVerifyAccountLinkActionToken() {
|
private IdpVerifyAccountLinkActionToken() {
|
||||||
super(null, TOKEN_TYPE, -1, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getIdentityProviderUsername() {
|
public String getIdentityProviderUsername() {
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authentication.actiontoken.resetcred;
|
package org.keycloak.authentication.actiontoken.resetcred;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import java.util.UUID;
|
|
||||||
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,26 +26,11 @@ import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
||||||
public class ResetCredentialsActionToken extends DefaultActionToken {
|
public class ResetCredentialsActionToken extends DefaultActionToken {
|
||||||
|
|
||||||
public static final String TOKEN_TYPE = "reset-credentials";
|
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)
|
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId) {
|
||||||
private Long lastChangedPasswordTimestamp;
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||||
|
|
||||||
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId, Long lastChangedPasswordTimestamp) {
|
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
|
|
||||||
setAuthenticationSessionId(authenticationSessionId);
|
|
||||||
this.lastChangedPasswordTimestamp = lastChangedPasswordTimestamp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResetCredentialsActionToken() {
|
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.events.EventType;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
import org.keycloak.services.resources.LoginActionsServiceChecks.IsActionRequired;
|
import org.keycloak.services.resources.LoginActionsServiceChecks.IsActionRequired;
|
||||||
|
@ -55,9 +54,7 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
|
||||||
return new Predicate[] {
|
return new Predicate[] {
|
||||||
TokenUtils.checkThat(tokenContext.getRealm()::isResetPasswordAllowed, Errors.NOT_ALLOWED, Messages.RESET_CREDENTIAL_NOT_ALLOWED),
|
TokenUtils.checkThat(tokenContext.getRealm()::isResetPasswordAllowed, Errors.NOT_ALLOWED, Messages.RESET_CREDENTIAL_NOT_ALLOWED),
|
||||||
|
|
||||||
new IsActionRequired(tokenContext, Action.AUTHENTICATE),
|
new IsActionRequired(tokenContext, Action.AUTHENTICATE)
|
||||||
|
|
||||||
// singleUseCheck, // TODO:hmlnarik - fix with single-use cache
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package org.keycloak.authentication.actiontoken.verifyemail;
|
package org.keycloak.authentication.actiontoken.verifyemail;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import java.util.UUID;
|
|
||||||
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
import org.keycloak.authentication.actiontoken.DefaultActionToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,15 +33,12 @@ public class VerifyEmailActionToken extends DefaultActionToken {
|
||||||
@JsonProperty(value = JSON_FIELD_EMAIL)
|
@JsonProperty(value = JSON_FIELD_EMAIL)
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId,
|
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
|
||||||
String email) {
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
|
|
||||||
setAuthenticationSessionId(authenticationSessionId);
|
|
||||||
this.email = email;
|
this.email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VerifyEmailActionToken() {
|
private VerifyEmailActionToken() {
|
||||||
super(null, TOKEN_TYPE, -1, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEmail() {
|
public String getEmail() {
|
||||||
|
|
|
@ -108,7 +108,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
||||||
UriInfo uriInfo = session.getContext().getUri();
|
UriInfo uriInfo = session.getContext().getUri();
|
||||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
|
|
||||||
int validityInSecs = realm.getAccessCodeLifespanUserAction();
|
int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan();
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||||
|
|
||||||
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
|
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
|
||||||
|
@ -120,7 +120,7 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
||||||
.removeDetail(Details.AUTH_TYPE);
|
.removeDetail(Details.AUTH_TYPE);
|
||||||
|
|
||||||
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
||||||
existingUser.getId(), absoluteExpirationInSecs, null, authSession.getId(),
|
existingUser.getId(), absoluteExpirationInSecs, authSession.getId(),
|
||||||
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
|
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
|
||||||
);
|
);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
||||||
|
|
|
@ -85,15 +85,11 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int validityInSecs = context.getRealm().getAccessCodeLifespanUserAction();
|
int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan();
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
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.
|
// We send the secret in the email in a link as a query param.
|
||||||
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs,
|
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId());
|
||||||
null, authenticationSession.getId(), lastCreatedPassword);
|
|
||||||
String link = UriBuilder
|
String link = UriBuilder
|
||||||
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
||||||
.build()
|
.build()
|
||||||
|
@ -101,6 +97,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||||
try {
|
try {
|
||||||
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
|
context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes);
|
||||||
|
|
||||||
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
||||||
.user(user)
|
.user(user)
|
||||||
.detail(Details.USERNAME, username)
|
.detail(Details.USERNAME, username)
|
||||||
|
|
|
@ -157,4 +157,9 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return UserModel.RequiredAction.UPDATE_PASSWORD.name();
|
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() {
|
public String getId() {
|
||||||
return UserModel.RequiredAction.CONFIGURE_TOTP.name();
|
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.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
|
@ -132,20 +131,20 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
UriInfo uriInfo = session.getContext().getUri();
|
UriInfo uriInfo = session.getContext().getUri();
|
||||||
|
|
||||||
int validityInSecs = realm.getAccessCodeLifespanUserAction();
|
int validityInSecs = realm.getActionTokenGeneratedByUserLifespan();
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
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));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
||||||
String link = builder.build(realm.getName()).toString();
|
String link = builder.build(realm.getName()).toString();
|
||||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||||
|
|
||||||
try {
|
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();
|
event.success();
|
||||||
} catch (EmailException e) {
|
} catch (EmailException e) {
|
||||||
logger.error("Failed to send verification email", 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.RequiredActionContextResult;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
@ -37,23 +38,10 @@ import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.jose.jws.AlgorithmType;
|
import org.keycloak.jose.jws.AlgorithmType;
|
||||||
import org.keycloak.jose.jws.JWSBuilder;
|
import org.keycloak.jose.jws.JWSBuilder;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.*;
|
||||||
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.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.LoginProtocol.Error;
|
import org.keycloak.protocol.LoginProtocol.Error;
|
||||||
import org.keycloak.protocol.RestartLoginCookie;
|
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
|
@ -77,11 +65,7 @@ import javax.ws.rs.core.UriBuilder;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Collection;
|
import java.util.*;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stateless object that manages authentication
|
* Stateless object that manages authentication
|
||||||
|
@ -92,6 +76,7 @@ import java.util.Set;
|
||||||
public class AuthenticationManager {
|
public class AuthenticationManager {
|
||||||
public static final String SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS= "SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS";
|
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 END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
|
||||||
|
public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN";
|
||||||
|
|
||||||
// Last authenticated client in userSession.
|
// Last authenticated client in userSession.
|
||||||
public static final String LAST_AUTHENTICATED_CLIENT = "LAST_AUTHENTICATED_CLIENT";
|
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,
|
public static Response finishedRequiredActions(KeycloakSession session, AuthenticationSessionModel authSession, UserSessionModel userSession,
|
||||||
ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) {
|
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) {
|
if (authSession.getAuthNote(END_AFTER_REQUIRED_ACTIONS) != null) {
|
||||||
LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class)
|
LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class)
|
||||||
.setSuccess(Messages.ACCOUNT_UPDATED);
|
.setSuccess(Messages.ACCOUNT_UPDATED);
|
||||||
|
|
|
@ -504,8 +504,12 @@ public class LoginActionsService {
|
||||||
|
|
||||||
authSession = tokenContext.getAuthenticationSession();
|
authSession = tokenContext.getAuthenticationSession();
|
||||||
event = tokenContext.getEvent();
|
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());
|
authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
|
||||||
|
|
||||||
|
|
|
@ -304,4 +304,12 @@ public class LoginActionsServiceChecks {
|
||||||
|
|
||||||
return true;
|
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) {
|
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
|
||||||
List<String> actions = new LinkedList<>();
|
List<String> actions = new LinkedList<>();
|
||||||
actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
|
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 id User is
|
||||||
* @param redirectUri Redirect uri
|
* @param redirectUri Redirect uri
|
||||||
* @param clientId Client id
|
* @param clientId Client id
|
||||||
|
* @param lifespan Number of seconds after which the generated token expires
|
||||||
* @param actions required actions the user needs to complete
|
* @param actions required actions the user needs to complete
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@ -840,6 +841,7 @@ public class UsersResource {
|
||||||
public Response executeActionsEmail(@PathParam("id") String id,
|
public Response executeActionsEmail(@PathParam("id") String id,
|
||||||
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
|
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
|
||||||
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId,
|
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId,
|
||||||
|
@QueryParam("lifespan") Integer lifespan,
|
||||||
List<String> actions) {
|
List<String> actions) {
|
||||||
auth.requireManage();
|
auth.requireManage();
|
||||||
|
|
||||||
|
@ -881,9 +883,11 @@ public class UsersResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long relativeExpiration = realm.getAccessCodeLifespanUserAction();
|
if (lifespan == null) {
|
||||||
int expiration = Time.currentTime() + realm.getAccessCodeLifespanUserAction();
|
lifespan = realm.getActionTokenGeneratedByAdminLifespan();
|
||||||
ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, UUID.randomUUID(), actions, redirectUri, clientId);
|
}
|
||||||
|
int expiration = Time.currentTime() + lifespan;
|
||||||
|
ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, actions, redirectUri, clientId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UriBuilder builder = LoginActionsService.actionTokenProcessor(uriInfo);
|
UriBuilder builder = LoginActionsService.actionTokenProcessor(uriInfo);
|
||||||
|
@ -894,7 +898,7 @@ public class UsersResource {
|
||||||
this.session.getProvider(EmailTemplateProvider.class)
|
this.session.getProvider(EmailTemplateProvider.class)
|
||||||
.setRealm(realm)
|
.setRealm(realm)
|
||||||
.setUser(user)
|
.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();
|
//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) {
|
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<>();
|
List<String> actions = new LinkedList<>();
|
||||||
actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name());
|
actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name());
|
||||||
return executeActionsEmail(id, redirectUri, clientId, actions);
|
return executeActionsEmail(id, redirectUri, clientId, null, actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
|
|
@ -358,8 +358,6 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
||||||
|
|
||||||
events.expectRequiredAction(EventType.VERIFY_EMAIL)
|
events.expectRequiredAction(EventType.VERIFY_EMAIL)
|
||||||
.user(testUserId)
|
.user(testUserId)
|
||||||
.detail(Details.USERNAME, "test-user@localhost")
|
|
||||||
.detail(Details.EMAIL, "test-user@localhost")
|
|
||||||
.detail(Details.CODE_ID, Matchers.not(Matchers.is(mailCodeId)))
|
.detail(Details.CODE_ID, Matchers.not(Matchers.is(mailCodeId)))
|
||||||
.client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID) // as authentication sessions are browser-specific,
|
.client(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID) // as authentication sessions are browser-specific,
|
||||||
// the client and redirect_uri is unrelated to
|
// 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.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.services.resources.RealmsResource;
|
import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
|
import org.keycloak.testsuite.page.LoginPasswordUpdatePage;
|
||||||
|
import org.keycloak.testsuite.pages.ErrorPage;
|
||||||
import org.keycloak.testsuite.pages.InfoPage;
|
import org.keycloak.testsuite.pages.InfoPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
import org.keycloak.testsuite.util.AdminEventPaths;
|
||||||
|
@ -68,6 +69,7 @@ import java.util.Collections;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
@ -95,6 +97,9 @@ public class UserTest extends AbstractAdminTest {
|
||||||
@Page
|
@Page
|
||||||
protected InfoPage infoPage;
|
protected InfoPage infoPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
@Page
|
@Page
|
||||||
protected LoginPage loginPage;
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@ -546,8 +551,49 @@ public class UserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
driver.navigate().to(link);
|
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
|
@Test
|
||||||
|
@ -608,8 +654,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
driver.navigate().to(link);
|
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
|
@Test
|
||||||
|
@ -674,8 +719,7 @@ public class UserTest extends AbstractAdminTest {
|
||||||
|
|
||||||
driver.navigate().to(link);
|
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);
|
driver.navigate().to(link);
|
||||||
|
|
||||||
Assert.assertEquals("Your account has been updated.", infoPage.getInfo());
|
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
|
@Test
|
||||||
|
|
|
@ -255,6 +255,8 @@ public class RealmTest extends AbstractAdminTest {
|
||||||
rep.setSsoSessionIdleTimeout(123);
|
rep.setSsoSessionIdleTimeout(123);
|
||||||
rep.setSsoSessionMaxLifespan(12);
|
rep.setSsoSessionMaxLifespan(12);
|
||||||
rep.setAccessCodeLifespanLogin(1234);
|
rep.setAccessCodeLifespanLogin(1234);
|
||||||
|
rep.setActionTokenGeneratedByAdminLifespan(2345);
|
||||||
|
rep.setActionTokenGeneratedByUserLifespan(3456);
|
||||||
rep.setRegistrationAllowed(true);
|
rep.setRegistrationAllowed(true);
|
||||||
rep.setRegistrationEmailAsUsername(true);
|
rep.setRegistrationEmailAsUsername(true);
|
||||||
rep.setEditUsernameAllowed(true);
|
rep.setEditUsernameAllowed(true);
|
||||||
|
@ -267,6 +269,8 @@ public class RealmTest extends AbstractAdminTest {
|
||||||
assertEquals(123, rep.getSsoSessionIdleTimeout().intValue());
|
assertEquals(123, rep.getSsoSessionIdleTimeout().intValue());
|
||||||
assertEquals(12, rep.getSsoSessionMaxLifespan().intValue());
|
assertEquals(12, rep.getSsoSessionMaxLifespan().intValue());
|
||||||
assertEquals(1234, rep.getAccessCodeLifespanLogin().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.isRegistrationAllowed());
|
||||||
assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername());
|
assertEquals(Boolean.TRUE, rep.isRegistrationEmailAsUsername());
|
||||||
assertEquals(Boolean.TRUE, rep.isEditUsernameAllowed());
|
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.getAccessCodeLifespan() != null) assertEquals(realm.getAccessCodeLifespan(), storedRealm.getAccessCodeLifespan());
|
||||||
if (realm.getAccessCodeLifespanUserAction() != null)
|
if (realm.getAccessCodeLifespanUserAction() != null)
|
||||||
assertEquals(realm.getAccessCodeLifespanUserAction(), storedRealm.getAccessCodeLifespanUserAction());
|
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.getNotBefore() != null) assertEquals(realm.getNotBefore(), storedRealm.getNotBefore());
|
||||||
if (realm.getAccessTokenLifespan() != null) assertEquals(realm.getAccessTokenLifespan(), storedRealm.getAccessTokenLifespan());
|
if (realm.getAccessTokenLifespan() != null) assertEquals(realm.getAccessTokenLifespan(), storedRealm.getAccessTokenLifespan());
|
||||||
if (realm.getAccessTokenLifespanForImplicitFlow() != null) assertEquals(realm.getAccessTokenLifespanForImplicitFlow(), storedRealm.getAccessTokenLifespanForImplicitFlow());
|
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) {
|
public void assertSecondPasswordResetFails(String changePasswordUrl, String clientId) {
|
||||||
// TODO:hmlnarik uncomment when single-use cache is implemented
|
driver.navigate().to(changePasswordUrl.trim());
|
||||||
// driver.navigate().to(changePasswordUrl.trim());
|
|
||||||
//
|
errorPage.assertCurrent();
|
||||||
// errorPage.assertCurrent();
|
assertEquals("Action expired. Please continue with login now.", errorPage.getError());
|
||||||
// assertEquals("An error occurred, please login again through your application.", errorPage.getError());
|
|
||||||
//
|
events.expect(EventType.RESET_PASSWORD)
|
||||||
// events.expect(EventType.RESET_PASSWORD)
|
.client("account")
|
||||||
// .client((String) null)
|
.session((String) null)
|
||||||
// .session((String) null)
|
.user(userId)
|
||||||
// .user(userId)
|
.error(Errors.EXPIRED_CODE)
|
||||||
// .detail(Details.USERNAME, "login-test")
|
.assertEvent();
|
||||||
// .error(Errors.EXPIRED_CODE)
|
|
||||||
// .assertEvent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -386,8 +384,8 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
||||||
final AtomicInteger originalValue = new AtomicInteger();
|
final AtomicInteger originalValue = new AtomicInteger();
|
||||||
|
|
||||||
RealmRepresentation realmRep = testRealm().toRepresentation();
|
RealmRepresentation realmRep = testRealm().toRepresentation();
|
||||||
originalValue.set(realmRep.getAccessCodeLifespan());
|
originalValue.set(realmRep.getActionTokenGeneratedByUserLifespan());
|
||||||
realmRep.setAccessCodeLifespanUserAction(60);
|
realmRep.setActionTokenGeneratedByUserLifespan(60);
|
||||||
testRealm().update(realmRep);
|
testRealm().update(realmRep);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -415,7 +413,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
|
||||||
} finally {
|
} finally {
|
||||||
setTimeOffset(0);
|
setTimeOffset(0);
|
||||||
|
|
||||||
realmRep.setAccessCodeLifespanUserAction(originalValue.get());
|
realmRep.setActionTokenGeneratedByUserLifespan(originalValue.get());
|
||||||
testRealm().update(realmRep);
|
testRealm().update(realmRep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||||
"requiredCredentials": [ "password" ],
|
"requiredCredentials": [ "password" ],
|
||||||
"defaultRoles": [ "user" ],
|
"defaultRoles": [ "user" ],
|
||||||
|
"actionTokenGeneratedByAdminLifespan": "147",
|
||||||
|
"actionTokenGeneratedByUserLifespan": "258",
|
||||||
"smtpServer": {
|
"smtpServer": {
|
||||||
"from": "auto@keycloak.org",
|
"from": "auto@keycloak.org",
|
||||||
"host": "localhost",
|
"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.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=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'.
|
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=Client login timeout
|
||||||
client-login-timeout.tooltip=Max time an client has to finish the access token protocol. This should normally be 1 minute.
|
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
|
login-timeout=Login timeout
|
||||||
|
@ -1292,6 +1296,8 @@ credential-types=Credential Types
|
||||||
manage-user-password=Manage Password
|
manage-user-password=Manage Password
|
||||||
disable-credentials=Disable Credentials
|
disable-credentials=Disable Credentials
|
||||||
credential-reset-actions=Credential Reset
|
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
|
ldap-mappers=LDAP Mappers
|
||||||
create-ldap-mapper=Create LDAP mapper
|
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.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan);
|
||||||
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
|
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
|
||||||
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
|
$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);
|
var oldCopy = angular.copy($scope.realm);
|
||||||
$scope.changed = false;
|
$scope.changed = false;
|
||||||
|
@ -1063,6 +1065,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
||||||
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
|
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
|
||||||
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
|
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
|
||||||
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.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 () {
|
Realm.update($scope.realm, function () {
|
||||||
$route.reload();
|
$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');
|
console.log('UserCredentialsCtrl');
|
||||||
|
|
||||||
$scope.realm = realm;
|
$scope.realm = realm;
|
||||||
|
@ -548,6 +548,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.emailActions = [];
|
$scope.emailActions = [];
|
||||||
|
$scope.emailActionsTimeout = TimeUnit2.asUnit(realm.actionTokenGeneratedByAdminLifespan);
|
||||||
$scope.disableableCredentialTypes = [];
|
$scope.disableableCredentialTypes = [];
|
||||||
|
|
||||||
$scope.sendExecuteActionsEmail = function() {
|
$scope.sendExecuteActionsEmail = function() {
|
||||||
|
@ -556,7 +557,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, R
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Dialog.confirm('Send Email', 'Are you sure you want to send email to user?', function() {
|
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");
|
Notifications.success("Email sent to user");
|
||||||
$scope.emailActions = [];
|
$scope.emailActions = [];
|
||||||
}, function() {
|
}, function() {
|
||||||
|
|
|
@ -496,7 +496,8 @@ module.factory('UserCredentials', function($resource) {
|
||||||
module.factory('UserExecuteActionsEmail', function($resource) {
|
module.factory('UserExecuteActionsEmail', function($resource) {
|
||||||
return $resource(authUrl + '/admin/realms/:realm/users/:userId/execute-actions-email', {
|
return $resource(authUrl + '/admin/realms/:realm/users/:userId/execute-actions-email', {
|
||||||
realm : '@realm',
|
realm : '@realm',
|
||||||
userId : '@userId'
|
userId : '@userId',
|
||||||
|
lifespan : '@lifespan',
|
||||||
}, {
|
}, {
|
||||||
update : {
|
update : {
|
||||||
method : 'PUT'
|
method : 'PUT'
|
||||||
|
|
|
@ -141,6 +141,40 @@
|
||||||
</kc-tooltip>
|
</kc-tooltip>
|
||||||
</div>
|
</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="form-group">
|
||||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||||
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
|
||||||
|
|
|
@ -75,6 +75,20 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'credentials.reset-actions.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'credentials.reset-actions.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group clearfix">
|
||||||
<label class="col-md-2 control-label" for="reqActionsEmail">{{:: 'reset-actions-email' | translate}}</label>
|
<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 {
|
public class KeycloakServerDeploymentProcessor implements DeploymentUnitProcessor {
|
||||||
|
|
||||||
private static final String[] CACHES = new String[] {
|
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
|
// This param name is defined again in Keycloak Services class
|
||||||
// org.keycloak.services.resources.KeycloakApplication. We have this value in
|
// 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";
|
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -43,6 +43,10 @@
|
||||||
<eviction max-entries="1000" strategy="LRU"/>
|
<eviction max-entries="1000" strategy="LRU"/>
|
||||||
<expiration max-idle="3600000" />
|
<expiration max-idle="3600000" />
|
||||||
</local-cache>
|
</local-cache>
|
||||||
|
<local-cache name="actionTokens">
|
||||||
|
<eviction max-entries="-1" strategy="NONE"/>
|
||||||
|
<expiration max-idle="-1" interval="300000"/>
|
||||||
|
</local-cache>
|
||||||
</cache-container>
|
</cache-container>
|
||||||
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
|
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
|
||||||
<local-cache name="default">
|
<local-cache name="default">
|
||||||
|
@ -109,6 +113,10 @@
|
||||||
<eviction max-entries="1000" strategy="LRU"/>
|
<eviction max-entries="1000" strategy="LRU"/>
|
||||||
<expiration max-idle="3600000" />
|
<expiration max-idle="3600000" />
|
||||||
</local-cache>
|
</local-cache>
|
||||||
|
<local-cache name="actionTokens">
|
||||||
|
<eviction max-entries="-1" strategy="NONE"/>
|
||||||
|
<expiration max-idle="-1" interval="300000"/>
|
||||||
|
</local-cache>
|
||||||
</cache-container>
|
</cache-container>
|
||||||
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
|
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
|
||||||
<transport lock-timeout="60000"/>
|
<transport lock-timeout="60000"/>
|
||||||
|
|
|
@ -43,6 +43,10 @@
|
||||||
<eviction max-entries="1000" strategy="LRU"/>
|
<eviction max-entries="1000" strategy="LRU"/>
|
||||||
<expiration max-idle="3600000" />
|
<expiration max-idle="3600000" />
|
||||||
</local-cache>
|
</local-cache>
|
||||||
|
<local-cache name="actionTokens">
|
||||||
|
<eviction max-entries="-1" strategy="NONE"/>
|
||||||
|
<expiration max-idle="-1" interval="300000"/>
|
||||||
|
</local-cache>
|
||||||
</cache-container>
|
</cache-container>
|
||||||
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
|
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
|
||||||
<local-cache name="default">
|
<local-cache name="default">
|
||||||
|
@ -112,6 +116,10 @@
|
||||||
<eviction max-entries="1000" strategy="LRU"/>
|
<eviction max-entries="1000" strategy="LRU"/>
|
||||||
<expiration max-idle="3600000" />
|
<expiration max-idle="3600000" />
|
||||||
</local-cache>
|
</local-cache>
|
||||||
|
<local-cache name="actionTokens">
|
||||||
|
<eviction max-entries="-1" strategy="NONE"/>
|
||||||
|
<expiration max-idle="-1" interval="300000"/>
|
||||||
|
</local-cache>
|
||||||
</cache-container>
|
</cache-container>
|
||||||
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
|
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
|
||||||
<transport lock-timeout="60000"/>
|
<transport lock-timeout="60000"/>
|
||||||
|
|
Loading…
Reference in a new issue