KEYCLOAK-15770 Skip creating session for docker protocol authentication
This commit is contained in:
parent
1a1c42c776
commit
ff05072c16
20 changed files with 381 additions and 33 deletions
|
@ -192,8 +192,12 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(false);
|
||||
AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, this, entity, client, userSession, userSessionUpdateTx, clientSessionUpdateTx, false);
|
||||
|
||||
// For now, the clientSession is considered transient in case that userSession was transient
|
||||
UserSessionModel.SessionPersistenceState persistenceState = (userSession instanceof UserSessionAdapter && ((UserSessionAdapter) userSession).getPersistenceState() != null) ?
|
||||
((UserSessionAdapter) userSession).getPersistenceState() : UserSessionModel.SessionPersistenceState.PERSISTENT;
|
||||
|
||||
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
|
||||
clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity);
|
||||
clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState);
|
||||
|
||||
SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId);
|
||||
userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask);
|
||||
|
@ -204,19 +208,21 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
@Override
|
||||
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
||||
final String userSessionId = keyGenerator.generateKeyString(session, sessionCache);
|
||||
return createUserSession(userSessionId, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
|
||||
return createUserSession(userSessionId, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
||||
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress,
|
||||
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
UserSessionEntity entity = new UserSessionEntity();
|
||||
entity.setId(id);
|
||||
updateSessionEntity(entity, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId);
|
||||
|
||||
SessionUpdateTask<UserSessionEntity> createSessionTask = Tasks.addIfAbsentSync();
|
||||
sessionTx.addTask(id, createSessionTask, entity);
|
||||
sessionTx.addTask(id, createSessionTask, entity, persistenceState);
|
||||
|
||||
UserSessionAdapter adapter = wrap(realm, entity, false);
|
||||
adapter.setPersistenceState(persistenceState);
|
||||
|
||||
if (adapter != null) {
|
||||
DeviceActivityManager.attachDevice(adapter, session);
|
||||
|
@ -694,7 +700,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
entity.setUserId(userId);
|
||||
|
||||
SessionUpdateTask<LoginFailureEntity> createLoginFailureTask = Tasks.addIfAbsentSync();
|
||||
loginFailuresTx.addTask(key, createLoginFailureTask, entity);
|
||||
loginFailuresTx.addTask(key, createLoginFailureTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
|
||||
return wrap(key, entity);
|
||||
}
|
||||
|
@ -999,7 +1005,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx = getClientSessionTransaction(offline);
|
||||
|
||||
SessionUpdateTask<UserSessionEntity> importTask = Tasks.addIfAbsentSync();
|
||||
userSessionUpdateTx.addTask(userSession.getId(), importTask, entity);
|
||||
userSessionUpdateTx.addTask(userSession.getId(), importTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
|
||||
UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline);
|
||||
|
||||
|
@ -1048,7 +1054,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
final UUID clientSessionId = entity.getId();
|
||||
|
||||
SessionUpdateTask<AuthenticatedClientSessionEntity> createClientSessionTask = Tasks.addIfAbsentSync();
|
||||
clientSessionUpdateTx.addTask(entity.getId(), createClientSessionTask, entity);
|
||||
clientSessionUpdateTx.addTask(entity.getId(), createClientSessionTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
|
||||
AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions();
|
||||
clientSessions.put(clientSession.getClient().getId(), clientSessionId);
|
||||
|
|
|
@ -64,6 +64,8 @@ public class UserSessionAdapter implements UserSessionModel {
|
|||
|
||||
private final boolean offline;
|
||||
|
||||
private SessionPersistenceState persistenceState;
|
||||
|
||||
public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider,
|
||||
InfinispanChangelogBasedTransaction<String, UserSessionEntity> userSessionUpdateTx,
|
||||
InfinispanChangelogBasedTransaction<UUID, AuthenticatedClientSessionEntity> clientSessionUpdateTx,
|
||||
|
@ -309,6 +311,14 @@ public class UserSessionAdapter implements UserSessionModel {
|
|||
update(task);
|
||||
}
|
||||
|
||||
public SessionPersistenceState getPersistenceState() {
|
||||
return persistenceState;
|
||||
}
|
||||
|
||||
public void setPersistenceState(SessionPersistenceState persistenceState) {
|
||||
this.persistenceState = persistenceState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restartSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
||||
UserSessionUpdateTask task = new UserSessionUpdateTask() {
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.models.AbstractKeycloakTransaction;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.CacheDecorators;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.remotestore.RemoteCacheInvoker;
|
||||
|
@ -77,14 +78,14 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
|
||||
|
||||
// Create entity and new version for it
|
||||
public void addTask(K key, SessionUpdateTask<V> task, V entity) {
|
||||
public void addTask(K key, SessionUpdateTask<V> task, V entity, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("Null entity not allowed");
|
||||
}
|
||||
|
||||
RealmModel realm = kcSession.realms().getRealm(entity.getRealmId());
|
||||
SessionEntityWrapper<V> wrappedEntity = new SessionEntityWrapper<>(entity);
|
||||
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity);
|
||||
SessionUpdatesList<V> myUpdates = new SessionUpdatesList<>(realm, wrappedEntity, persistenceState);
|
||||
updates.put(key, myUpdates);
|
||||
|
||||
// Run the update now, so reader in same transaction can see it
|
||||
|
@ -149,6 +150,9 @@ public class InfinispanChangelogBasedTransaction<K, V extends SessionEntity> ext
|
|||
SessionUpdatesList<V> sessionUpdates = entry.getValue();
|
||||
SessionEntityWrapper<V> sessionWrapper = sessionUpdates.getEntityWrapper();
|
||||
|
||||
// Don't save transient entities to infinispan. They are valid just for current transaction
|
||||
if (sessionUpdates.getPersistenceState() == UserSessionModel.SessionPersistenceState.TRANSIENT) continue;
|
||||
|
||||
RealmModel realm = sessionUpdates.getRealm();
|
||||
|
||||
MergedUpdate<V> merged = MergedUpdate.computeUpdate(sessionUpdates.getUpdateTasks(), sessionWrapper);
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
/**
|
||||
|
@ -36,9 +37,16 @@ class SessionUpdatesList<S extends SessionEntity> {
|
|||
|
||||
private List<SessionUpdateTask<S>> updateTasks = new LinkedList<>();
|
||||
|
||||
private final UserSessionModel.SessionPersistenceState persistenceState;
|
||||
|
||||
public SessionUpdatesList(RealmModel realm, SessionEntityWrapper<S> entityWrapper) {
|
||||
this(realm, entityWrapper, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
}
|
||||
|
||||
public SessionUpdatesList(RealmModel realm, SessionEntityWrapper<S> entityWrapper, UserSessionModel.SessionPersistenceState persistenceState) {
|
||||
this.realm = realm;
|
||||
this.entityWrapper = entityWrapper;
|
||||
this.persistenceState = persistenceState;
|
||||
}
|
||||
|
||||
public RealmModel getRealm() {
|
||||
|
@ -61,4 +69,8 @@ class SessionUpdatesList<S extends SessionEntity> {
|
|||
public void setUpdateTasks(List<SessionUpdateTask<S>> updateTasks) {
|
||||
this.updateTasks = updateTasks;
|
||||
}
|
||||
|
||||
public UserSessionModel.SessionPersistenceState getPersistenceState() {
|
||||
return persistenceState;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,4 +91,27 @@ public interface UserSessionModel {
|
|||
LOGGED_OUT
|
||||
}
|
||||
|
||||
/**
|
||||
* Flag used when creating user session
|
||||
*/
|
||||
enum SessionPersistenceState {
|
||||
|
||||
/**
|
||||
* Session will be marked as persistent when created and it will be saved into the persistent storage (EG. infinispan cache).
|
||||
* This is the default behaviour
|
||||
*/
|
||||
PERSISTENT,
|
||||
|
||||
/**
|
||||
* This userSession will be valid just for the single request. Hence there won't be real
|
||||
* userSession created in the persistent store. Flag can be used for the protocols, which need just "dummy"
|
||||
* userSession to be able to run protocolMappers SPI. Example is DockerProtocol or OAuth2 client credentials grant.
|
||||
*/
|
||||
TRANSIENT;
|
||||
|
||||
public static SessionPersistenceState fromString(String sessionPersistenceString) {
|
||||
return (sessionPersistenceString == null) ? PERSISTENT : Enum.valueOf(SessionPersistenceState.class, sessionPersistenceString);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,7 +35,10 @@ public interface UserSessionProvider extends Provider {
|
|||
AuthenticatedClientSessionModel getClientSession(UserSessionModel userSession, ClientModel client, UUID clientSessionId, boolean offline);
|
||||
|
||||
UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
|
||||
UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
|
||||
|
||||
UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress,
|
||||
String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState);
|
||||
|
||||
UserSessionModel getUserSession(RealmModel realm, String id);
|
||||
List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
|
||||
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);
|
||||
|
|
|
@ -1011,8 +1011,10 @@ public class AuthenticationProcessor {
|
|||
|
||||
userSession = session.sessions().getUserSession(realm, authSession.getParentSession().getId());
|
||||
if (userSession == null) {
|
||||
UserSessionModel.SessionPersistenceState persistenceState = UserSessionModel.SessionPersistenceState.fromString(authSession.getClientNote(AuthenticationManager.USER_SESSION_PERSISTENT_STATE));
|
||||
|
||||
userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
|
||||
, remember, brokerSessionId, brokerUserId);
|
||||
, remember, brokerSessionId, brokerUserId, persistenceState);
|
||||
} else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||
userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
|
||||
, remember, brokerSessionId, brokerUserId);
|
||||
|
|
|
@ -264,7 +264,8 @@ public class PolicyEvaluationService {
|
|||
.createAuthenticationSession(clientModel);
|
||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
authSession.setAuthenticatedUser(userModel);
|
||||
userSession = keycloakSession.sessions().createUserSession(authSession.getParentSession().getId(), realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
|
||||
userSession = keycloakSession.sessions().createUserSession(authSession.getParentSession().getId(), realm, userModel,
|
||||
userModel.getUsername(), "127.0.0.1", "passwd", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
|
||||
AuthenticationManager.setClientScopesInSession(authSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(keycloakSession, userSession, authSession);
|
||||
|
|
|
@ -7,11 +7,13 @@ import org.keycloak.events.EventType;
|
|||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.AuthorizationEndpointBase;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.util.CacheControlUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel;
|
||||
|
@ -79,6 +81,9 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
|
|||
authenticationSession.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
|
||||
authenticationSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
|
||||
|
||||
// Use transient userSession for the docker protocol. There is no need to persist session as there is no endpoint for "refresh token" or "introspection"
|
||||
authenticationSession.setClientNote(AuthenticationManager.USER_SESSION_PERSISTENT_STATE, UserSessionModel.SessionPersistenceState.TRANSIENT.toString());
|
||||
|
||||
// Docker specific stuff
|
||||
authenticationSession.setClientNote(DockerAuthV2Protocol.ACCOUNT_PARAM, account);
|
||||
authenticationSession.setClientNote(DockerAuthV2Protocol.SERVICE_PARAM, service);
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.jboss.resteasy.spi.HttpResponse;
|
|||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.TokenVerifier;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.authorization.AuthorizationTokenService;
|
||||
|
@ -37,13 +36,10 @@ import org.keycloak.broker.provider.IdentityProviderMapper;
|
|||
import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.VerificationException;
|
||||
import org.keycloak.common.constants.ServiceAccountConstants;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
import org.keycloak.constants.AdapterConstants;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.crypto.SignatureVerifierContext;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
|
@ -719,8 +715,10 @@ public class TokenEndpoint {
|
|||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
|
||||
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
||||
|
||||
// TODO: This should create transient session by default - hence not persist userSession at all. However we should have compatibility switch for support
|
||||
// persisting of userSession
|
||||
UserSessionModel userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, clientUser, clientUsername,
|
||||
clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
|
||||
clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
event.session(userSession);
|
||||
|
||||
AuthenticationManager.setClientScopesInSession(authSession);
|
||||
|
|
|
@ -113,6 +113,12 @@ public class AuthenticationManager {
|
|||
public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
|
||||
public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN";
|
||||
|
||||
/**
|
||||
* Auth session note, which indicates if user session will be persistent (Saved to real persistent store) or
|
||||
* transient (transient session will be scoped to single request and hence there is no need to save it in the underlying store)
|
||||
*/
|
||||
public static final String USER_SESSION_PERSISTENT_STATE = "USER_SESSION_PERSISTENT_STATE";
|
||||
|
||||
/**
|
||||
* Auth session note on client logout state (when logging out)
|
||||
*/
|
||||
|
|
|
@ -36,12 +36,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import org.jboss.logging.Logger;
|
||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ProtocolMapperContainerModel;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
|
@ -197,7 +195,7 @@ public class ClientScopeEvaluateResource {
|
|||
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scopeParam);
|
||||
|
||||
userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, user, user.getUsername(),
|
||||
clientConnection.getRemoteAddr(), "example-auth", false, null, null);
|
||||
clientConnection.getRemoteAddr(), "example-auth", false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
|
||||
|
||||
AuthenticationManager.setClientScopesInSession(authSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(session, userSession, authSession);
|
||||
|
@ -213,9 +211,6 @@ public class ClientScopeEvaluateResource {
|
|||
if (authSession != null) {
|
||||
authSessionManager.removeAuthenticationSession(realm, authSession, false);
|
||||
}
|
||||
if (userSession != null) {
|
||||
session.sessions().removeUserSession(realm, userSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.forms;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class SetClientNoteAuthenticator implements Authenticator, AuthenticatorFactory {
|
||||
|
||||
protected static final Logger logger = Logger.getLogger(SetClientNoteAuthenticator.class);
|
||||
|
||||
public static final String PROVIDER_ID = "set-client-note-authenticator";
|
||||
|
||||
// Query parameters of this name will be used to save the client note to authentication session
|
||||
public static final String PREFIX = "note-";
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
|
||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||
|
||||
inputData.keySet().stream()
|
||||
.filter(paramName -> paramName.startsWith(PREFIX))
|
||||
.forEach(paramName -> {
|
||||
String key = paramName.substring(PREFIX.length());
|
||||
String value = inputData.getFirst(paramName);
|
||||
logger.infof("Set authentication session client note %s=%s", key, value);
|
||||
authSession.setClientNote(key, value);
|
||||
});
|
||||
|
||||
context.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void action(AuthenticationFlowContext context) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Set Client Note Authenticator";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getReferenceCategory() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
|
||||
AuthenticationExecutionModel.Requirement.REQUIRED
|
||||
};
|
||||
|
||||
@Override
|
||||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
|
||||
return REQUIREMENT_CHOICES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUserSetupAllowed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Set client note of specified name with the specified value to the authenticationSession.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
#
|
||||
|
||||
org.keycloak.testsuite.forms.PassThroughAuthenticator
|
||||
org.keycloak.testsuite.forms.SetClientNoteAuthenticator
|
||||
org.keycloak.testsuite.forms.PassThroughRegistration
|
||||
org.keycloak.testsuite.forms.ClickThroughAuthenticator
|
||||
org.keycloak.testsuite.authentication.ExpectedParamAuthenticatorFactory
|
||||
|
|
|
@ -558,6 +558,11 @@ public class OAuthClient {
|
|||
post.addHeader("User-Agent", userAgent);
|
||||
}
|
||||
|
||||
if (customParameters != null) {
|
||||
customParameters.keySet().stream()
|
||||
.forEach(paramName -> parameters.add(new BasicNameValuePair(paramName, customParameters.get(paramName))));
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity formEntity;
|
||||
try {
|
||||
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
|
||||
|
@ -1220,7 +1225,7 @@ public class OAuthClient {
|
|||
return this;
|
||||
}
|
||||
|
||||
public OAuthClient addCustomerParameter(String key, String value) {
|
||||
public OAuthClient addCustomParameter(String key, String value) {
|
||||
if (customParameters == null) {
|
||||
customParameters = new HashMap<>();
|
||||
}
|
||||
|
|
|
@ -196,6 +196,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||
addProviderInfo(result, "testsuite-dummy-registration", "Testsuite Dummy Pass Thru",
|
||||
"Testsuite Dummy authenticator. Just passes through and is hardcoded to a specific user");
|
||||
addProviderInfo(result, "set-client-note-authenticator", "Set Client Note Authenticator", "Set client note of specified name with the specified value to the authenticationSession.");
|
||||
addProviderInfo(result, "testsuite-username", "Testsuite Username Only",
|
||||
"Testsuite Username authenticator. Username parameter sets username");
|
||||
addProviderInfo(result, "webauthn-authenticator", "WebAuthn Authenticator", "Authenticator for WebAuthn. Usually used for WebAuthn two-factor authentication");
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright 2020 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.forms;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||
import org.keycloak.testsuite.util.FlowUtil;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.keycloak.models.AuthenticationExecutionModel.Requirement.REQUIRED;
|
||||
|
||||
/**
|
||||
* Test for transient user session
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
@AuthServerContainerExclude(AuthServerContainerExclude.AuthServer.REMOTE)
|
||||
public class TransientSessionTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginSuccess() throws Exception {
|
||||
setUpDirectGrantFlowWithSetClientNoteAuthenticator();
|
||||
|
||||
oauth.clientId("direct-grant");
|
||||
|
||||
// Signal that we want userSession to be transient
|
||||
oauth.addCustomParameter(SetClientNoteAuthenticator.PREFIX + AuthenticationManager.USER_SESSION_PERSISTENT_STATE, UserSessionModel.SessionPersistenceState.TRANSIENT.toString());
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
|
||||
|
||||
assertEquals(200, response.getStatusCode());
|
||||
|
||||
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
|
||||
// sessionState is available, but the session was transient and hence not really persisted on the server
|
||||
assertNotNull(accessToken.getSessionState());
|
||||
assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
|
||||
|
||||
// Refresh will fail. There is no userSession on the server
|
||||
OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
|
||||
Assert.assertNull(refreshedResponse.getAccessToken());
|
||||
assertNotNull(refreshedResponse.getError());
|
||||
Assert.assertEquals("Session not active", refreshedResponse.getErrorDescription());
|
||||
}
|
||||
|
||||
private void setUpDirectGrantFlowWithSetClientNoteAuthenticator() {
|
||||
final String newFlowAlias = "directGrantCustom";
|
||||
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyFlow(DefaultAuthenticationFlows.DIRECT_GRANT_FLOW, newFlowAlias));
|
||||
testingClient.server("test").run(session -> {
|
||||
FlowUtil.inCurrentRealm(session)
|
||||
.selectFlow(newFlowAlias)
|
||||
.addAuthenticatorExecution(REQUIRED, SetClientNoteAuthenticator.PROVIDER_ID)
|
||||
.defineAsDirectGrantFlow();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -116,7 +116,8 @@ public class CacheTest extends AbstractTestRealmKeycloakTest {
|
|||
user.setFirstName("firstName");
|
||||
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
|
||||
|
||||
UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null);
|
||||
UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache",
|
||||
"127.0.0.1", "auth", false, null, null, UserSessionModel.SessionPersistenceState.PERSISTENT);
|
||||
user = userSession.getUser();
|
||||
|
||||
user.setLastName("lastName");
|
||||
|
|
|
@ -397,6 +397,38 @@ public class UserSessionProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@ModelTest
|
||||
public void testTransientUserSession(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName("test");
|
||||
ClientModel client = realm.getClientByClientId("test-app");
|
||||
|
||||
// create an user session, but don't persist it to infinispan
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession session1) -> {
|
||||
long sessionsBefore = session1.sessions().getActiveUserSessions(realm, client);
|
||||
|
||||
UserSessionModel userSession = session1.sessions().createUserSession("123", realm, session1.users().getUserByUsername("user1", realm),
|
||||
"user1", "127.0.0.1", "form", true, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
|
||||
AuthenticatedClientSessionModel clientSession = session1.sessions().createClientSession(realm, client, userSession);
|
||||
assertEquals(userSession, clientSession.getUserSession());
|
||||
|
||||
assertSession(userSession, session.users().getUserByUsername("user1", realm), "127.0.0.1", userSession.getStarted(), userSession.getStarted(), "test-app");
|
||||
|
||||
// Can find session by ID in current transaction
|
||||
UserSessionModel foundSession = session1.sessions().getUserSession(realm, "123");
|
||||
Assert.assertEquals(userSession, foundSession);
|
||||
|
||||
// Count of sessions should be still the same
|
||||
Assert.assertEquals(sessionsBefore, session1.sessions().getActiveUserSessions(realm, client));
|
||||
});
|
||||
|
||||
// create an user session whose last refresh exceeds the max session idle timeout.
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession session1) -> {
|
||||
UserSessionModel userSession = session1.sessions().getUserSession(realm, "123");
|
||||
Assert.assertNull(userSession);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the removal of expired sessions with remember-me enabled. It differs from the non remember me scenario by
|
||||
* taking into consideration the specific remember-me timeout values.
|
||||
|
|
|
@ -226,9 +226,9 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
|
||||
Map<String, String> extraParams = new HashMap<>();
|
||||
|
||||
oauth.addCustomerParameter(OAuth2Constants.SCOPE, "read_write")
|
||||
.addCustomerParameter(OAuth2Constants.STATE, "abcdefg")
|
||||
.addCustomerParameter(OAuth2Constants.SCOPE, "pop push");
|
||||
oauth.addCustomParameter(OAuth2Constants.SCOPE, "read_write")
|
||||
.addCustomParameter(OAuth2Constants.STATE, "abcdefg")
|
||||
.addCustomParameter(OAuth2Constants.SCOPE, "pop push");
|
||||
|
||||
oauth.openLoginForm();
|
||||
|
||||
|
@ -242,11 +242,11 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
|
|||
public void authorizationRequestClientParamsMoreThanOnce() throws IOException {
|
||||
oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
|
||||
|
||||
oauth.addCustomerParameter(OAuth2Constants.SCOPE, "read_write")
|
||||
.addCustomerParameter(OAuth2Constants.CLIENT_ID, "client2client")
|
||||
.addCustomerParameter(OAuth2Constants.REDIRECT_URI, "https://www.example.com")
|
||||
.addCustomerParameter(OAuth2Constants.STATE, "abcdefg")
|
||||
.addCustomerParameter(OAuth2Constants.SCOPE, "pop push");
|
||||
oauth.addCustomParameter(OAuth2Constants.SCOPE, "read_write")
|
||||
.addCustomParameter(OAuth2Constants.CLIENT_ID, "client2client")
|
||||
.addCustomParameter(OAuth2Constants.REDIRECT_URI, "https://www.example.com")
|
||||
.addCustomParameter(OAuth2Constants.STATE, "abcdefg")
|
||||
.addCustomParameter(OAuth2Constants.SCOPE, "pop push");
|
||||
|
||||
oauth.openLoginForm();
|
||||
|
||||
|
|
Loading…
Reference in a new issue