Merge single-use token providers into one

Fixes first part of: #11173

* Merge single-use token providers into one

* Remove PushedAuthzRequestStoreProvider

* Remove OAuth2DeviceTokenStoreProvider

* Delete SamlArtifactSessionMappingStoreProvider

* SingleUseTokenStoreProvider cleanup

* Addressing Michal's comments

* Add contains method

* Add revoked suffix

* Rename to SingleUseObjectProvider
This commit is contained in:
Martin Kanis 2022-05-11 13:58:58 +02:00 committed by GitHub
parent d3b43a9f59
commit 0d6bbd437f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 385 additions and 2060 deletions

View file

@ -1,91 +0,0 @@
/*
* 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 java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.CodeToTokenStoreProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.connections.infinispan.InfinispanUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanCodeToTokenStoreProvider implements CodeToTokenStoreProvider {
public static final Logger logger = Logger.getLogger(InfinispanCodeToTokenStoreProvider.class);
private final Supplier<BasicCache<UUID, ActionTokenValueEntity>> codeCache;
private final KeycloakSession session;
public InfinispanCodeToTokenStoreProvider(KeycloakSession session, Supplier<BasicCache<UUID, ActionTokenValueEntity>> actionKeyCache) {
this.session = session;
this.codeCache = actionKeyCache;
}
@Override
public void put(UUID codeId, int lifespanSeconds, Map<String, String> codeData) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(codeData);
try {
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
cache.put(codeId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when adding code %s", codeId);
}
throw re;
}
}
@Override
public Map<String, String> remove(UUID codeId) {
try {
BasicCache<UUID, ActionTokenValueEntity> cache = codeCache.get();
ActionTokenValueEntity existing = cache.remove(codeId);
return existing == null ? null : existing.getNotes();
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when removing code %s", codeId);
}
return null;
}
}
@Override
public void close() {
}
}

View file

@ -1,83 +0,0 @@
/*
* 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 java.util.UUID;
import java.util.function.Supplier;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.CodeToTokenStoreProvider;
import org.keycloak.models.CodeToTokenStoreProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanCodeToTokenStoreProviderFactory implements CodeToTokenStoreProviderFactory {
private static final Logger LOG = Logger.getLogger(InfinispanCodeToTokenStoreProviderFactory.class);
// Reuse "actionTokens" infinispan cache for now
private volatile Supplier<BasicCache<UUID, ActionTokenValueEntity>> codeCache;
@Override
public CodeToTokenStoreProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanCodeToTokenStoreProvider(session, codeCache);
}
private void lazyInit(KeycloakSession session) {
if (codeCache == null) {
synchronized (this) {
if (codeCache == null) {
this.codeCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan";
}
@Override
public int order() {
return PROVIDER_PRIORITY;
}
}

View file

@ -1,243 +0,0 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.authorization.policy.evaluation.Realm;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.OAuth2DeviceUserCodeModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public class InfinispanOAuth2DeviceTokenStoreProvider implements OAuth2DeviceTokenStoreProvider {
public static final Logger logger = Logger.getLogger(InfinispanOAuth2DeviceTokenStoreProvider.class);
private final Supplier<BasicCache<String, ActionTokenValueEntity>> codeCache;
private final KeycloakSession session;
public InfinispanOAuth2DeviceTokenStoreProvider(KeycloakSession session, Supplier<BasicCache<String, ActionTokenValueEntity>> actionKeyCache) {
this.session = session;
this.codeCache = actionKeyCache;
}
@Override
public OAuth2DeviceCodeModel getByDeviceCode(RealmModel realm, String deviceCode) {
try {
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
ActionTokenValueEntity existing = cache.get(OAuth2DeviceCodeModel.createKey(deviceCode));
if (existing == null) {
return null;
}
return OAuth2DeviceCodeModel.fromCache(realm, deviceCode, existing.getNotes());
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when getting device code %s", deviceCode);
}
return null;
}
}
@Override
public void close() {
}
@Override
public void put(OAuth2DeviceCodeModel deviceCode, OAuth2DeviceUserCodeModel userCode, int lifespanSeconds) {
ActionTokenValueEntity deviceCodeValue = new ActionTokenValueEntity(deviceCode.toMap());
ActionTokenValueEntity userCodeValue = new ActionTokenValueEntity(userCode.serializeValue());
try {
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
cache.put(deviceCode.serializeKey(), deviceCodeValue, lifespanSeconds, TimeUnit.SECONDS);
cache.put(userCode.serializeKey(), userCodeValue, lifespanSeconds, TimeUnit.SECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when adding device code %s and user code %s",
deviceCode.getDeviceCode(), userCode.getUserCode());
}
throw re;
}
}
@Override
public boolean isPollingAllowed(OAuth2DeviceCodeModel deviceCode) {
try {
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
String key = deviceCode.serializePollingKey();
ActionTokenValueEntity value = new ActionTokenValueEntity(null);
ActionTokenValueEntity existing = cache.putIfAbsent(key, value, deviceCode.getPollingInterval(), TimeUnit.SECONDS);
return existing == null;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when putting polling key for device code %s", deviceCode.getDeviceCode());
}
return false;
}
}
@Override
public OAuth2DeviceCodeModel getByUserCode(RealmModel realm, String userCode) {
try {
OAuth2DeviceCodeModel deviceCode = findDeviceCodeByUserCode(realm, userCode);
if (deviceCode == null) {
return null;
}
return deviceCode;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when getting device code by user code %s", userCode);
}
return null;
}
}
private OAuth2DeviceCodeModel findDeviceCodeByUserCode(RealmModel realm, String userCode) throws HotRodClientException {
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
String userCodeKey = OAuth2DeviceUserCodeModel.createKey(realm, userCode);
ActionTokenValueEntity existing = cache.get(userCodeKey);
if (existing == null) {
return null;
}
OAuth2DeviceUserCodeModel data = OAuth2DeviceUserCodeModel.fromCache(realm, userCode, existing.getNotes());
String deviceCode = data.getDeviceCode();
String deviceCodeKey = OAuth2DeviceCodeModel.createKey(deviceCode);
ActionTokenValueEntity existingDeviceCode = cache.get(deviceCodeKey);
if (existingDeviceCode == null) {
return null;
}
return OAuth2DeviceCodeModel.fromCache(realm, deviceCode, existingDeviceCode.getNotes());
}
@Override
public boolean approve(RealmModel realm, String userCode, String userSessionId, Map<String, String> additionalParams) {
try {
OAuth2DeviceCodeModel deviceCode = findDeviceCodeByUserCode(realm, userCode);
if (deviceCode == null) {
return false;
}
OAuth2DeviceCodeModel approved = deviceCode.approve(userSessionId, additionalParams);
// Update the device code with approved status
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
cache.replace(approved.serializeKey(), new ActionTokenValueEntity(approved.toMap()));
return true;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when verifying device user code %s", userCode);
}
return false;
}
}
@Override
public boolean deny(RealmModel realm, String userCode) {
try {
OAuth2DeviceCodeModel deviceCode = findDeviceCodeByUserCode(realm, userCode);
if (deviceCode == null) {
return false;
}
OAuth2DeviceCodeModel denied = deviceCode.deny();
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
cache.replace(denied.serializeKey(), new ActionTokenValueEntity(denied.toMap()));
return true;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when denying device user code %s", userCode);
}
return false;
}
}
@Override
public boolean removeDeviceCode(RealmModel realm, String deviceCode) {
try {
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
String key = OAuth2DeviceCodeModel.createKey(deviceCode);
ActionTokenValueEntity existing = cache.remove(key);
return existing == null ? false : true;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when removing device code %s", deviceCode);
}
return false;
}
}
@Override
public boolean removeUserCode(RealmModel realm, String userCode) {
try {
BasicCache<String, ActionTokenValueEntity> cache = codeCache.get();
String key = OAuth2DeviceUserCodeModel.createKey(realm, userCode);
ActionTokenValueEntity existing = cache.remove(key);
return existing == null ? false : true;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when removing user code %s", userCode);
}
return false;
}
}
}

View file

@ -1,77 +0,0 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan;
import org.infinispan.commons.api.BasicCache;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.OAuth2DeviceTokenStoreProviderFactory;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import java.util.function.Supplier;
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public class InfinispanOAuth2DeviceTokenStoreProviderFactory implements OAuth2DeviceTokenStoreProviderFactory {
// Reuse "actionTokens" infinispan cache for now
private volatile Supplier<BasicCache<String, ActionTokenValueEntity>> codeCache;
@Override
public OAuth2DeviceTokenStoreProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanOAuth2DeviceTokenStoreProvider(session, codeCache);
}
private void lazyInit(KeycloakSession session) {
if (codeCache == null) {
synchronized (this) {
codeCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan";
}
@Override
public int order() {
return PROVIDER_PRIORITY;
}
}

View file

@ -1,85 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PushedAuthzRequestStoreProvider;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.connections.infinispan.InfinispanUtil;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class InfinispanPushedAuthzRequestStoreProvider implements PushedAuthzRequestStoreProvider {
public static final Logger logger = Logger.getLogger(InfinispanPushedAuthzRequestStoreProvider.class);
private final Supplier<BasicCache<UUID, ActionTokenValueEntity>> parDataCache;
public InfinispanPushedAuthzRequestStoreProvider(KeycloakSession session, Supplier<BasicCache<UUID, ActionTokenValueEntity>> actionKeyCache) {
this.parDataCache = actionKeyCache;
}
@Override
public void put(UUID key, int lifespanSeconds, Map<String, String> codeData) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(codeData);
try {
BasicCache<UUID, ActionTokenValueEntity> cache = parDataCache.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
cache.put(key, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when adding PAR data for redirect URI: %s", key);
}
throw re;
}
}
@Override
public Map<String, String> remove(UUID key) {
try {
BasicCache<UUID, ActionTokenValueEntity> cache = parDataCache.get();
ActionTokenValueEntity existing = cache.remove(key);
return existing == null ? null : existing.getNotes();
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when removing PAR data for redirect URI %s", key);
}
return null;
}
}
@Override
public void close() {
}
}

View file

@ -1,78 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan;
import org.infinispan.commons.api.BasicCache;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.PushedAuthzRequestStoreProvider;
import org.keycloak.models.PushedAuthzRequestStoreProviderFactory;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import java.util.UUID;
import java.util.function.Supplier;
public class InfinispanPushedAuthzRequestStoreProviderFactory implements PushedAuthzRequestStoreProviderFactory, EnvironmentDependentProviderFactory {
// Reuse "actionTokens" infinispan cache for now
private volatile Supplier<BasicCache<UUID, ActionTokenValueEntity>> codeCache;
@Override
public PushedAuthzRequestStoreProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanPushedAuthzRequestStoreProvider(session, codeCache);
}
private void lazyInit(KeycloakSession session) {
if (codeCache == null) {
synchronized (this) {
if (codeCache == null) {
this.codeCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan";
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.PAR);
}
}

View file

@ -1,103 +0,0 @@
/*
* 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.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.SamlArtifactSessionMappingModel;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.connections.infinispan.InfinispanUtil;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* @author mhajas
*/
public class InfinispanSamlArtifactSessionMappingStoreProvider implements SamlArtifactSessionMappingStoreProvider {
public static final Logger logger = Logger.getLogger(InfinispanSamlArtifactSessionMappingStoreProvider.class);
private final Supplier<BasicCache<UUID, String[]>> cacheSupplier;
public InfinispanSamlArtifactSessionMappingStoreProvider(Supplier<BasicCache<UUID, String[]>> actionKeyCache) {
this.cacheSupplier = actionKeyCache;
}
@Override
public void put(String artifact, int lifespanSeconds, AuthenticatedClientSessionModel clientSessionModel) {
try {
BasicCache<UUID, String[]> cache = cacheSupplier.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
cache.put(UUID.nameUUIDFromBytes(artifact.getBytes(StandardCharsets.UTF_8)), new String[]{artifact, clientSessionModel.getUserSession().getId(), clientSessionModel.getClient().getId()}, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed adding artifact %s", artifact);
}
throw re;
}
}
@Override
public SamlArtifactSessionMappingModel get(String artifact) {
try {
BasicCache<UUID, String[]> cache = cacheSupplier.get();
String[] existing = cache.get(UUID.nameUUIDFromBytes(artifact.getBytes(StandardCharsets.UTF_8)));
if (existing == null || existing.length != 3) return null;
if (!artifact.equals(existing[0])) return null; // Check
return new SamlArtifactSessionMappingModel(existing[1], existing[2]);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when obtaining data for artifact %s", artifact);
}
return null;
}
}
@Override
public void remove(String artifact) {
try {
BasicCache<UUID, String[]> cache = cacheSupplier.get();
if (cache.remove(UUID.nameUUIDFromBytes(artifact.getBytes(StandardCharsets.UTF_8))) == null) {
logger.debugf("Artifact %s was already removed", artifact);
}
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed to remove artifact %s", artifact);
}
}
}
@Override
public void close() {
}
}

View file

@ -1,82 +0,0 @@
/*
* 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.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.models.SamlArtifactSessionMappingStoreProviderFactory;
import java.util.UUID;
import java.util.function.Supplier;
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
/**
* @author mhajas
*/
public class InfinispanSamlArtifactSessionMappingStoreProviderFactory implements SamlArtifactSessionMappingStoreProviderFactory {
private static final Logger LOG = Logger.getLogger(InfinispanSamlArtifactSessionMappingStoreProviderFactory.class);
// Reuse "actionTokens" infinispan cache for now
private volatile Supplier<BasicCache<UUID, String[]>> codeCache;
@Override
public SamlArtifactSessionMappingStoreProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanSamlArtifactSessionMappingStoreProvider(codeCache);
}
private void lazyInit(KeycloakSession session) {
if (codeCache == null) {
synchronized (this) {
if (codeCache == null) {
this.codeCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan";
}
@Override
public int order() {
return PROVIDER_PRIORITY;
}
}

View file

@ -0,0 +1,129 @@
/*
* 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 java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.connections.infinispan.InfinispanUtil;
/**
* TODO: Check if Boolean can be used as single-use cache argument instead of ActionTokenValueEntity. With respect to other single-use cache usecases like "Revoke Refresh Token" .
* Also with respect to the usage of streams iterating over "actionTokens" cache (check there are no ClassCastExceptions when casting values directly to ActionTokenValueEntity)
*
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanSingleUseObjectProvider implements SingleUseObjectProvider {
public static final Logger logger = Logger.getLogger(InfinispanSingleUseObjectProvider.class);
private final Supplier<BasicCache<String, ActionTokenValueEntity>> tokenCache;
private final KeycloakSession session;
public InfinispanSingleUseObjectProvider(KeycloakSession session, Supplier<BasicCache<String, ActionTokenValueEntity>> actionKeyCache) {
this.session = session;
this.tokenCache = actionKeyCache;
}
@Override
public void put(String key, long lifespanSeconds, Map<String, String> notes) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(notes);
try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds));
cache.put(key, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when adding code %s", key);
}
throw re;
}
}
@Override
public Map<String, String> get(String key) {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
ActionTokenValueEntity actionTokenValueEntity = cache.get(key);
return actionTokenValueEntity != null ? actionTokenValueEntity.getNotes() : null;
}
@Override
public Map<String, String> remove(String key) {
try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
ActionTokenValueEntity existing = cache.remove(key);
return existing == null ? null : existing.getNotes();
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to remove the code from different place.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when removing code %s", key);
}
return null;
}
}
@Override
public boolean replace(String key, Map<String, String> notes) {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
return cache.replace(key, new ActionTokenValueEntity(notes)) != null;
}
@Override
public boolean putIfAbsent(String key, long lifespanInSeconds) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(null);
try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanInSeconds));
ActionTokenValueEntity existing = cache.putIfAbsent(key, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
return existing == null;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to use the token from different place.
logger.debugf(re, "Failed when adding token %s", key);
return false;
}
}
@Override
public boolean contains(String key) {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
return cache.containsKey(key);
}
@Override
public void close() {
}
}

View file

@ -28,7 +28,7 @@ import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.SingleUseTokenStoreProviderFactory;
import org.keycloak.models.SingleUseObjectProviderFactory;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.connections.infinispan.InfinispanUtil;
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
@ -36,17 +36,17 @@ import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSe
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanSingleUseTokenStoreProviderFactory implements SingleUseTokenStoreProviderFactory {
public class InfinispanSingleUseObjectProviderFactory implements SingleUseObjectProviderFactory {
private static final Logger LOG = Logger.getLogger(InfinispanSingleUseTokenStoreProviderFactory.class);
private static final Logger LOG = Logger.getLogger(InfinispanSingleUseObjectProviderFactory.class);
// Reuse "actionTokens" infinispan cache for now
private volatile Supplier<BasicCache<String, ActionTokenValueEntity>> tokenCache;
@Override
public InfinispanSingleUseTokenStoreProvider create(KeycloakSession session) {
public InfinispanSingleUseObjectProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanSingleUseTokenStoreProvider(session, tokenCache);
return new InfinispanSingleUseObjectProvider(session, tokenCache);
}
private void lazyInit(KeycloakSession session) {

View file

@ -1,77 +0,0 @@
/*
* 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 java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.SingleUseTokenStoreProvider;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.connections.infinispan.InfinispanUtil;
/**
* TODO: Check if Boolean can be used as single-use cache argument instead of ActionTokenValueEntity. With respect to other single-use cache usecases like "Revoke Refresh Token" .
* Also with respect to the usage of streams iterating over "actionTokens" cache (check there are no ClassCastExceptions when casting values directly to ActionTokenValueEntity)
*
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanSingleUseTokenStoreProvider implements SingleUseTokenStoreProvider {
public static final Logger logger = Logger.getLogger(InfinispanSingleUseTokenStoreProvider.class);
private final Supplier<BasicCache<String, ActionTokenValueEntity>> tokenCache;
private final KeycloakSession session;
public InfinispanSingleUseTokenStoreProvider(KeycloakSession session, Supplier<BasicCache<String, ActionTokenValueEntity>> actionKeyCache) {
this.session = session;
this.tokenCache = actionKeyCache;
}
@Override
public boolean putIfAbsent(String tokenId, int lifespanInSeconds) {
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(null);
// Rather keep the items in the cache for a bit longer
lifespanInSeconds = lifespanInSeconds + 10;
try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanInSeconds));
ActionTokenValueEntity existing = cache.putIfAbsent(tokenId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
return existing == null;
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
// In case of lock conflict, we don't want to retry anyway as there was likely an attempt to use the token from different place.
logger.debugf(re, "Failed when adding token %s", tokenId);
return false;
}
}
@Override
public void close() {
}
}

View file

@ -1,99 +0,0 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.models.sessions.infinispan;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import org.infinispan.client.hotrod.exceptions.HotRodClientException;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.TokenRevocationStoreProvider;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import org.keycloak.connections.infinispan.InfinispanUtil;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanTokenRevocationStoreProvider implements TokenRevocationStoreProvider {
public static final Logger logger = Logger.getLogger(InfinispanTokenRevocationStoreProvider.class);
private final Supplier<BasicCache<String, ActionTokenValueEntity>> tokenCache;
private final KeycloakSession session;
// Key in the data, which indicates that token is considered revoked
private final String REVOKED_KEY = "revoked";
public InfinispanTokenRevocationStoreProvider(KeycloakSession session, Supplier<BasicCache<String, ActionTokenValueEntity>> tokenCache) {
this.session = session;
this.tokenCache = tokenCache;
}
@Override
public void putRevokedToken(String tokenId, long lifespanSeconds) {
Map<String, String> data = Collections.singletonMap(REVOKED_KEY, "true");
ActionTokenValueEntity tokenValue = new ActionTokenValueEntity(data);
try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
long lifespanMs = InfinispanUtil.toHotrodTimeMs(cache, Time.toMillis(lifespanSeconds + 1));
cache.put(tokenId, tokenValue, lifespanMs, TimeUnit.MILLISECONDS);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when adding revoked token %s", tokenId);
}
throw re;
}
}
@Override
public boolean isRevoked(String tokenId) {
try {
BasicCache<String, ActionTokenValueEntity> cache = tokenCache.get();
ActionTokenValueEntity existing = cache.get(tokenId);
if (existing == null) {
return false;
}
return existing.getNotes().containsKey(REVOKED_KEY);
} catch (HotRodClientException re) {
// No need to retry. The hotrod (remoteCache) has some retries in itself in case of some random network error happened.
if (logger.isDebugEnabled()) {
logger.debugf(re, "Failed when trying to get revoked token %s", tokenId);
}
return false;
}
}
@Override
public void close() {
}
}

View file

@ -1,81 +0,0 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.keycloak.models.sessions.infinispan;
import java.util.function.Supplier;
import org.infinispan.commons.api.BasicCache;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.TokenRevocationStoreProvider;
import org.keycloak.models.TokenRevocationStoreProviderFactory;
import org.keycloak.models.sessions.infinispan.entities.ActionTokenValueEntity;
import static org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory.PROVIDER_PRIORITY;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanTokenRevocationStoreProviderFactory implements TokenRevocationStoreProviderFactory {
// Reuse "actionTokens" infinispan cache for now
private volatile Supplier<BasicCache<String, ActionTokenValueEntity>> tokenCache;
@Override
public TokenRevocationStoreProvider create(KeycloakSession session) {
lazyInit(session);
return new InfinispanTokenRevocationStoreProvider(session, tokenCache);
}
private void lazyInit(KeycloakSession session) {
if (tokenCache == null) {
synchronized (this) {
if (tokenCache == null) {
this.tokenCache = InfinispanSingleUseTokenStoreProviderFactory.getActionTokenCache(session);
}
}
}
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan";
}
@Override
public int order() {
return PROVIDER_PRIORITY;
}
}

View file

@ -1,18 +0,0 @@
#
# Copyright 2019 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.
#
org.keycloak.models.sessions.infinispan.InfinispanOAuth2DeviceTokenStoreProviderFactory

View file

@ -1,18 +0,0 @@
#
# Copyright 2021 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.
#
org.keycloak.models.sessions.infinispan.InfinispanPushedAuthzRequestStoreProviderFactory

View file

@ -1,18 +0,0 @@
#
# Copyright 2021 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.
#
org.keycloak.models.sessions.infinispan.InfinispanSamlArtifactSessionMappingStoreProviderFactory

View file

@ -15,4 +15,4 @@
# limitations under the License.
#
org.keycloak.models.sessions.infinispan.InfinispanCodeToTokenStoreProviderFactory
org.keycloak.models.sessions.infinispan.InfinispanSingleUseObjectProviderFactory

View file

@ -1,18 +0,0 @@
#
# 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.
#
org.keycloak.models.sessions.infinispan.InfinispanSingleUseTokenStoreProviderFactory

View file

@ -1,19 +0,0 @@
#
# Copyright 2020 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.
#
#
org.keycloak.models.sessions.infinispan.InfinispanTokenRevocationStoreProviderFactory

View file

@ -23,7 +23,7 @@ import java.util.Map;
/**
* Internal action token store provider.
*
* It's used for store the details about used action tokens. There is separate provider for OAuth2 codes - {@link CodeToTokenStoreProvider},
* It's used for store the details about used action tokens. There is separate provider for OAuth2 codes - {@link SingleUseObjectProvider},
* which may reuse some components (eg. same infinispan cache)
*
* @author hmlnarik

View file

@ -1,53 +0,0 @@
/*
* 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;
import org.keycloak.provider.Provider;
/**
* Provides single-use cache for OAuth2 code parameter. Used to ensure that particular value of code parameter is used once.
*
* For now, it is separate provider as it's a bit different use-case than {@link ActionTokenStoreProvider}, however it may reuse some components (eg. same infinispan cache)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface CodeToTokenStoreProvider extends Provider {
/**
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
* @param codeId
* @param lifespanSeconds
* @param codeData
* @return true if data were successfully put
*/
void put(UUID codeId, int lifespanSeconds, Map<String, String> codeData);
/**
* This method returns data just if removal was successful. Implementation should guarantee that "remove" is single-use. So if
* 2 threads (even on different cluster nodes or on different cross-dc nodes) calls "remove(123)" concurrently, then just one of them
* is allowed to succeed and return data back. It can't happen that both will succeed.
*
* @param codeId
* @return context data related to OAuth2 code. It returns null if there are not context data available.
*/
Map<String, String> remove(UUID codeId);
}

View file

@ -1,103 +0,0 @@
/*
* Copyright 2019 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;
/**
* Provides cache for OAuth2 Device Authorization Grant tokens.
*
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public interface OAuth2DeviceTokenStoreProvider extends Provider {
/**
* Stores the given device code and user code
*
* @param deviceCode
* @param userCode
* @param lifespanSeconds
*/
void put(OAuth2DeviceCodeModel deviceCode, OAuth2DeviceUserCodeModel userCode, int lifespanSeconds);
/**
* Get the model object by the given device code
*
* @param realm
* @param deviceCode
* @return
*/
OAuth2DeviceCodeModel getByDeviceCode(RealmModel realm, String deviceCode);
/**
* Check the device code is allowed to poll
*
* @param deviceCode
* @return Return true if the given device code is allowed to poll
*/
boolean isPollingAllowed(OAuth2DeviceCodeModel deviceCode);
/**
* Get the model object by the given user code
*
* @param realm
* @param userCode
* @return
*/
OAuth2DeviceCodeModel getByUserCode(RealmModel realm, String userCode);
/**
* Approve the given user code
*
* @param realm
* @param userCode
* @param userSessionId
* @return Return true if approving successful. If the code is already expired and cleared, it returns false.
*/
boolean approve(RealmModel realm, String userCode, String userSessionId, Map<String, String> additionalParams);
/**
* Deny the given user code
*
* @param realm
* @param userCode
* @return Return true if denying successful. If the code is already expired and cleared, it returns false.
*/
boolean deny(RealmModel realm, String userCode);
/**
* Remove the given device code
*
* @param realm
* @param deviceCode
* @return Return true if removing successful. If the code is already expired and cleared, it returns false.
*/
boolean removeDeviceCode(RealmModel realm, String deviceCode);
/**
* Remove the given user code
*
* @param realm
* @param userCode
* @return Return true if removing successful. If the code is already expired and cleared, it returns false.
*/
boolean removeUserCode(RealmModel realm, String userCode);
}

View file

@ -1,26 +0,0 @@
/*
* Copyright 2019 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 <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public interface OAuth2DeviceTokenStoreProviderFactory extends ProviderFactory<OAuth2DeviceTokenStoreProvider> {
}

View file

@ -1,50 +0,0 @@
/*
* Copyright 2019 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 org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
*/
public class OAuth2DeviceTokenStoreSpi implements Spi {
public static final String NAME = "oauth2DeviceTokenStore";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return OAuth2DeviceTokenStoreProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return OAuth2DeviceTokenStoreProviderFactory.class;
}
}

View file

@ -1,49 +0,0 @@
/*
* Copyright 2021 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;
import java.util.UUID;
/**
* Provides single-use cache for Pushed Authorization Request. The data of this request may be used only once.
*/
public interface PushedAuthzRequestStoreProvider extends Provider {
/**
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter.
*
* @param key unique identifier
* @param lifespanSeconds time to live
* @param codeData the data to store
*/
void put(UUID key, int lifespanSeconds, Map<String, String> codeData);
/**
* This method returns data just if removal was successful. Implementation should guarantee that "remove" is single-use. So if
* 2 threads (even on different cluster nodes or on different cross-dc nodes) calls "remove(123)" concurrently, then just one of them
* is allowed to succeed and return data back. It can't happen that both will succeed.
*
* @param key unique identifier
* @return context data related Pushed Authorization Request. It returns null if there is no context data available.
*/
Map<String, String> remove(UUID key);
}

View file

@ -1,23 +0,0 @@
/*
* Copyright 2021 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;
public interface PushedAuthzRequestStoreProviderFactory extends ProviderFactory<PushedAuthzRequestStoreProvider> {
}

View file

@ -1,47 +0,0 @@
/*
* Copyright 2021 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 org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class PushedAuthzRequestStoreSpi implements Spi {
public static final String NAME = "pushedAuthzRequestStore";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return PushedAuthzRequestStoreProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return PushedAuthzRequestStoreProviderFactory.class;
}
}

View file

@ -1,55 +0,0 @@
/*
* 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;
/**
* Provides cache for session mapping for SAML artifacts.
*
* For now, it is separate provider as it's a bit different use-case than {@link ActionTokenStoreProvider}, however it may reuse some components (eg. same infinispan cache)
*
* @author mhajas
*/
public interface SamlArtifactSessionMappingStoreProvider extends Provider {
/**
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
*
* @param artifact
* @param lifespanSeconds
* @param clientSessionModel
*/
void put(String artifact, int lifespanSeconds, AuthenticatedClientSessionModel clientSessionModel);
/**
* This method returns session mapping associated with the given {@param artifact}
*
* @param artifact
* @return session mapping corresponding to given artifact or {@code null} if it does not exist.
*/
SamlArtifactSessionMappingModel get(String artifact);
/**
* Removes data for the given {@param artifact} from the store
*
* @param artifact
*/
void remove(String artifact);
}

View file

@ -1,26 +0,0 @@
/*
* 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 mhajas
*/
public interface SamlArtifactSessionMappingStoreProviderFactory extends ProviderFactory<SamlArtifactSessionMappingStoreProvider> {
}

View file

@ -1,50 +0,0 @@
/*
* 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 org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author mhajas
*/
public class SamlArtifactSessionMappingStoreSpi implements Spi {
public static final String NAME = "samlArtifactSessionMappingStore";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return SamlArtifactSessionMappingStoreProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return SamlArtifactSessionMappingStoreProviderFactory.class;
}
}

View file

@ -0,0 +1,82 @@
/*
* 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;
/**
* Provides a cache to store data for single-use use case. Data are represented by a {@code String} key.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface SingleUseObjectProvider extends Provider {
// suffix to a key to indicate that token is considered revoked
String REVOKED_KEY = ".revoked";
/**
* Stores the given data and guarantees that data should be available in the store for at least the time specified by {@param lifespanSeconds} parameter
* @param key
* @param lifespanSeconds
* @param notes
*/
void put(String key, long lifespanSeconds, Map<String, String> notes);
/**
* Gets data associated with the given key.
* @param key String
* @return Map<String, String> Data associated with the given key or {@code null} if there is no associated data.
*/
Map<String, String> get(String key);
/**
* This method returns data just if removal was successful. Implementation should guarantee that "remove" is single-use. So if
* 2 threads (even on different cluster nodes or on different cross-dc nodes) calls "remove(123)" concurrently, then just one of them
* is allowed to succeed and return data back. It can't happen that both will succeed.
*
* @param key
* @return context data associated to the key. It returns {@code null} if there are no context data available.
*/
Map<String, String> remove(String key);
/**
* Replaces data associated with the given key in the store if the store contains the key.
* @param key String
* @param notes Map<String, String> New data to be stored
* @return {@code true} if the store contains the key and data was replaced, otherwise {@code false}.
*/
boolean replace(String key, Map<String, String> notes);
/**
* Will try to put the key into the cache. It will succeed just if key is not already there.
*
* @param key
* @param lifespanInSeconds Minimum lifespan for which successfully added key will be kept in the cache.
* @return true if the key was successfully put into the cache. This means that same key wasn't in the cache before
*/
boolean putIfAbsent(String key, long lifespanInSeconds);
/**
* Checks if there is a record in the store for the given key.
* @param key String
* @return {@code true} if the record is present in the store, {@code false} otherwise.
*/
boolean contains(String key);
}

View file

@ -22,5 +22,5 @@ import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface CodeToTokenStoreProviderFactory extends ProviderFactory<CodeToTokenStoreProvider> {
public interface SingleUseObjectProviderFactory extends ProviderFactory<SingleUseObjectProvider> {
}

View file

@ -24,9 +24,9 @@ import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class CodeToTokenStoreSpi implements Spi {
public class SingleUseObjectSpi implements Spi {
public static final String NAME = "codeToTokenStore";
public static final String NAME = "singleUseObject";
@Override
public boolean isInternal() {
@ -40,11 +40,11 @@ public class CodeToTokenStoreSpi implements Spi {
@Override
public Class<? extends Provider> getProviderClass() {
return CodeToTokenStoreProvider.class;
return SingleUseObjectProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return CodeToTokenStoreProviderFactory.class;
return SingleUseObjectProviderFactory.class;
}
}

View file

@ -1,41 +0,0 @@
/*
* 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;
/**
* Provides single-use cache for OAuth2 code parameter. Used to ensure that particular value of code parameter is used once.
*
* TODO: For now, it is separate provider as {@link CodeToTokenStoreProvider}, however will be good to merge those 2 providers to "SingleUseCacheProvider"
* in the future as they provide very similar thing
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface SingleUseTokenStoreProvider extends Provider {
/**
* Will try to put the token into the cache. It will success just if token is not already there.
*
* @param tokenId
* @param lifespanInSeconds Minimum lifespan for which successfully added token will be kept in the cache.
* @return true if token was successfully put into the cache. This means that same token wasn't in the cache before
*/
boolean putIfAbsent(String tokenId, int lifespanInSeconds);
}

View file

@ -1,26 +0,0 @@
/*
* 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface SingleUseTokenStoreProviderFactory extends ProviderFactory<SingleUseTokenStoreProvider> {
}

View file

@ -1,50 +0,0 @@
/*
* 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 org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SingleUseTokenStoreSpi implements Spi {
public static final String NAME = "singleUseTokenStore";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return SingleUseTokenStoreProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return SingleUseTokenStoreProviderFactory.class;
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright 2020 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;
import org.keycloak.provider.Provider;
/**
* Provides the cache for store revoked tokens.
*
* For now, it is separate provider as it is bit different use-case that existing providers like {@link CodeToTokenStoreProvider},
* {@link SingleUseTokenStoreProvider} and {@link ActionTokenStoreProvider}
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface TokenRevocationStoreProvider extends Provider {
/**
* Mark given token as revoked. Parameter "lifespanSeconds" is the time for which the token is considered revoked. After this time, it may be removed from this store,
* which means that {@link #isRevoked} method will return false. In reality, the token will usually still be invalid due the "expiration" claim on it, however
* that is out of scope of this provider.
*
* @param tokenId
* @oaran lifespanSeconds
*/
void putRevokedToken(String tokenId, long lifespanSeconds);
/**
* @param tokenId
* @return true if token exists in the store, which indicates that it is revoked.
*/
boolean isRevoked(String tokenId);
}

View file

@ -1,26 +0,0 @@
/*
* Copyright 2020 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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface TokenRevocationStoreProviderFactory extends ProviderFactory<TokenRevocationStoreProvider> {
}

View file

@ -1,52 +0,0 @@
/*
* Copyright 2020 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 org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class TokenRevocationStoreSpi implements Spi {
public static final String NAME = "tokenRevocationStore";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return TokenRevocationStoreProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return TokenRevocationStoreProviderFactory.class;
}
}

View file

@ -26,13 +26,8 @@ org.keycloak.models.RealmSpi
org.keycloak.models.RoleSpi
org.keycloak.models.DeploymentStateSpi
org.keycloak.models.ActionTokenStoreSpi
org.keycloak.models.CodeToTokenStoreSpi
org.keycloak.models.OAuth2DeviceTokenStoreSpi
org.keycloak.models.OAuth2DeviceUserCodeSpi
org.keycloak.models.SamlArtifactSessionMappingStoreSpi
org.keycloak.models.PushedAuthzRequestStoreSpi
org.keycloak.models.SingleUseTokenStoreSpi
org.keycloak.models.TokenRevocationStoreSpi
org.keycloak.models.SingleUseObjectSpi
org.keycloak.models.UserSessionSpi
org.keycloak.models.UserLoginFailureSpi
org.keycloak.models.UserSpi

View file

@ -1,20 +0,0 @@
package org.keycloak.models;
public class SamlArtifactSessionMappingModel {
private final String userSessionId;
private final String clientSessionId;
public SamlArtifactSessionMappingModel(String userSessionId, String clientSessionId) {
this.userSessionId = userSessionId;
this.clientSessionId = clientSessionId;
}
public String getUserSessionId() {
return userSessionId;
}
public String getClientSessionId() {
return clientSessionId;
}
}

View file

@ -29,7 +29,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -44,7 +43,7 @@ import org.keycloak.keys.loader.PublicKeyStorageManager;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseTokenStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@ -190,7 +189,7 @@ public class JWTClientAuthenticator extends AbstractClientAuthenticator {
throw new RuntimeException("Missing ID on the token");
}
SingleUseTokenStoreProvider singleUseCache = context.getSession().getProvider(SingleUseTokenStoreProvider.class);
SingleUseObjectProvider singleUseCache = context.getSession().getProvider(SingleUseObjectProvider.class);
int lifespanInSecs = Math.max(token.getExpiration() - currentTime, 10);
if (singleUseCache.putIfAbsent(token.getId(), lifespanInSecs)) {
logger.tracef("Added token '%s' to single-use cache. Lifespan: %d seconds, client: %s", token.getId(), lifespanInSecs, clientId);

View file

@ -25,11 +25,9 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.apache.http.client.utils.CloneUtils;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowError;
@ -37,9 +35,7 @@ import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.SingleUseTokenStoreProvider;
import org.keycloak.models.delegate.ClientModelLazyDelegate;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -196,7 +192,7 @@ public class JWTClientSecretAuthenticator extends AbstractClientAuthenticator {
throw new RuntimeException("Missing ID on the token");
}
SingleUseTokenStoreProvider singleUseCache = context.getSession().getProvider(SingleUseTokenStoreProvider.class);
SingleUseObjectProvider singleUseCache = context.getSession().getProvider(SingleUseObjectProvider.class);
int lifespanInSecs = Math.max(token.getExpiration() - currentTime, 10);
if (singleUseCache.putIfAbsent(token.getId(), lifespanInSecs)) {

View file

@ -231,7 +231,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
// Standard or hybrid flow
String code = null;
if (responseType.hasResponseType(OIDCResponseType.CODE)) {
OAuth2Code codeData = new OAuth2Code(UUID.randomUUID(),
OAuth2Code codeData = new OAuth2Code(UUID.randomUUID().toString(),
Time.currentTime() + userSession.getRealm().getAccessCodeLifespan(),
nonce,
authSession.getClientNote(OAuth2Constants.SCOPE),

View file

@ -25,7 +25,6 @@ import org.keycloak.OAuthErrorException;
import org.keycloak.TokenCategory;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.authenticators.util.AcrStore;
import org.keycloak.authentication.authenticators.util.LoAUtil;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.cluster.ClusterProvider;
@ -50,7 +49,7 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.TokenRevocationStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@ -64,7 +63,6 @@ import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
import org.keycloak.rar.AuthorizationDetails;
import org.keycloak.representations.AuthorizationDetailsJSONRepresentation;
import org.keycloak.rar.AuthorizationRequestContext;
import org.keycloak.protocol.oidc.utils.AcrUtils;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
@ -1354,8 +1352,8 @@ public class TokenManager {
@Override
public boolean test(AccessToken token) {
TokenRevocationStoreProvider revocationStore = session.getProvider(TokenRevocationStoreProvider.class);
return !revocationStore.isRevoked(token.getId());
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
return !singleUseStore.contains(token.getId() + SingleUseObjectProvider.REVOKED_KEY);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.protocol.oidc.endpoints;
import java.util.Collections;
import java.util.Objects;
import java.util.stream.Collectors;
@ -41,7 +42,7 @@ import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.TokenRevocationStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.TokenManager;
@ -257,9 +258,9 @@ public class TokenRevocationEndpoint {
}
private void revokeAccessToken() {
TokenRevocationStoreProvider revocationStore = session.getProvider(TokenRevocationStoreProvider.class);
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
int currentTime = Time.currentTime();
long lifespanInSecs = Math.max(token.getExp() - currentTime, 10);
revocationStore.putRevokedToken(token.getId(), lifespanInSecs);
singleUseStore.put(token.getId() + SingleUseObjectProvider.REVOKED_KEY, lifespanInSecs, Collections.emptyMap());
}
}

View file

@ -36,7 +36,6 @@ import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
@ -49,6 +48,7 @@ import org.keycloak.protocol.oidc.grants.ciba.clientpolicy.context.BackchannelTo
import org.keycloak.protocol.oidc.grants.ciba.endpoints.CibaRootEndpoint;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
@ -160,8 +160,7 @@ public class CibaGrantType {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_GRANT, cpe.getErrorDetail(), Response.Status.BAD_REQUEST);
}
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
OAuth2DeviceCodeModel deviceCode = store.getByDeviceCode(realm, request.getId());
OAuth2DeviceCodeModel deviceCode = DeviceGrantType.getDeviceByDeviceCode(session, realm, request.getId());
if (deviceCode == null) {
// Auth Req ID has not put onto cache, no need to remove Auth Req ID.
@ -179,8 +178,8 @@ public class CibaGrantType {
throw new CorsErrorResponseException(cors, OAuthErrorException.EXPIRED_TOKEN, "authentication timed out", Response.Status.BAD_REQUEST);
}
if (!store.isPollingAllowed(deviceCode)) {
logDebug("pooling.", request);
if (!DeviceGrantType.isPollingAllowed(session, deviceCode)) {
logDebug("polling.", request);
throw new CorsErrorResponseException(cors, OAuthErrorException.SLOW_DOWN, "too early to access", Response.Status.BAD_REQUEST);
}
@ -198,7 +197,7 @@ public class CibaGrantType {
UserSessionModel userSession = createUserSession(request, deviceCode.getAdditionalParams());
UserModel user = userSession.getUser();
store.removeDeviceCode(realm, request.getId());
DeviceGrantType.removeDeviceByDeviceCode(session, request. getId());
// Compute client scopes again from scope parameter. Check if user still has them granted
// (but in code-to-token request, it could just theoretically happen that they are not available)

View file

@ -29,9 +29,10 @@ import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse.Status;
import org.keycloak.protocol.oidc.grants.device.DeviceGrantType;
import org.keycloak.protocol.oidc.grants.device.endpoints.DeviceEndpoint;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
@ -123,8 +124,7 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.FORBIDDEN);
}
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
OAuth2DeviceCodeModel deviceCode = store.getByUserCode(realm, bearerToken.getId());
OAuth2DeviceCodeModel deviceCode = DeviceEndpoint.getDeviceByUserCode(session, realm, bearerToken.getId());
if (deviceCode == null) {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.FORBIDDEN);
@ -154,15 +154,13 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
}
private void cancelRequest(String authResultId) {
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
OAuth2DeviceCodeModel userCode = store.getByUserCode(realm, authResultId);
store.removeDeviceCode(realm, userCode.getDeviceCode());
store.removeUserCode(realm, authResultId);
OAuth2DeviceCodeModel userCode = DeviceEndpoint.getDeviceByUserCode(session, realm, authResultId);
DeviceGrantType.removeDeviceByDeviceCode(session, userCode.getDeviceCode());
DeviceGrantType.removeDeviceByUserCode(session, realm, authResultId);
}
private void approveRequest(AccessToken authReqId, Map<String, String> additionalParams) {
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
store.approve(realm, authReqId.getId(), "fake", additionalParams);
DeviceGrantType.approveUserCode(session, realm, authReqId.getId(), "fake", additionalParams);
}
private void denyRequest(AccessToken authReqId, Status status) {
@ -172,9 +170,7 @@ public class BackchannelAuthenticationCallbackEndpoint extends AbstractCibaEndpo
event.error(Errors.CONSENT_DENIED);
}
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
store.deny(realm, authReqId.getId());
DeviceGrantType.denyUserCode(session, realm, authReqId.getId());
}
protected void sendClientNotificationRequest(ClientModel client, CibaConfig cibaConfig, OAuth2DeviceCodeModel deviceModel) {

View file

@ -27,9 +27,9 @@ import org.keycloak.models.CibaConfig;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.OAuth2DeviceUserCodeModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.grants.ciba.CibaGrantType;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelProvider;
@ -123,7 +123,7 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
/**
* TODO: Leverage the device code storage for tracking authentication requests. Not sure if we need a specific storage,
* but probably make the {@link OAuth2DeviceTokenStoreProvider} more generic for ciba, device, or any other use case
* or we can leverage the {@link SingleUseObjectProvider} for ciba, device, or any other use case
* that relies on cross-references for unsolicited user authentication requests from devices.
*/
private void storeAuthenticationRequest(CIBAAuthenticationRequest request, CibaConfig cibaConfig, String authReqId) {
@ -147,9 +147,10 @@ public class BackchannelAuthenticationEndpoint extends AbstractCibaEndpoint {
// To inform "expired_token" to the client, the lifespan of the cache provider is longer than device code
int lifespanSeconds = expiresIn + poolingInterval + 10;
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
store.put(deviceCode, userCode, lifespanSeconds);
singleUseStore.put(deviceCode.serializeKey(), lifespanSeconds, deviceCode.toMap());
singleUseStore.put(userCode.serializeKey(), lifespanSeconds, userCode.serializeValue());
}
private CIBAAuthenticationRequest authorizeClient(MultivaluedMap<String, String> params) {

View file

@ -26,14 +26,14 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.OAuth2DeviceUserCodeModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
@ -60,7 +60,7 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.stream.Stream;
import java.util.Map;
/**
* @author <a href="mailto:h2-wada@nri.co.jp">Hiroyuki Wada</a>
@ -101,8 +101,7 @@ public class DeviceGrantType {
String errorType = OAuthErrorException.SERVER_ERROR;
if (error == LoginProtocol.Error.CONSENT_DENIED) {
String verifiedUserCode = authSession.getClientNote(DeviceGrantType.OAUTH2_DEVICE_VERIFIED_USER_CODE);
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
if (!store.deny(realm, verifiedUserCode)) {
if (!denyUserCode(session, realm, verifiedUserCode)) {
// Already expired and removed in the store
errorType = OAuthErrorException.EXPIRED_TOKEN;
} else {
@ -123,8 +122,7 @@ public class DeviceGrantType {
String verifiedUserCode = authSession.getClientNote(DeviceGrantType.OAUTH2_DEVICE_VERIFIED_USER_CODE);
String userSessionId = clientSession.getUserSession().getId();
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
if (!store.approve(realm, verifiedUserCode, userSessionId, null)) {
if (!approveUserCode(session, realm, verifiedUserCode, userSessionId, null)) {
// Already expired and removed in the store
return Response.status(302).location(
uriBuilder.queryParam(OAuth2Constants.ERROR, OAuthErrorException.EXPIRED_TOKEN)
@ -133,7 +131,7 @@ public class DeviceGrantType {
}
// Now, remove the verified user code
store.removeUserCode(realm, verifiedUserCode);
removeDeviceByUserCode(session, realm, verifiedUserCode);
return Response.status(302).location(
uriBuilder.build(realm.getName())
@ -145,6 +143,51 @@ public class DeviceGrantType {
return flow != null;
}
public static OAuth2DeviceCodeModel getDeviceByDeviceCode(KeycloakSession session, RealmModel realm, String deviceCode) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
Map<String, String> notes = singleUseStore.get(OAuth2DeviceCodeModel.createKey(deviceCode));
return notes != null ? OAuth2DeviceCodeModel.fromCache(realm, deviceCode, notes) : null;
}
public static void removeDeviceByDeviceCode(KeycloakSession session, String deviceCode) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
singleUseStore.remove(OAuth2DeviceCodeModel.createKey(deviceCode));
}
public static void removeDeviceByUserCode(KeycloakSession session, RealmModel realm, String userCode) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
singleUseStore.remove(OAuth2DeviceUserCodeModel.createKey(realm, userCode));
}
public static boolean isPollingAllowed(KeycloakSession session, OAuth2DeviceCodeModel deviceCodeModel) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
return singleUseStore.putIfAbsent(deviceCodeModel.serializePollingKey(), deviceCodeModel.getPollingInterval());
}
public static boolean approveUserCode(KeycloakSession session, RealmModel realm, String userCode, String userSessionId, Map<String, String> additionalParams) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
OAuth2DeviceCodeModel deviceCodeModel = DeviceEndpoint.getDeviceByUserCode(session, realm, userCode);
if (deviceCodeModel != null) {
OAuth2DeviceCodeModel approvedDeviceCode = deviceCodeModel.approve(userSessionId, additionalParams);
return singleUseStore.replace(approvedDeviceCode.serializeKey(), approvedDeviceCode.toMap());
}
return false;
}
public static boolean denyUserCode(KeycloakSession session, RealmModel realm, String userCode) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
OAuth2DeviceCodeModel deviceCodeModel = DeviceEndpoint.getDeviceByUserCode(session, realm, userCode);
if (deviceCodeModel != null) {
OAuth2DeviceCodeModel deniedDeviceCode = deviceCodeModel.deny();
return singleUseStore.replace(deniedDeviceCode.serializeKey(), deniedDeviceCode.toMap());
}
return false;
}
private MultivaluedMap<String, String> formParams;
private ClientModel client;
@ -182,8 +225,7 @@ public class DeviceGrantType {
"Missing parameter: " + OAuth2Constants.DEVICE_CODE, Response.Status.BAD_REQUEST);
}
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
OAuth2DeviceCodeModel deviceCodeModel = store.getByDeviceCode(realm, deviceCode);
OAuth2DeviceCodeModel deviceCodeModel = getDeviceByDeviceCode(session, realm, deviceCode);
if (deviceCodeModel == null) {
event.error(Errors.INVALID_OAUTH2_DEVICE_CODE);
@ -197,7 +239,7 @@ public class DeviceGrantType {
Response.Status.BAD_REQUEST);
}
if (!store.isPollingAllowed(deviceCodeModel)) {
if (!isPollingAllowed(session, deviceCodeModel)) {
event.error(Errors.SLOW_DOWN);
throw new CorsErrorResponseException(cors, OAuthErrorException.SLOW_DOWN, "Slow down", Response.Status.BAD_REQUEST);
}
@ -245,7 +287,7 @@ public class DeviceGrantType {
}
// Now, remove the device code
store.removeDeviceCode(realm, deviceCode);
removeDeviceByDeviceCode(session, deviceCode);
UserModel user = userSession.getUser();
if (user == null) {

View file

@ -30,11 +30,12 @@ import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OAuth2DeviceCodeModel;
import org.keycloak.models.OAuth2DeviceTokenStoreProvider;
import org.keycloak.models.OAuth2DeviceUserCodeModel;
import org.keycloak.models.OAuth2DeviceUserCodeProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
@ -70,6 +71,8 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.Map;
import static org.keycloak.protocol.oidc.grants.device.DeviceGrantType.OAUTH2_DEVICE_USER_CODE;
/**
@ -161,8 +164,10 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
// To inform "expired_token" to the client, the lifespan of the cache provider is longer than device code
int lifespanSeconds = expiresIn + interval + 10;
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
store.put(deviceCode, userCode, lifespanSeconds);
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
singleUseStore.put(deviceCode.serializeKey(), lifespanSeconds, deviceCode.toMap());
singleUseStore.put(userCode.serializeKey(), lifespanSeconds, userCode.serializeValue());
try {
String deviceUrl = DeviceGrantType.oauth2DeviceVerificationUrl(session.getContext().getUri()).build(realm.getName())
@ -212,10 +217,9 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
} else {
// code exists, probably due to using a verification_uri_complete. Start the authentication considering the client
// that started the flow.
OAuth2DeviceTokenStoreProvider store = session.getProvider(OAuth2DeviceTokenStoreProvider.class);
OAuth2DeviceUserCodeProvider userCodeProvider = session.getProvider(OAuth2DeviceUserCodeProvider.class);
String formattedUserCode = userCodeProvider.format(userCode);
OAuth2DeviceCodeModel deviceCode = store.getByUserCode(realm, formattedUserCode);
OAuth2DeviceCodeModel deviceCode = getDeviceByUserCode(session, realm, formattedUserCode);
if (deviceCode == null) {
return invalidUserCodeResponse(Messages.OAUTH2_DEVICE_INVALID_USER_CODE, "device code not found (it may already have been used)");
@ -287,6 +291,21 @@ public class DeviceEndpoint extends AuthorizationEndpointBase implements RealmRe
}
}
public static OAuth2DeviceCodeModel getDeviceByUserCode(KeycloakSession session, RealmModel realm, String userCode) {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
Map<String, String> notes = singleUseStore.get(OAuth2DeviceUserCodeModel.createKey(realm, userCode));
if (notes != null) {
OAuth2DeviceUserCodeModel data = OAuth2DeviceUserCodeModel.fromCache(realm, userCode, notes);
String deviceCode = data.getDeviceCode();
notes = singleUseStore.get(OAuth2DeviceCodeModel.createKey(deviceCode));
return notes != null ? OAuth2DeviceCodeModel.fromCache(realm, deviceCode, notes) : null;
}
return null;
}
/**
* @param errorMessage Message code for the verification page
* @param reason For event details; not exposed to end user

View file

@ -24,7 +24,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.headers.SecurityHeadersProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PushedAuthzRequestStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpointChecker;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
@ -146,8 +146,8 @@ public class ParEndpoint extends AbstractParEndpoint {
Map<String, String> params = new HashMap<>();
UUID key = UUID.randomUUID();
String requestUri = REQUEST_URI_PREFIX + key.toString();
String key = UUID.randomUUID().toString();
String requestUri = REQUEST_URI_PREFIX + key;
int expiresIn = realm.getParPolicy().getRequestUriLifespan();
@ -158,8 +158,8 @@ public class ParEndpoint extends AbstractParEndpoint {
});
params.put(PAR_CREATED_TIME, String.valueOf(System.currentTimeMillis()));
PushedAuthzRequestStoreProvider parStore = session.getProvider(PushedAuthzRequestStoreProvider.class);
parStore.put(key, expiresIn, params);
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
singleUseStore.put(key, expiresIn, params);
ParResponse parResponse = new ParResponse(requestUri, expiresIn);

View file

@ -20,13 +20,12 @@ package org.keycloak.protocol.oidc.par.endpoints.request;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PushedAuthzRequestStoreProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthzEndpointRequestObjectParser;
@ -51,15 +50,15 @@ public class AuthzEndpointParParser extends AuthzEndpointRequestParser {
public AuthzEndpointParParser(KeycloakSession session, ClientModel client, String requestUri) {
this.session = session;
this.client = client;
PushedAuthzRequestStoreProvider parStore = session.getProvider(PushedAuthzRequestStoreProvider.class);
UUID key;
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
String key;
try {
key = UUID.fromString(requestUri.substring(ParEndpoint.REQUEST_URI_PREFIX_LENGTH));
key = requestUri.substring(ParEndpoint.REQUEST_URI_PREFIX_LENGTH);
} catch (RuntimeException re) {
logger.warnf(re,"Unable to parse request_uri: %s", requestUri);
throw new RuntimeException("Unable to parse request_uri");
}
Map<String, String> retrievedRequest = parStore.remove(key);
Map<String, String> retrievedRequest = singleUseStore.remove(key);
if (retrievedRequest == null) {
throw new RuntimeException("PAR not found. not issued or used multiple times.");
}

View file

@ -19,7 +19,6 @@ package org.keycloak.protocol.oidc.utils;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Data associated with the oauth2 code.
@ -40,7 +39,7 @@ public class OAuth2Code {
private static final String CODE_CHALLENGE_NOTE = "code_challenge";
private static final String CODE_CHALLENGE_METHOD_NOTE = "code_challenge_method";
private final UUID id;
private final String id;
private final int expiration;
@ -55,7 +54,7 @@ public class OAuth2Code {
private final String codeChallengeMethod;
public OAuth2Code(UUID id, int expiration, String nonce, String scope, String redirectUriParam,
public OAuth2Code(String id, int expiration, String nonce, String scope, String redirectUriParam,
String codeChallenge, String codeChallengeMethod) {
this.id = id;
this.expiration = expiration;
@ -68,7 +67,7 @@ public class OAuth2Code {
private OAuth2Code(Map<String, String> data) {
id = UUID.fromString(data.get(ID_NOTE));
id = data.get(ID_NOTE);
expiration = Integer.parseInt(data.get(EXPIRATION_NOTE));
nonce = data.get(NONCE_NOTE);
scope = data.get(SCOPE_NOTE);
@ -98,7 +97,7 @@ public class OAuth2Code {
}
public UUID getId() {
public String getId() {
return id;
}

View file

@ -18,7 +18,6 @@
package org.keycloak.protocol.oidc.utils;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Pattern;
import org.jboss.logging.Logger;
@ -26,9 +25,9 @@ import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.CodeToTokenStoreProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.UserSessionCrossDCManager;
@ -51,16 +50,16 @@ public class OAuth2CodeParser {
* @return code parameter to be used in OAuth2 handshake
*/
public static String persistCode(KeycloakSession session, AuthenticatedClientSessionModel clientSession, OAuth2Code codeData) {
CodeToTokenStoreProvider codeStore = session.getProvider(CodeToTokenStoreProvider.class);
SingleUseObjectProvider codeStore = session.getProvider(SingleUseObjectProvider.class);
UUID key = codeData.getId();
String key = codeData.getId();
if (key == null) {
throw new IllegalStateException("ID not present in the data");
}
Map<String, String> serialized = codeData.serializeCode();
codeStore.put(key, clientSession.getUserSession().getRealm().getAccessCodeLifespan(), serialized);
return key.toString() + "." + clientSession.getUserSession().getId() + "." + clientSession.getClient().getId();
return key + "." + clientSession.getUserSession().getId() + "." + clientSession.getClient().getId();
}
@ -91,9 +90,9 @@ public class OAuth2CodeParser {
event.session(userSessionId);
// Parse UUID
UUID codeUUID;
String codeUUID;
try {
codeUUID = UUID.fromString(parsed[0]);
codeUUID = parsed[0];
} catch (IllegalArgumentException re) {
logger.warn("Invalid format of the UUID in the code");
return result.illegalCode();
@ -111,7 +110,7 @@ public class OAuth2CodeParser {
result.clientSession = userSession.getAuthenticatedClientSessionByClient(clientUUID);
CodeToTokenStoreProvider codeStore = session.getProvider(CodeToTokenStoreProvider.class);
SingleUseObjectProvider codeStore = session.getProvider(SingleUseObjectProvider.class);
Map<String, String> codeData = codeStore.remove(codeUUID);
// Either code not available or was already used

View file

@ -17,7 +17,6 @@
package org.keycloak.protocol.saml;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
@ -43,7 +42,7 @@ import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol;
@ -85,7 +84,6 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
@ -140,6 +138,9 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_LOGIN_REQUEST_FORCEAUTHN = "SAML_LOGIN_REQUEST_FORCEAUTHN";
public static final String SAML_FORCEAUTHN_REQUIREMENT = "true";
public static final String SAML_LOGOUT_INITIATOR_CLIENT_ID = "SAML_LOGOUT_INITIATOR_CLIENT_ID";
public static final String USER_SESSION_ID = "userSessionId";
public static final String CLIENT_SESSION_ID = "clientSessionId";
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
@ -154,7 +155,7 @@ public class SamlProtocol implements LoginProtocol {
protected EventBuilder event;
protected ArtifactResolver artifactResolver;
protected SamlArtifactSessionMappingStoreProvider artifactSessionMappingStore;
protected SingleUseObjectProvider singleUseStore;
@Override
public SamlProtocol setSession(KeycloakSession session) {
@ -193,11 +194,11 @@ public class SamlProtocol implements LoginProtocol {
return artifactResolver;
}
private SamlArtifactSessionMappingStoreProvider getArtifactSessionMappingStore() {
if (artifactSessionMappingStore == null) {
artifactSessionMappingStore = session.getProvider(SamlArtifactSessionMappingStoreProvider.class);
private SingleUseObjectProvider getSingleUseStore() {
if (singleUseStore == null) {
singleUseStore = session.getProvider(SingleUseObjectProvider.class);
}
return artifactSessionMappingStore;
return singleUseStore;
}
@Override
@ -929,7 +930,10 @@ public class SamlProtocol implements LoginProtocol {
// Create artifact and store session mapping
SAMLDataMarshaller marshaller = new SAMLDataMarshaller();
String artifact = getArtifactResolver().buildArtifact(clientSessionModel, entityId, marshaller.serialize(artifactResponseType));
getArtifactSessionMappingStore().put(artifact, realm.getAccessCodeLifespan(), clientSessionModel);
HashMap<String, String> notes = new HashMap<>();
notes.put(USER_SESSION_ID, clientSessionModel.getUserSession().getId());
notes.put(CLIENT_SESSION_ID, clientSessionModel.getClient().getId());
getSingleUseStore().put(artifact, realm.getAccessCodeLifespan(), notes);
return artifact;
}

View file

@ -60,8 +60,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel;
import org.keycloak.models.SamlArtifactSessionMappingModel;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
@ -137,6 +136,7 @@ import java.security.PublicKey;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
@ -1104,8 +1104,8 @@ public class SamlService extends AuthorizationEndpointBase {
}
private SamlArtifactSessionMappingStoreProvider getArtifactSessionMappingStore() {
return session.getProvider(SamlArtifactSessionMappingStoreProvider.class);
private SingleUseObjectProvider getSingleUseStore() {
return session.getProvider(SingleUseObjectProvider.class);
}
/**
@ -1136,26 +1136,27 @@ public class SamlService extends AuthorizationEndpointBase {
}
// Obtain details of session that issued artifact and check if it corresponds to issuer of Resolve message
SamlArtifactSessionMappingModel sessionMapping = getArtifactSessionMappingStore().get(artifact);
Map<String, String> sessionMapping = getSingleUseStore().get(artifact);
if (sessionMapping == null) {
logger.errorf("No data stored for artifact %s", artifact);
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
UserSessionModel userSessionModel = session.sessions().getUserSession(realm, sessionMapping.getUserSessionId());
UserSessionModel userSessionModel = session.sessions().getUserSession(realm, sessionMapping.get(SamlProtocol.USER_SESSION_ID));
if (userSessionModel == null) {
logger.errorf("UserSession with id: %s, that corresponds to artifact: %s does not exist.", sessionMapping.getUserSessionId(), artifact);
logger.errorf("UserSession with id: %s, that corresponds to artifact: %s does not exist.", sessionMapping.get(SamlProtocol.USER_SESSION_ID), artifact);
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
AuthenticatedClientSessionModel clientSessionModel = userSessionModel.getAuthenticatedClientSessions().get(sessionMapping.getClientSessionId());
AuthenticatedClientSessionModel clientSessionModel = userSessionModel.getAuthenticatedClientSessions().get(sessionMapping.get(SamlProtocol.CLIENT_SESSION_ID));
if (clientSessionModel == null) {
logger.errorf("ClientSession with id: %s, that corresponds to artifact: %s and UserSession: %s does not exist.", sessionMapping.getClientSessionId(), artifact, sessionMapping.getUserSessionId());
logger.errorf("ClientSession with id: %s, that corresponds to artifact: %s and UserSession: %s does not exist.",
sessionMapping.get(SamlProtocol.CLIENT_SESSION_ID), artifact, sessionMapping.get(SamlProtocol.USER_SESSION_ID));
return emptyArtifactResponseMessage(artifactResolveMessage, null);
}
ClientModel clientModel = getAndCheckClientModel(sessionMapping.getClientSessionId(), artifactResolveMessage.getIssuer().getValue());
ClientModel clientModel = getAndCheckClientModel(sessionMapping.get(SamlProtocol.CLIENT_SESSION_ID), artifactResolveMessage.getIssuer().getValue());
SamlClient samlClient = new SamlClient(clientModel);
// Check signature within ArtifactResolve request if client requires it
@ -1178,7 +1179,9 @@ public class SamlService extends AuthorizationEndpointBase {
}
// Artifact is successfully resolved, we can remove session mapping from storage
getArtifactSessionMappingStore().remove(artifact);
if (getSingleUseStore().remove(artifact) == null) {
logger.debugf("Artifact %s was already removed", artifact);
}
Document artifactResponseDocument = null;
ArtifactResponseType artifactResponseType = null;

View file

@ -11,7 +11,8 @@ import org.apache.http.util.EntityUtils;
import org.keycloak.common.util.KeyUtils;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.ArtifactResolveType;
import org.keycloak.models.SamlArtifactSessionMappingStoreProvider;
import org.keycloak.models.SingleUseObjectProvider;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.SamlService;
import org.keycloak.protocol.saml.profile.util.Soap;
import org.keycloak.saml.BaseSAML2BindingBuilder;
@ -195,8 +196,8 @@ public class HandleArtifactStepBuilder extends SamlDocumentStepBuilder<ArtifactR
if (beforeStepChecker != null && beforeStepChecker instanceof SessionStateChecker) {
SessionStateChecker sessionStateChecker = (SessionStateChecker) beforeStepChecker;
sessionStateChecker.setUserSessionProvider(session -> session.getProvider(SamlArtifactSessionMappingStoreProvider.class).get(artifact).getUserSessionId());
sessionStateChecker.setClientSessionProvider(session -> session.getProvider(SamlArtifactSessionMappingStoreProvider.class).get(artifact).getClientSessionId());
sessionStateChecker.setUserSessionProvider(session -> session.getProvider(SingleUseObjectProvider.class).get(artifact).get(SamlProtocol.USER_SESSION_ID));
sessionStateChecker.setClientSessionProvider(session -> session.getProvider(SingleUseObjectProvider.class).get(artifact).get(SamlProtocol.CLIENT_SESSION_ID));
}
HttpPost post = Soap.createMessage().addToBody(DocumentUtil.getDocument(transformed)).buildHttpPost(authServerSamlUrl);

View file

@ -707,7 +707,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest {
}
@Test
public void testPooling() throws Exception {
public void testPolling() throws Exception {
getTestingClient().testing().setTestingInfinispanTimeService();
try {