Skip to content

Commit

Permalink
Avoid the same userSessionId after re-authentication (#138)
Browse files Browse the repository at this point in the history
Closes #69

Signed-off-by: Giuseppe Graziano <[email protected]>
  • Loading branch information
graziang authored Mar 23, 2024
1 parent abd03e3 commit ab3b30f
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
Expand All @@ -50,12 +51,14 @@
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;

import jakarta.ws.rs.core.MultivaluedHashMap;
Expand Down Expand Up @@ -936,6 +939,22 @@ public static void resetFlow(AuthenticationSessionModel authSession, String flow
authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
}

// Recreate new root auth session and new auth session from the given auth session.
public static AuthenticationSessionModel recreate(KeycloakSession session, AuthenticationSessionModel authSession) {
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
RootAuthenticationSessionModel rootAuthenticationSession = authenticationSessionManager.createAuthenticationSession(authSession.getRealm(), true);
AuthenticationSessionModel newAuthSession = rootAuthenticationSession.createAuthenticationSession(authSession.getClient());
newAuthSession.setRedirectUri(authSession.getRedirectUri());
newAuthSession.setProtocol(authSession.getProtocol());

for (Map.Entry<String, String> clientNote : authSession.getClientNotes().entrySet()) {
newAuthSession.setClientNote(clientNote.getKey(), clientNote.getValue());
}

authenticationSessionManager.removeAuthenticationSession(authSession.getRealm(), authSession, true);
RestartLoginCookie.setRestartCookie(session, authSession.getRealm(), session.getContext().getConnection(), session.getContext().getUri(), authSession);
return newAuthSession;
}

// Clone new authentication session from the given authSession. New authenticationSession will have same parent (rootSession) and will use same client
public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,12 @@ public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId
if (userSession != null) {
logger.debugf("Logout of user session %s when restarting flow during re-authentication", userSession.getId());
AuthenticationManager.backchannelLogout(session, userSession, false);
authSession = AuthenticationProcessor.recreate(session, authSession);
}

AuthenticationProcessor.resetFlow(authSession, flowPath);

URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId);
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), authSession.getTabId());
logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
Expand Down Expand Up @@ -301,6 +304,67 @@ public void identityFirstFormReauthenticationWithGithubLink() {
BrowserFlowTest.revertFlows(testRealm(), "browser - identity first");
}

@Test
public void restartLoginWithNewRootAuthSession() {
loginPage.open();
loginPage.login("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");

oauth.prompt(OIDCLoginProtocol.PROMPT_VALUE_LOGIN);
loginPage.open();
loginPage.clickResetLogin();
loginPage.login("john-doh@localhost", "password");

code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response2 = oauth.doAccessTokenRequest(code, "password");


AccessToken accessToken1 = oauth.verifyToken(response1.getAccessToken());
AccessToken accessToken2 = oauth.verifyToken(response2.getAccessToken());

Assert.assertNotEquals(accessToken1.getSubject(), accessToken2.getSubject());
Assert.assertNotEquals(accessToken1.getSessionId(), accessToken2.getSessionId());
}

@Test
public void loginAfterExpiredUserSession() {
RealmRepresentation rep = testRealm().toRepresentation();
Integer originalSsoSessionIdleTimeout = rep.getSsoSessionIdleTimeout();
Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();

rep.setSsoSessionIdleTimeout(10);
rep.setSsoSessionMaxLifespan(10);
realmsResouce().realm(rep.getRealm()).update(rep);

loginPage.open();
driver.navigate().refresh();
loginPage.login("test-user@localhost", "password");

String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");

//set time offset after user session expiration (10s) but before accessCodeLifespanLogin (1800s) and accessCodeLifespan (60s)
setTimeOffset(20);

loginPage.open();
loginPage.login("john-doh@localhost", "password");

code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response2 = oauth.doAccessTokenRequest(code, "password");

AccessToken accessToken1 = oauth.verifyToken(response1.getAccessToken());
AccessToken accessToken2 = oauth.verifyToken(response2.getAccessToken());

Assert.assertNotEquals(accessToken1.getSubject(), accessToken2.getSubject());
Assert.assertNotEquals(accessToken1.getSessionId(), accessToken2.getSessionId());

setTimeOffset(0);
rep.setSsoSessionIdleTimeout(originalSsoSessionIdleTimeout);
rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
realmsResouce().realm(rep.getRealm()).update(rep);
}

private void setupIdentityFirstFlow() {
String newFlowAlias = "browser - identity first";
testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias));
Expand Down

0 comments on commit ab3b30f

Please sign in to comment.