Map storage: Single-use objects (action token)

This commit is contained in:
Martin Kanis 2022-04-14 16:22:29 +02:00 committed by Hynek Mlnařík
parent 6dda69a634
commit 0cb3c95ed5
29 changed files with 1049 additions and 15 deletions

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractSingleUseObjectModel<E extends AbstractEntity> 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();
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapActionTokenProviderFactory extends AbstractMapProviderFactory<MapSingleUseObjectProvider, MapSingleUseObjectEntity, ActionTokenValueModel>
implements ActionTokenStoreProviderFactory<MapSingleUseObjectProvider> {
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";
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapSingleUseObjectAdapter extends AbstractSingleUseObjectModel<MapSingleUseObjectEntity> {
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<String, String> getNotes() {
Map<String, String> notes = entity.getNotes();
return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes);
}
@Override
public String getNote(String name) {
Map<String, String> notes = entity.getNotes();
return notes == null ? null : notes.get(name);
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
@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<String, String> getNotes();
void setNotes(Map<String, String> notes);
String getNote(String name);
void setNote(String key, String value);
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, SingleUseObjectProvider {
private static final Logger LOG = Logger.getLogger(MapSingleUseObjectProvider.class);
private final KeycloakSession session;
protected final MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> actionTokenStoreTx;
public MapSingleUseObjectProvider(KeycloakSession session, MapStorage<MapSingleUseObjectEntity, ActionTokenValueModel> 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<String, String> 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<ActionTokenValueModel> 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<ActionTokenValueModel> 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<ActionTokenValueModel> 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<String, String> 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<String, String> get(String key) {
LOG.tracef("get(%s)%s", key, getShortStackTrace());
MapSingleUseObjectEntity actionToken = getWithExpiration(key);
if (actionToken != null) {
Map<String, String> notes = actionToken.getNotes();
return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes);
}
return null;
}
@Override
public Map<String, String> remove(String key) {
LOG.tracef("remove(%s)%s", key, getShortStackTrace());
MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key);
if (singleUseEntity != null) {
Map<String, String> 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<String, String> 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;
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapSingleUseObjectProviderFactory extends AbstractMapProviderFactory<MapSingleUseObjectProvider, MapSingleUseObjectEntity, ActionTokenValueModel>
implements SingleUseObjectProviderFactory<MapSingleUseObjectProvider> {
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";
}
}

View file

@ -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<Class<?>, 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<?>, Class<? extends AbstractEntity>> 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);

View file

@ -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<String, StringKeyConverter> 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

View file

@ -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<SearchableModelField<UserSessionModel>, UpdatePredicatesFunc<Object, MapUserSessionEntity, UserSessionModel>> USER_SESSION_PREDICATES = basePredicates(UserSessionModel.SearchableFields.ID);
public static final Map<SearchableModelField<Event>, UpdatePredicatesFunc<Object, MapAuthEventEntity, Event>> AUTH_EVENTS_PREDICATES = basePredicates(Event.SearchableFields.ID);
public static final Map<SearchableModelField<AdminEvent>, UpdatePredicatesFunc<Object, MapAdminEventEntity, AdminEvent>> ADMIN_EVENTS_PREDICATES = basePredicates(AdminEvent.SearchableFields.ID);
public static final Map<SearchableModelField<ActionTokenValueModel>, UpdatePredicatesFunc<Object, MapSingleUseObjectEntity, ActionTokenValueModel>> ACTION_TOKEN_PREDICATES = basePredicates(ActionTokenValueModel.SearchableFields.ID);
@SuppressWarnings("unchecked")
private static final Map<Class<?>, 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 <K, V extends AbstractEntity, M, L extends Comparable<L>> void put(

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class SingleUseObjectConcurrentHashMapStorage<K, V extends AbstractEntity, M> extends ConcurrentHashMapStorage<K, MapSingleUseObjectEntity, ActionTokenValueModel> {
public SingleUseObjectConcurrentHashMapStorage(StringKeyConverter<K> keyConverter, DeepCloner cloner) {
super(ActionTokenValueModel.class, keyConverter, cloner);
}
@Override
@SuppressWarnings("unchecked")
public MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> createTransaction(KeycloakSession session) {
MapKeycloakTransaction<MapSingleUseObjectEntity, ActionTokenValueModel> 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<MapSingleUseObjectEntity> read(QueryParameters<ActionTokenValueModel> queryParameters) {
DefaultModelCriteria<ActionTokenValueModel> 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<K, MapSingleUseObjectEntity, ActionTokenValueModel> {
public Transaction(StringKeyConverter<K> keyConverter, DeepCloner cloner,
Map<SearchableModelField<? super ActionTokenValueModel>, MapModelCriteriaBuilder.UpdatePredicatesFunc<K, MapSingleUseObjectEntity, ActionTokenValueModel>> 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);
}
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
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;
}
}

View file

@ -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

View file

@ -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

View file

@ -22,6 +22,6 @@ import org.keycloak.provider.ProviderFactory;
*
* @author hmlnarik
*/
public interface ActionTokenStoreProviderFactory extends ProviderFactory<ActionTokenStoreProvider> {
public interface ActionTokenStoreProviderFactory<T extends ActionTokenStoreProvider> extends ProviderFactory<T> {
}

View file

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

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.models;
import org.keycloak.storage.SearchableModelField;
import java.util.Map;
/**
@ -24,6 +26,13 @@ import java.util.Map;
*/
public interface ActionTokenValueModel {
class SearchableFields {
public static final SearchableModelField<ActionTokenValueModel> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<ActionTokenValueModel> USER_ID = new SearchableModelField<>("userId", String.class);
public static final SearchableModelField<ActionTokenValueModel> ACTION_ID = new SearchableModelField<>("actionId", String.class);
public static final SearchableModelField<ActionTokenValueModel> ACTION_VERIFICATION_NONCE = new SearchableModelField<>("actionVerificationNonce", String.class);
}
/**
* Returns unmodifiable map of all notes.
* @return see description. Returns empty map if no note is set, never returns {@code null}.

View file

@ -843,6 +843,8 @@
<keycloak.loginFailure.provider>map</keycloak.loginFailure.provider>
<keycloak.authorization.provider>map</keycloak.authorization.provider>
<keycloak.eventsStore.provider>map</keycloak.eventsStore.provider>
<keycloak.actionToken.provider>map</keycloak.actionToken.provider>
<keycloak.singleUseObject.provider>map</keycloak.singleUseObject.provider>
<keycloak.authorizationCache.enabled>false</keycloak.authorizationCache.enabled>
<keycloak.realmCache.enabled>false</keycloak.realmCache.enabled>
<keycloak.userCache.enabled>false</keycloak.userCache.enabled>

View file

@ -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);
try {
setTimeOffset(2);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(400, response.getStatusCode());
setTimeOffset(0);
} 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)

View file

@ -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();
}

View file

@ -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"
},

View file

@ -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() {

View file

@ -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")

View file

@ -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();

View file

@ -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)

View file

@ -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);
}

View file

@ -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<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>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");
}
}

View file

@ -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<String, String> 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<String, String> 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<String, String> notes = new HashMap<>();
notes.put("foo", "bar");
Map<String, String> 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<String, String> actualNotes = singleUseStore.get(key);
Assert.assertEquals(notes, actualNotes);
Assert.assertTrue(singleUseStore.replace(key, notes2));
});
inComittedTransaction(session -> {
SingleUseObjectProvider singleUseStore = session.getProvider(SingleUseObjectProvider.class);
Map<String, String> 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<String, String> actualNotes = singleUseStore.get(key);
assertThat(actualNotes, Matchers.anEmptyMap());
Time.setOffset(70);
Assert.assertNull(singleUseStore.get(key));
});
}
}

View file

@ -297,6 +297,8 @@
<systemProperty><key>keycloak.userSession.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.loginFailure.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.authorization.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.actionToken.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.singleUseObject.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.authorizationCache.enabled</key><value>false</value></systemProperty>
<systemProperty><key>keycloak.realmCache.enabled</key><value>false</value></systemProperty>
<systemProperty><key>keycloak.userCache.enabled</key><value>false</value></systemProperty>

View file

@ -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"
},