From 0cb3c95ed5e8d62d9590c6bf7d1569909dc61014 Mon Sep 17 00:00:00 2001 From: Martin Kanis Date: Thu, 14 Apr 2022 16:22:29 +0200 Subject: [PATCH] Map storage: Single-use objects (action token) --- .../AbstractSingleUseObjectModel.java | 55 ++++ .../MapActionTokenProviderFactory.java | 45 ++++ .../MapSingleUseObjectAdapter.java | 69 +++++ .../MapSingleUseObjectEntity.java | 67 +++++ .../MapSingleUseObjectProvider.java | 242 ++++++++++++++++++ .../MapSingleUseObjectProviderFactory.java | 45 ++++ .../models/map/storage/ModelEntityUtil.java | 4 + ...ncurrentHashMapStorageProviderFactory.java | 11 + .../map/storage/chm/MapFieldPredicates.java | 8 + ...ngleUseObjectConcurrentHashMapStorage.java | 98 +++++++ .../SingleUseObjectModelCriteriaBuilder.java | 93 +++++++ ...oak.models.ActionTokenStoreProviderFactory | 19 ++ ...loak.models.SingleUseObjectProviderFactory | 19 ++ .../ActionTokenStoreProviderFactory.java | 2 +- .../SingleUseObjectProviderFactory.java | 2 +- .../models/ActionTokenValueModel.java | 9 + .../integration-arquillian/tests/base/pom.xml | 2 + .../testsuite/oauth/AccessTokenTest.java | 16 +- .../OAuth2DeviceAuthorizationGrantTest.java | 11 +- .../resources/META-INF/keycloak-server.json | 19 ++ .../testsuite/model/HotRodServerRule.java | 5 + .../model/parameters/HotRodMapStorage.java | 8 +- .../model/parameters/Infinispan.java | 8 + .../model/parameters/JpaMapStorage.java | 5 + .../model/parameters/LdapMapStorage.java | 9 +- .../testsuite/model/parameters/Map.java | 13 + .../SingleUseObjectModelTest.java | 159 ++++++++++++ testsuite/utils/pom.xml | 2 + .../resources/META-INF/keycloak-server.json | 19 ++ 29 files changed, 1049 insertions(+), 15 deletions(-) create mode 100644 model/map/src/main/java/org/keycloak/models/map/singleUseObject/AbstractSingleUseObjectModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapActionTokenProviderFactory.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectModelCriteriaBuilder.java create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.models.SingleUseObjectProviderFactory create mode 100644 testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/AbstractSingleUseObjectModel.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/AbstractSingleUseObjectModel.java new file mode 100644 index 0000000000..1be5ca1467 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/AbstractSingleUseObjectModel.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 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.map.singleUseObject; + +import org.keycloak.models.ActionTokenKeyModel; +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.common.AbstractEntity; + +import java.util.Objects; + +/** + * @author Martin Kanis + */ +public abstract class AbstractSingleUseObjectModel implements ActionTokenKeyModel, ActionTokenValueModel { + + protected final KeycloakSession session; + protected final E entity; + + public AbstractSingleUseObjectModel(KeycloakSession session, E entity) { + Objects.requireNonNull(entity, "entity"); + + this.session = session; + this.entity = entity; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ActionTokenValueModel)) return false; + + MapSingleUseObjectAdapter that = (MapSingleUseObjectAdapter) o; + return Objects.equals(that.entity.getId(), entity.getId()); + } + + @Override + public int hashCode() { + return entity.getId().hashCode(); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapActionTokenProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapActionTokenProviderFactory.java new file mode 100644 index 0000000000..e0c02feedb --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapActionTokenProviderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 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.map.singleUseObject; + +import org.keycloak.models.ActionTokenStoreProviderFactory; +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.common.AbstractMapProviderFactory; + +/** + * @author Martin Kanis + */ +public class MapActionTokenProviderFactory extends AbstractMapProviderFactory + implements ActionTokenStoreProviderFactory { + + public MapActionTokenProviderFactory() { + super(ActionTokenValueModel.class, MapSingleUseObjectProvider.class); + } + + @Override + public MapSingleUseObjectProvider createNew(KeycloakSession session) { + return new MapSingleUseObjectProvider(session, getStorage(session)); + } + + @Override + public String getHelpText() { + return "Action token provider"; + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectAdapter.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectAdapter.java new file mode 100644 index 0000000000..1f30b906a3 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectAdapter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 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.map.singleUseObject; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.common.TimeAdapter; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +/** + * @author Martin Kanis + */ +public class MapSingleUseObjectAdapter extends AbstractSingleUseObjectModel { + + public MapSingleUseObjectAdapter(KeycloakSession session, MapSingleUseObjectEntity entity) { + super(session, entity); + } + + @Override + public String getUserId() { + return entity.getUserId(); + } + + @Override + public String getActionId() { + return entity.getActionId(); + } + + @Override + public int getExpiration() { + Long expiration = entity.getExpiration(); + return expiration != null ? TimeAdapter.fromLongWithTimeInSecondsToIntegerWithTimeInSeconds(expiration) : 0; + } + + @Override + public UUID getActionVerificationNonce() { + String actionVerificationNonce = entity.getActionVerificationNonce(); + return actionVerificationNonce != null ? UUID.fromString(actionVerificationNonce) : null; + } + + @Override + public Map getNotes() { + Map notes = entity.getNotes(); + return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes); + } + + @Override + public String getNote(String name) { + Map notes = entity.getNotes(); + return notes == null ? null : notes.get(name); + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java new file mode 100644 index 0000000000..0863f6a893 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java @@ -0,0 +1,67 @@ +/* + * Copyright 2022 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.map.singleUseObject; + +import org.keycloak.models.map.annotations.GenerateEntityImplementations; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.ExpirableEntity; +import org.keycloak.models.map.common.UpdatableEntity; + +import java.util.Map; + +/** + * @author Martin Kanis + */ +@GenerateEntityImplementations( + inherits = "org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity.AbstractSingleUseObjectEntity" +) +@DeepCloner.Root +public interface MapSingleUseObjectEntity extends AbstractEntity, UpdatableEntity, ExpirableEntity { + + public abstract class AbstractSingleUseObjectEntity extends UpdatableEntity.Impl implements MapSingleUseObjectEntity { + + private String id; + + @Override + public String getId() { + return this.id; + } + + @Override + public void setId(String id) { + if (this.id != null) throw new IllegalStateException("Id cannot be changed"); + this.id = id; + this.updated |= id != null; + } + } + + String getUserId(); + void setUserId(String userId); + + String getActionId(); + void setActionId(String actionId); + + String getActionVerificationNonce(); + void setActionVerificationNonce(String actionVerificationNonce); + + Map getNotes(); + void setNotes(Map notes); + String getNote(String name); + void setNote(String key, String value); +} diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java new file mode 100644 index 0000000000..ba316e771c --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java @@ -0,0 +1,242 @@ +/* + * Copyright 2022 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.map.singleUseObject; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.models.ActionTokenKeyModel; +import org.keycloak.models.ActionTokenStoreProvider; +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelDuplicateException; +import org.keycloak.models.SingleUseObjectProvider; +import org.keycloak.models.map.common.TimeAdapter; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; + +import java.util.Collections; +import java.util.Map; + +import static org.keycloak.common.util.StackUtil.getShortStackTrace; +import static org.keycloak.models.map.storage.QueryParameters.withCriteria; +import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria; + +/** + * @author Martin Kanis + */ +public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, SingleUseObjectProvider { + + private static final Logger LOG = Logger.getLogger(MapSingleUseObjectProvider.class); + private final KeycloakSession session; + protected final MapKeycloakTransaction actionTokenStoreTx; + + public MapSingleUseObjectProvider(KeycloakSession session, MapStorage storage) { + this.session = session; + actionTokenStoreTx = storage.createTransaction(session); + + session.getTransactionManager().enlistAfterCompletion(actionTokenStoreTx); + } + + private ActionTokenValueModel singleUseEntityToAdapter(MapSingleUseObjectEntity origEntity) { + long expiration = origEntity.getExpiration() != null ? origEntity.getExpiration() : 0L; + if (Time.currentTime() < expiration) { + return new MapSingleUseObjectAdapter(session, origEntity); + } else { + actionTokenStoreTx.delete(origEntity.getId()); + return null; + } + } + + @Override + public void put(ActionTokenKeyModel actionTokenKey, Map notes) { + if (actionTokenKey == null || actionTokenKey.getUserId() == null || actionTokenKey.getActionId() == null || actionTokenKey.getActionVerificationNonce() == null) { + return; + } + + LOG.tracef("put(%s, %s, %s)%s", actionTokenKey.getUserId(), actionTokenKey.getActionId(), actionTokenKey.getActionVerificationNonce(), getShortStackTrace()); + + DefaultModelCriteria mcb = criteria(); + mcb = mcb.compare(ActionTokenValueModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, actionTokenKey.getUserId()) + .compare(ActionTokenValueModel.SearchableFields.ACTION_ID, ModelCriteriaBuilder.Operator.EQ, actionTokenKey.getActionId()) + .compare(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, ModelCriteriaBuilder.Operator.EQ, actionTokenKey.getActionVerificationNonce().toString()); + + ActionTokenValueModel existing = actionTokenStoreTx.read(withCriteria(mcb)) + .findFirst().map(this::singleUseEntityToAdapter).orElse(null); + + if (existing == null) { + MapSingleUseObjectEntity actionTokenStoreEntity = new MapSingleUseObjectEntityImpl(); + actionTokenStoreEntity.setUserId(actionTokenKey.getUserId()); + actionTokenStoreEntity.setActionId(actionTokenKey.getActionId()); + actionTokenStoreEntity.setActionVerificationNonce(actionTokenKey.getActionVerificationNonce().toString()); + actionTokenStoreEntity.setExpiration(TimeAdapter.fromIntegerWithTimeInSecondsToLongWithTimeAsInSeconds(actionTokenKey.getExpiration())); + actionTokenStoreEntity.setNotes(notes); + + LOG.debugf("Adding used action token to actionTokens cache: %s", actionTokenKey.toString()); + + actionTokenStoreTx.create(actionTokenStoreEntity); + } + } + + @Override + public ActionTokenValueModel get(ActionTokenKeyModel key) { + if (key == null || key.getUserId() == null || key.getActionId() == null || key.getActionVerificationNonce() == null) { + return null; + } + + LOG.tracef("get(%s, %s, %s)%s", key.getUserId(), key.getActionId(), key.getActionVerificationNonce(), getShortStackTrace()); + + DefaultModelCriteria mcb = criteria(); + mcb = mcb.compare(ActionTokenValueModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, key.getUserId()) + .compare(ActionTokenValueModel.SearchableFields.ACTION_ID, ModelCriteriaBuilder.Operator.EQ, key.getActionId()) + .compare(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, ModelCriteriaBuilder.Operator.EQ, key.getActionVerificationNonce().toString()); + + return actionTokenStoreTx.read(withCriteria(mcb)) + .findFirst().map(this::singleUseEntityToAdapter).orElse(null); + } + + @Override + public ActionTokenValueModel remove(ActionTokenKeyModel key) { + if (key == null || key.getUserId() == null || key.getActionId() == null || key.getActionVerificationNonce() == null) { + return null; + } + + LOG.tracef("remove(%s, %s, %s)%s", key.getUserId(), key.getActionId(), key.getActionVerificationNonce(), getShortStackTrace()); + + DefaultModelCriteria mcb = criteria(); + mcb = mcb.compare(ActionTokenValueModel.SearchableFields.USER_ID, ModelCriteriaBuilder.Operator.EQ, key.getUserId()) + .compare(ActionTokenValueModel.SearchableFields.ACTION_ID, ModelCriteriaBuilder.Operator.EQ, key.getActionId()) + .compare(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, ModelCriteriaBuilder.Operator.EQ, key.getActionVerificationNonce().toString()); + + MapSingleUseObjectEntity mapSingleUseObjectEntity = actionTokenStoreTx.read(withCriteria(mcb)).findFirst().orElse(null); + if (mapSingleUseObjectEntity != null) { + ActionTokenValueModel actionToken = singleUseEntityToAdapter(mapSingleUseObjectEntity); + if (actionToken != null) { + actionTokenStoreTx.delete(mapSingleUseObjectEntity.getId()); + return actionToken; + } + } + + return null; + } + + @Override + public void put(String key, long lifespanSeconds, Map notes) { + LOG.tracef("put(%s)%s", key, getShortStackTrace()); + + MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key); + + if (singleUseEntity != null) { + throw new ModelDuplicateException("Single-use object entity exists: " + singleUseEntity.getId()); + } + + singleUseEntity = new MapSingleUseObjectEntityImpl(); + singleUseEntity.setId(key); + singleUseEntity.setExpiration((long) Time.currentTime() + lifespanSeconds); + singleUseEntity.setNotes(notes); + + actionTokenStoreTx.create(singleUseEntity); + } + + @Override + public Map get(String key) { + LOG.tracef("get(%s)%s", key, getShortStackTrace()); + + MapSingleUseObjectEntity actionToken = getWithExpiration(key); + if (actionToken != null) { + Map notes = actionToken.getNotes(); + return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes); + } + + return null; + } + + @Override + public Map remove(String key) { + LOG.tracef("remove(%s)%s", key, getShortStackTrace()); + + MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key); + + if (singleUseEntity != null) { + Map notes = singleUseEntity.getNotes(); + if (actionTokenStoreTx.delete(key)) { + return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes); + } + } + // the single-use entity expired or someone else already used and deleted it + return null; + } + + @Override + public boolean replace(String key, Map notes) { + LOG.tracef("replace(%s)%s", key, getShortStackTrace()); + + MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key); + if (singleUseEntity != null) { + singleUseEntity.setNotes(notes); + return true; + } + + return false; + } + + @Override + public boolean putIfAbsent(String key, long lifespanInSeconds) { + LOG.tracef("putIfAbsent(%s)%s", key, getShortStackTrace()); + + MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key); + if (singleUseEntity != null) { + return false; + } else { + singleUseEntity = new MapSingleUseObjectEntityImpl(); + singleUseEntity.setId(key); + singleUseEntity.setExpiration((long) Time.currentTime() + lifespanInSeconds); + + actionTokenStoreTx.create(singleUseEntity); + + return true; + } + } + + @Override + public boolean contains(String key) { + LOG.tracef("contains(%s)%s", key, getShortStackTrace()); + + MapSingleUseObjectEntity actionToken = getWithExpiration(key); + + return actionToken != null; + } + + @Override + public void close() { + + } + + private MapSingleUseObjectEntity getWithExpiration(String key) { + MapSingleUseObjectEntity singleUseEntity = actionTokenStoreTx.read(key); + if (singleUseEntity != null) { + long expiration = singleUseEntity.getExpiration() != null ? singleUseEntity.getExpiration() : 0L; + if (Time.currentTime() < expiration) { + return singleUseEntity; + } + actionTokenStoreTx.delete(key); + } + return null; + } + } diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java new file mode 100644 index 0000000000..f9ff465054 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProviderFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 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.map.singleUseObject; + +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.SingleUseObjectProviderFactory; +import org.keycloak.models.map.common.AbstractMapProviderFactory; + +/** + * @author Martin Kanis + */ +public class MapSingleUseObjectProviderFactory extends AbstractMapProviderFactory + implements SingleUseObjectProviderFactory { + + public MapSingleUseObjectProviderFactory() { + super(ActionTokenValueModel.class, MapSingleUseObjectProvider.class); + } + + @Override + public MapSingleUseObjectProvider createNew(KeycloakSession session) { + return new MapSingleUseObjectProvider(session, getStorage(session)); + } + + @Override + public String getHelpText() { + return "Single use object provider"; + } + +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java b/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java index 2b64d622af..9fec6861b6 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/ModelEntityUtil.java @@ -22,6 +22,7 @@ import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.ActionTokenValueModel; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -31,6 +32,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.MapPolicyEntity; @@ -64,6 +66,7 @@ public class ModelEntityUtil { private static final Map, String> MODEL_TO_NAME = new HashMap<>(); static { + MODEL_TO_NAME.put(ActionTokenValueModel.class, "single-use-objects"); MODEL_TO_NAME.put(AuthenticatedClientSessionModel.class, "client-sessions"); MODEL_TO_NAME.put(ClientScopeModel.class, "client-scopes"); MODEL_TO_NAME.put(ClientModel.class, "clients"); @@ -90,6 +93,7 @@ public class ModelEntityUtil { private static final Map, Class> MODEL_TO_ENTITY_TYPE = new HashMap<>(); static { + MODEL_TO_ENTITY_TYPE.put(ActionTokenValueModel.class, MapSingleUseObjectEntity.class); MODEL_TO_ENTITY_TYPE.put(AuthenticatedClientSessionModel.class, MapAuthenticatedClientSessionEntity.class); MODEL_TO_ENTITY_TYPE.put(ClientScopeModel.class, MapClientScopeEntity.class); MODEL_TO_ENTITY_TYPE.put(ClientModel.class, MapClientEntity.class); diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java index 3f7301fa58..b97cded696 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/ConcurrentHashMapStorageProviderFactory.java @@ -16,6 +16,8 @@ */ package org.keycloak.models.map.storage.chm; +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity; import org.keycloak.models.map.authSession.MapAuthenticationSessionEntityImpl; import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity; @@ -64,6 +66,7 @@ import java.util.EnumSet; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import org.jboss.logging.Logger; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntityImpl; import org.keycloak.models.map.storage.MapStorageProvider; import org.keycloak.models.map.storage.MapStorageProviderFactory; import org.keycloak.models.map.user.MapUserConsentEntityImpl; @@ -143,6 +146,7 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide .constructor(MapAuthenticatedClientSessionEntity.class, MapAuthenticatedClientSessionEntityImpl::new) .constructor(MapAuthEventEntity.class, MapAuthEventEntityImpl::new) .constructor(MapAdminEventEntity.class, MapAdminEventEntityImpl::new) + .constructor(MapSingleUseObjectEntity.class, MapSingleUseObjectEntityImpl::new) .build(); private static final Map KEY_CONVERTERS = new HashMap<>(); @@ -244,6 +248,13 @@ public class ConcurrentHashMapStorageProviderFactory implements AmphibianProvide return "ConcurrentHashMapStorage(" + mapName + suffix + ")"; } }; + } else if(modelType == ActionTokenValueModel.class) { + store = new SingleUseObjectConcurrentHashMapStorage(kc, CLONER) { + @Override + public String toString() { + return "ConcurrentHashMapStorage(" + mapName + suffix + ")"; + } + }; } else { store = new ConcurrentHashMapStorage(modelType, kc, CLONER) { @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index 78ffae64d3..c2fd6090d0 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -23,6 +23,7 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.ActionTokenValueModel; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; @@ -47,6 +48,7 @@ import org.keycloak.models.map.group.MapGroupEntity; import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; import org.keycloak.models.map.realm.MapRealmEntity; import org.keycloak.models.map.role.MapRoleEntity; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.user.MapUserConsentEntity; import org.keycloak.storage.SearchableModelField; @@ -98,6 +100,7 @@ public class MapFieldPredicates { public static final Map, UpdatePredicatesFunc> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID); public static final Map, UpdatePredicatesFunc> AUTH_EVENTS_PREDICATES = basePredicates(Event.SearchableFields.ID); public static final Map, UpdatePredicatesFunc> ADMIN_EVENTS_PREDICATES = basePredicates(AdminEvent.SearchableFields.ID); + public static final Map, UpdatePredicatesFunc> ACTION_TOKEN_PREDICATES = basePredicates(ActionTokenValueModel.SearchableFields.ID); @SuppressWarnings("unchecked") private static final Map, Map> PREDICATES = new HashMap<>(); @@ -226,6 +229,10 @@ public class MapFieldPredicates { put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.OPERATION_TYPE, MapAdminEventEntity::getOperationType); put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.RESOURCE_TYPE, MapAdminEventEntity::getResourceType); put(ADMIN_EVENTS_PREDICATES, AdminEvent.SearchableFields.RESOURCE_PATH, MapAdminEventEntity::getResourcePath); + + put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.USER_ID, MapSingleUseObjectEntity::getUserId); + put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.ACTION_ID, MapSingleUseObjectEntity::getActionId); + put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, MapSingleUseObjectEntity::getActionVerificationNonce); } static { @@ -246,6 +253,7 @@ public class MapFieldPredicates { PREDICATES.put(UserLoginFailureModel.class, USER_LOGIN_FAILURE_PREDICATES); PREDICATES.put(Event.class, AUTH_EVENTS_PREDICATES); PREDICATES.put(AdminEvent.class, ADMIN_EVENTS_PREDICATES); + PREDICATES.put(ActionTokenValueModel.class, ACTION_TOKEN_PREDICATES); } private static > void put( diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java new file mode 100644 index 0000000000..b218b700eb --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java @@ -0,0 +1,98 @@ +/* + * Copyright 2022 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.map.storage.chm; + +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.StringKeyConverter; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; +import org.keycloak.storage.SearchableModelField; + +import java.util.Map; +import java.util.stream.Stream; + +/** + * @author Martin Kanis + */ +public class SingleUseObjectConcurrentHashMapStorage extends ConcurrentHashMapStorage { + + public SingleUseObjectConcurrentHashMapStorage(StringKeyConverter keyConverter, DeepCloner cloner) { + super(ActionTokenValueModel.class, keyConverter, cloner); + } + + @Override + @SuppressWarnings("unchecked") + public MapKeycloakTransaction createTransaction(KeycloakSession session) { + MapKeycloakTransaction actionTokenTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); + return actionTokenTransaction == null ? new SingleUseObjectConcurrentHashMapStorage.Transaction(getKeyConverter(), cloner, fieldPredicates) : actionTokenTransaction; + } + + @Override + public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) { + if (value.getId() == null) { + if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) { + value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce()); + } + } + return super.create(value); + } + + @Override + public Stream read(QueryParameters queryParameters) { + DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); + + if (criteria == null) { + return Stream.empty(); + } + + SingleUseObjectModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createSingleUseObjectCriteriaBuilder()); + if (mcb.isValid()) { + MapSingleUseObjectEntity value = read(mcb.getKey()); + return value != null ? Stream.of(value) : Stream.empty(); + } + + return super.read(queryParameters); + } + + private SingleUseObjectModelCriteriaBuilder createSingleUseObjectCriteriaBuilder() { + return new SingleUseObjectModelCriteriaBuilder(); + } + + private class Transaction extends ConcurrentHashMapKeycloakTransaction { + + public Transaction(StringKeyConverter keyConverter, DeepCloner cloner, + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates) { + super(SingleUseObjectConcurrentHashMapStorage.this, keyConverter, cloner, fieldPredicates); + } + + @Override + public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) { + if (value.getId() == null) { + if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) { + value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce()); + } + } + return super.create(value); + } + } +} diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectModelCriteriaBuilder.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectModelCriteriaBuilder.java new file mode 100644 index 0000000000..4e9b63b405 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectModelCriteriaBuilder.java @@ -0,0 +1,93 @@ +/* + * Copyright 2022 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.map.storage.chm; + +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.storage.SearchableModelField; + +/** + * @author Martin Kanis + */ +public class SingleUseObjectModelCriteriaBuilder implements ModelCriteriaBuilder { + + private String userId; + + private String actionId; + + private String actionVerificationNonce; + + public SingleUseObjectModelCriteriaBuilder() { + } + + public SingleUseObjectModelCriteriaBuilder(String userId, String actionId, String actionVerificationNonce) { + this.userId = userId; + this.actionId = actionId; + this.actionVerificationNonce = actionVerificationNonce; + } + + @Override + public ModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value) { + if (modelField == org.keycloak.models.ActionTokenValueModel.SearchableFields.USER_ID) { + userId = value[0].toString(); + } else if (modelField == org.keycloak.models.ActionTokenValueModel.SearchableFields.ACTION_ID) { + actionId = value[0].toString(); + } else if (modelField == org.keycloak.models.ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE) { + actionVerificationNonce = value[0].toString(); + } + return new SingleUseObjectModelCriteriaBuilder(userId, actionId, actionVerificationNonce); + } + + @Override + public ModelCriteriaBuilder and(ModelCriteriaBuilder[] builders) { + String userId = null; + String actionId = null; + String actionVerificationNonce = null; + + for (ModelCriteriaBuilder builder: builders) { + SingleUseObjectModelCriteriaBuilder suoMcb = (SingleUseObjectModelCriteriaBuilder) builder; + if (suoMcb.userId != null) { + userId = suoMcb.userId; + } + if (suoMcb.actionId != null) { + actionId = suoMcb.actionId; + } + if (suoMcb.actionVerificationNonce != null) { + actionVerificationNonce = suoMcb.actionVerificationNonce; + } + } + return new SingleUseObjectModelCriteriaBuilder(userId, actionId, actionVerificationNonce); + } + + @Override + public ModelCriteriaBuilder or(ModelCriteriaBuilder[] builders) { + throw new IllegalStateException("SingleUseObjectModelCriteriaBuilder does not support OR operation."); + } + + @Override + public ModelCriteriaBuilder not(ModelCriteriaBuilder builder) { + throw new IllegalStateException("SingleUseObjectModelCriteriaBuilder does not support NOT operation."); + } + + public boolean isValid() { + return userId != null && actionId != null && actionVerificationNonce != null; + } + + public String getKey() { + return userId + ":" + actionId + ":" + actionVerificationNonce; + } +} diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory new file mode 100644 index 0000000000..9aeb62a832 --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.ActionTokenStoreProviderFactory @@ -0,0 +1,19 @@ +# +# Copyright 2022 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.map.singleUseObject.MapActionTokenProviderFactory + diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.models.SingleUseObjectProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.models.SingleUseObjectProviderFactory new file mode 100644 index 0000000000..8734b778e0 --- /dev/null +++ b/model/map/src/main/resources/META-INF/services/org.keycloak.models.SingleUseObjectProviderFactory @@ -0,0 +1,19 @@ +# +# Copyright 2022 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.map.singleUseObject.MapSingleUseObjectProviderFactory + diff --git a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java index 26d086d3a4..f8a6b6b054 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/models/ActionTokenStoreProviderFactory.java @@ -22,6 +22,6 @@ import org.keycloak.provider.ProviderFactory; * * @author hmlnarik */ -public interface ActionTokenStoreProviderFactory extends ProviderFactory { +public interface ActionTokenStoreProviderFactory extends ProviderFactory { } diff --git a/server-spi-private/src/main/java/org/keycloak/models/SingleUseObjectProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/models/SingleUseObjectProviderFactory.java index 974b5e2372..4152218ded 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/SingleUseObjectProviderFactory.java +++ b/server-spi-private/src/main/java/org/keycloak/models/SingleUseObjectProviderFactory.java @@ -22,5 +22,5 @@ import org.keycloak.provider.ProviderFactory; /** * @author Marek Posolda */ -public interface SingleUseObjectProviderFactory extends ProviderFactory { +public interface SingleUseObjectProviderFactory extends ProviderFactory { } diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java index a7d7f0441f..170dd0ff68 100644 --- a/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java @@ -16,6 +16,8 @@ */ package org.keycloak.models; +import org.keycloak.storage.SearchableModelField; + import java.util.Map; /** @@ -23,6 +25,13 @@ import java.util.Map; * @author hmlnarik */ public interface ActionTokenValueModel { + + class SearchableFields { + public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField USER_ID = new SearchableModelField<>("userId", String.class); + public static final SearchableModelField ACTION_ID = new SearchableModelField<>("actionId", String.class); + public static final SearchableModelField ACTION_VERIFICATION_NONCE = new SearchableModelField<>("actionVerificationNonce", String.class); + } /** * Returns unmodifiable map of all notes. diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index 1d0ce533a4..b7dc2e8499 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -843,6 +843,8 @@ map map map + map + map false false false diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index a820253f1d..5d94f55ab2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -387,6 +387,7 @@ public class AccessTokenTest extends AbstractKeycloakTest { @Test public void accessTokenCodeExpired() { + getTestingClient().testing().setTestingInfinispanTimeService(); RealmManager.realm(adminClient.realm("test")).accessCodeLifeSpan(1); oauth.doLogin("test-user@localhost", "password"); @@ -397,15 +398,18 @@ public class AccessTokenTest extends AbstractKeycloakTest { String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); - setTimeOffset(2); + try { + setTimeOffset(2); - OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); - Assert.assertEquals(400, response.getStatusCode()); - - setTimeOffset(0); + OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password"); + Assert.assertEquals(400, response.getStatusCode()); + } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); + resetTimeOffset(); + } AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, codeId); - expectedEvent.error("expired_code") + expectedEvent.error("invalid_code") .removeDetail(Details.TOKEN_ID) .removeDetail(Details.REFRESH_TOKEN_ID) .removeDetail(Details.REFRESH_TOKEN_TYPE) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java index 4ff15c85b8..fa945d2461 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuth2DeviceAuthorizationGrantTest.java @@ -469,6 +469,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest { @Test public void testExpiredUserCodeTest() throws Exception { + getTestingClient().testing().setTestingInfinispanTimeService(); // Device Authorization Request from device oauth.realm(REALM_NAME); oauth.clientId(DEVICE_APP); @@ -486,10 +487,12 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest { setTimeOffset(610); openVerificationPage(response.getVerificationUriComplete()); } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); resetTimeOffset(); } - verificationPage.assertExpiredUserCodePage(); + // device code not found in the cache because of expiration => invalid_grant error and redirection to the login page + loginPage.assertCurrent(); } @Test @@ -561,6 +564,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest { @Test public void testExpiredDeviceCode() throws Exception { + getTestingClient().testing().setTestingInfinispanTimeService(); // Device Authorization Request from device oauth.realm(REALM_NAME); oauth.clientId(DEVICE_APP); @@ -581,8 +585,9 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest { response.getDeviceCode()); Assert.assertEquals(400, tokenResponse.getStatusCode()); - Assert.assertEquals("expired_token", tokenResponse.getError()); + Assert.assertEquals("invalid_grant", tokenResponse.getError()); } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); resetTimeOffset(); } } @@ -600,6 +605,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest { @Test public void testDeviceCodeLifespanPerClient() throws Exception { + getTestingClient().testing().setTestingInfinispanTimeService(); ClientResource client = ApiUtil.findClientByClientId(adminClient.realm(REALM_NAME), DEVICE_APP); ClientRepresentation clientRepresentation = client.toRepresentation(); // Device Authorization Request from device @@ -638,6 +644,7 @@ public class OAuth2DeviceAuthorizationGrantTest extends AbstractKeycloakTest { Assert.assertEquals(400, tokenResponse.getStatusCode()); Assert.assertEquals("expired_token", tokenResponse.getError()); } finally { + getTestingClient().testing().revertTestingInfinispanTimeService(); resetTimeOffset(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 544e2a8619..770eef5d36 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -142,9 +142,28 @@ } }, + "actionToken": { + "provider": "${keycloak.actionToken.provider:infinispan}", + "map": { + "storage": { + "provider": "${keycloak.actionToken.map.storage.provider:concurrenthashmap}" + } + } + }, + + "singleUseObject": { + "provider": "${keycloak.singleUseObject.provider:infinispan}", + "map": { + "storage": { + "provider": "${keycloak.singleUseObject.map.storage.provider:concurrenthashmap}" + } + } + }, + "mapStorage": { "concurrenthashmap": { "dir": "${project.build.directory:target}", + "keyType.single-use-objects": "string", "keyType.realms": "string", "keyType.authz-resource-servers": "string" }, diff --git a/testsuite/model/src/main/java/org/keycloak/testsuite/model/HotRodServerRule.java b/testsuite/model/src/main/java/org/keycloak/testsuite/model/HotRodServerRule.java index a39bf8779a..3ffb7579dd 100644 --- a/testsuite/model/src/main/java/org/keycloak/testsuite/model/HotRodServerRule.java +++ b/testsuite/model/src/main/java/org/keycloak/testsuite/model/HotRodServerRule.java @@ -14,6 +14,7 @@ import org.infinispan.server.hotrod.configuration.HotRodServerConfiguration; import org.infinispan.server.hotrod.configuration.HotRodServerConfigurationBuilder; import org.junit.rules.ExternalResource; import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanUtil; import org.keycloak.models.map.storage.hotRod.common.HotRodUtils; import java.io.IOException; @@ -77,6 +78,10 @@ public class HotRodServerRule extends ExternalResource { getCaches(USER_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME, LOGIN_FAILURE_CACHE_NAME, WORK_CACHE_NAME, ACTION_TOKEN_CACHE); + + // Use Keycloak time service in remote caches + InfinispanUtil.setTimeServiceToKeycloakTime(hotRodCacheManager); + InfinispanUtil.setTimeServiceToKeycloakTime(hotRodCacheManager2); } public void createHotRodMapStoreServer() { diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java index 035f641c58..61ec065380 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java @@ -21,7 +21,9 @@ import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.keycloak.authorization.store.StoreFactorySpi; import org.keycloak.events.EventStoreSpi; +import org.keycloak.models.ActionTokenStoreSpi; import org.keycloak.models.DeploymentStateSpi; +import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.dblock.NoLockingDBLockProviderFactory; @@ -30,6 +32,7 @@ import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory; import org.keycloak.models.map.client.MapClientProviderFactory; import org.keycloak.models.map.clientscope.MapClientScopeProviderFactory; import org.keycloak.models.map.events.MapEventStoreProviderFactory; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionProviderFactory; import org.keycloak.models.map.storage.hotRod.connections.HotRodConnectionSpi; @@ -73,6 +76,8 @@ public class HotRodMapStorage extends KeycloakModelParameters { @Override public void updateConfig(Config cf) { cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) + .spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) .spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) .spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) @@ -90,7 +95,8 @@ public class HotRodMapStorage extends KeycloakModelParameters { cf.spi(MapStorageSpi.NAME) .provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) - .config("dir", "${project.build.directory:target}"); + .config("dir", "${project.build.directory:target}") + .config("keyType.single-use-objects", "string"); cf.spi(HotRodConnectionSpi.NAME).provider(DefaultHotRodConnectionProviderFactory.PROVIDER_ID) .config("enableSecurity", "false") diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java index 0540d9053c..efb73e0379 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java @@ -19,8 +19,12 @@ package org.keycloak.testsuite.model.parameters; import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; import org.keycloak.connections.infinispan.InfinispanConnectionSpi; +import org.keycloak.models.ActionTokenStoreSpi; +import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.session.UserSessionPersisterSpi; +import org.keycloak.models.sessions.infinispan.InfinispanActionTokenStoreProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory; +import org.keycloak.models.sessions.infinispan.InfinispanSingleUseObjectProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanUserLoginFailureProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanUserSessionProviderFactory; import org.keycloak.sessions.AuthenticationSessionSpi; @@ -55,6 +59,8 @@ public class Infinispan extends KeycloakModelParameters { .add(InfinispanConnectionSpi.class) .add(StickySessionEncoderSpi.class) .add(UserSessionPersisterSpi.class) + .add(ActionTokenStoreSpi.class) + .add(SingleUseObjectSpi.class) .build(); @@ -66,6 +72,8 @@ public class Infinispan extends KeycloakModelParameters { .add(InfinispanUserCacheProviderFactory.class) .add(InfinispanUserSessionProviderFactory.class) .add(InfinispanUserLoginFailureProviderFactory.class) + .add(InfinispanActionTokenStoreProviderFactory.class) + .add(InfinispanSingleUseObjectProviderFactory.class) .add(StickySessionEncoderProviderFactory.class) .add(TimerProviderFactory.class) .build(); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java index 1a80c2517c..0ca4181b76 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java @@ -20,8 +20,10 @@ import com.google.common.collect.ImmutableSet; import java.util.Set; import org.jboss.logging.Logger; import org.keycloak.authorization.store.StoreFactorySpi; +import org.keycloak.models.ActionTokenStoreSpi; import org.keycloak.events.EventStoreSpi; import org.keycloak.models.DeploymentStateSpi; +import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.dblock.NoLockingDBLockProviderFactory; @@ -34,6 +36,7 @@ import org.keycloak.models.map.group.MapGroupProviderFactory; import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory; import org.keycloak.models.map.realm.MapRealmProviderFactory; import org.keycloak.models.map.role.MapRoleProviderFactory; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory; import org.keycloak.models.map.storage.MapStorageSpi; import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory; import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; @@ -94,6 +97,8 @@ public class JpaMapStorage extends KeycloakModelParameters { .spi("user").provider(MapUserProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi(UserLoginFailureSpi.NAME).provider(MapUserLoginFailureProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID) .spi("dblock").provider(NoLockingDBLockProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-user-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .config("storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi(EventStoreSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java index 0b199e6eaa..0b65690b5f 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/LdapMapStorage.java @@ -22,10 +22,10 @@ import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.keycloak.authorization.store.StoreFactorySpi; import org.keycloak.events.EventStoreSpi; +import org.keycloak.models.ActionTokenStoreSpi; import org.keycloak.models.DeploymentStateSpi; import org.keycloak.models.LDAPConstants; -import org.keycloak.models.ModelDuplicateException; -import org.keycloak.models.ModelException; +import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.map.storage.MapStorageSpi; @@ -38,7 +38,6 @@ import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.testsuite.util.LDAPRule; import org.keycloak.util.ldap.LDAPEmbeddedServer; -import javax.naming.NamingException; import java.util.Set; /** @@ -104,7 +103,9 @@ public class LdapMapStorage extends KeycloakModelParameters { .spi("authorizationPersister").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("authenticationSessions").config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi(EventStoreSpi.NAME).config("map.storage-admin-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) - .spi(EventStoreSpi.NAME).config("map.storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID); + .spi(EventStoreSpi.NAME).config("map.storage-auth-events.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(ActionTokenStoreSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(SingleUseObjectSpi.NAME).config("map.storage.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID); } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java index 75c258ac18..10b1729443 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Map.java @@ -18,7 +18,11 @@ package org.keycloak.testsuite.model.parameters; import org.keycloak.authorization.store.StoreFactorySpi; import org.keycloak.events.EventStoreSpi; +import org.keycloak.models.ActionTokenStoreProviderFactory; +import org.keycloak.models.ActionTokenStoreSpi; import org.keycloak.models.DeploymentStateSpi; +import org.keycloak.models.SingleUseObjectProviderFactory; +import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserSessionSpi; import org.keycloak.models.dblock.NoLockingDBLockProviderFactory; @@ -26,6 +30,8 @@ import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderF import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory; import org.keycloak.models.map.events.MapEventStoreProviderFactory; import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory; +import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory; import org.keycloak.models.map.userSession.MapUserSessionProviderFactory; import org.keycloak.sessions.AuthenticationSessionSpi; import org.keycloak.testsuite.model.KeycloakModelParameters; @@ -51,6 +57,8 @@ public class Map extends KeycloakModelParameters { static final Set> ALLOWED_SPIS = ImmutableSet.>builder() .add(AuthenticationSessionSpi.class) + .add(ActionTokenStoreSpi.class) + .add(SingleUseObjectSpi.class) .add(MapStorageSpi.class) .build(); @@ -69,6 +77,8 @@ public class Map extends KeycloakModelParameters { .add(MapUserLoginFailureProviderFactory.class) .add(NoLockingDBLockProviderFactory.class) .add(MapEventStoreProviderFactory.class) + .add(ActionTokenStoreProviderFactory.class) + .add(SingleUseObjectProviderFactory.class) .build(); public Map() { @@ -78,6 +88,8 @@ public class Map extends KeycloakModelParameters { @Override public void updateConfig(Config cf) { cf.spi(AuthenticationSessionSpi.PROVIDER_ID).defaultProvider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) + .spi(ActionTokenStoreSpi.NAME).defaultProvider(MapSingleUseObjectProviderFactory.PROVIDER_ID) + .spi(SingleUseObjectSpi.NAME).defaultProvider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .spi("client").defaultProvider(MapClientProviderFactory.PROVIDER_ID) .spi("clientScope").defaultProvider(MapClientScopeProviderFactory.PROVIDER_ID) .spi("group").defaultProvider(MapGroupProviderFactory.PROVIDER_ID) @@ -91,5 +103,6 @@ public class Map extends KeycloakModelParameters { .spi("dblock").defaultProvider(NoLockingDBLockProviderFactory.PROVIDER_ID) .spi(EventStoreSpi.NAME).defaultProvider(MapEventStoreProviderFactory.PROVIDER_ID) ; + cf.spi(MapStorageSpi.NAME).provider(ConcurrentHashMapStorageProviderFactory.PROVIDER_ID).config("keyType.single-use-objects", "string"); } } diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java new file mode 100644 index 0000000000..f8936d4acd --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/singleUseObject/SingleUseObjectModelTest.java @@ -0,0 +1,159 @@ +/* + * Copyright 2022 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.testsuite.model.singleUseObject; + +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; +import org.keycloak.common.util.Time; +import org.keycloak.models.ActionTokenKeyModel; +import org.keycloak.models.ActionTokenStoreProvider; +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.SingleUseObjectProvider; +import org.keycloak.models.UserModel; +import org.keycloak.testsuite.model.KeycloakModelTest; +import org.keycloak.testsuite.model.RequireProvider; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; + +@RequireProvider(ActionTokenStoreProvider.class) +@RequireProvider(SingleUseObjectProvider.class) +public class SingleUseObjectModelTest extends KeycloakModelTest { + + private String realmId; + + private String userId; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("realm"); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + realmId = realm.getId(); + UserModel user = s.users().addUser(realm, "user"); + userId = user.getId(); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + Time.setOffset(0); + s.realms().removeRealm(realmId); + } + + @Test + public void testActionTokens() { + ActionTokenKeyModel key = withRealm(realmId, (session, realm) -> { + ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class); + DefaultActionTokenKey actionTokenKey = new DefaultActionTokenKey(userId, UUID.randomUUID().toString(), Time.currentTime() + 60, null); + Map notes = new HashMap<>(); + notes.put("foo", "bar"); + actionTokenStore.put(actionTokenKey, notes); + return actionTokenKey; + }); + + inComittedTransaction(session -> { + ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class); + ActionTokenValueModel valueModel = actionTokenStore.get(key); + Assert.assertNotNull(valueModel); + Assert.assertEquals("bar", valueModel.getNote("foo")); + + valueModel = actionTokenStore.remove(key); + Assert.assertNotNull(valueModel); + Assert.assertEquals("bar", valueModel.getNote("foo")); + }); + + inComittedTransaction(session -> { + ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class); + ActionTokenValueModel valueModel = actionTokenStore.get(key); + Assert.assertNull(valueModel); + + Map notes = new HashMap<>(); + notes.put("foo", "bar"); + actionTokenStore.put(key, notes); + }); + + inComittedTransaction(session -> { + ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class); + ActionTokenValueModel valueModel = actionTokenStore.get(key); + Assert.assertNotNull(valueModel); + Assert.assertEquals("bar", valueModel.getNote("foo")); + + Time.setOffset(70); + + valueModel = actionTokenStore.get(key); + Assert.assertNull(valueModel); + }); + } + + @Test + public void testSingleUseStore() { + String key = UUID.randomUUID().toString(); + Map notes = new HashMap<>(); + notes.put("foo", "bar"); + + Map notes2 = new HashMap<>(); + notes2.put("baf", "meow"); + + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); + Assert.assertFalse(singleUseStore.replace(key, notes2)); + + singleUseStore.put(key, 60, notes); + }); + + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); + Map actualNotes = singleUseStore.get(key); + Assert.assertEquals(notes, actualNotes); + + Assert.assertTrue(singleUseStore.replace(key, notes2)); + }); + + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); + Map actualNotes = singleUseStore.get(key); + Assert.assertEquals(notes2, actualNotes); + + Assert.assertFalse(singleUseStore.putIfAbsent(key, 60)); + + Assert.assertEquals(notes2, singleUseStore.remove(key)); + }); + + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); + Assert.assertTrue(singleUseStore.putIfAbsent(key, 60)); + }); + + inComittedTransaction(session -> { + SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class); + Map actualNotes = singleUseStore.get(key); + assertThat(actualNotes, Matchers.anEmptyMap()); + + Time.setOffset(70); + + Assert.assertNull(singleUseStore.get(key)); + }); + } +} diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml index 275990d30b..22e74750fa 100755 --- a/testsuite/utils/pom.xml +++ b/testsuite/utils/pom.xml @@ -297,6 +297,8 @@ keycloak.userSession.providermap keycloak.loginFailure.providermap keycloak.authorization.providermap + keycloak.actionToken.providermap + keycloak.singleUseObject.providermap keycloak.authorizationCache.enabledfalse keycloak.realmCache.enabledfalse keycloak.userCache.enabledfalse diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index ae865c1044..b0381b3c3f 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -103,10 +103,29 @@ } }, + "actionToken": { + "provider": "${keycloak.actionToken.provider:infinispan}", + "map": { + "storage": { + "provider": "${keycloak.actionToken.map.storage.provider:concurrenthashmap}" + } + } + }, + + "singleUseObject": { + "provider": "${keycloak.singleUseObject.provider:infinispan}", + "map": { + "storage": { + "provider": "${keycloak.singleUseObject.map.storage.provider:concurrenthashmap}" + } + } + }, + "mapStorage": { "provider": "${keycloak.mapStorage.provider:}", "concurrenthashmap": { "dir": "${project.build.directory:target/map}", + "keyType.single-use-objects": "string", "keyType.realms": "string", "keyType.authz-resource-servers": "string" },