KEYCLOAK-15770 Skip creating session for docker protocol authentication

This commit is contained in:
mposolda 2020-09-30 10:52:50 +02:00 committed by Marek Posolda
parent 1a1c42c776
commit ff05072c16
20 changed files with 381 additions and 33 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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