8000 Forged authorization code lets any client detach another client's client session (26.8) by graziang · Pull Request #55041 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
< 8000 tbody>
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,17 @@ public Response process(Context context) {

OAuth2CodeParser.ParseResult parseResult = OAuth2CodeParser.parseCode(session, code, realm, event);
if (parseResult.isIllegalCode()) {
// Attempt to use same code twice should invalidate existing clientSession.
// Detach must persist even when the error response rolls back the main tx.
KeycloakModelUtils.enlistAfterRollback(session, ctx -> {
AuthenticatedClientSessionModel cs = ctx.findClientSession(parseResult.getClientSession());
if (cs != null) cs.detachFromUserSession();
});
AuthenticatedClientSessionModel illegalCodeClientSession = parseResult.getClientSession();

// Attempt to use same code twice should invalidate existing clientSession. Only the client the clientSession
// belongs to is allowed to invalidate it, otherwise any client could detach another client's session with a forged code.
if (illegalCodeClientSession != null && client.getId().equals(illegalCodeClientSession.getClient().getId())) {
// Detach must persist even when the error response rolls back the main tx.
KeycloakModelUtils.enlistAfterRollback(session, ctx -> {
AuthenticatedClientSessionModel cs = ctx.findClientSession(illegalCodeClientSession);
if (cs != null) cs.detachFromUserSession();
});
}

event.error(Errors.INVALID_CODE);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,80 @@ public void authorizationCodeCannotBeRedeemedByDifferentUser() {
EventAssertion.assertError(events.poll()).type(EventType.CODE_TO_TOKEN_ERROR).error(Errors.INVALID_CODE);
}

@Test
public void forgedCodeDoesNotDetachClientSessionOfAnotherClient() {
String redirectUri = oauth.getRedirectUri();

ClientRepresentation clientRep = ClientBuilder.create("attacker-client")
.secret("secret")
.redirectUris(redirectUri)
.build();
try (Response resp = managedRealm.admin().clients().create(clientRep)) {
managedRealm.cleanup().add(r -> r.clients().delete(ApiUtil.getCreatedId(resp)));

// The victim client logs in and exchanges its code for tokens
AuthorizationEndpointResponse victimLogin = oauth
.client("test-app", "test-secret")
.redirectUri(redirectUri)
.loginForm()
.doLogin("test-user@localhost", "password");
AccessTokenResponse victimTokens = oauth.accessTokenRequest(victimLogin.getCode()).send();
assertTrue(victimTokens.isSuccess());

String victimClientUuid = managedRealm.admin()
.clients()
.findByClientId("test-app")
.get(0)
.getId();
String forgedCode = "forged-code." + victimLogin.getSessionState() + "." + victimClientUuid;

events.clear();

// The attacker client sends a forged code pointing at the victim's client session
AccessTokenResponse tokenResponse = oauth
.client("attacker-client", "secret")
.redirectUri(redirectUri)
.accessTokenRequest(forgedCode)
.send();

Assertions.assertFalse(tokenResponse.isSuccess());
assertEquals(INVALID_GRANT, tokenResponse.getError());
EventAssertion.assertError(events.poll()).type(EventType.CODE_TO_TOKEN_ERROR).error(Errors.INVALID_CODE);

// The victim client session must not have been detached, so its refresh token still works
AccessTokenResponse refreshResponse = oauth
.client("test-app", "test-secret")
.redirectUri(redirectUri)
.doRefreshTokenRequest(victimTokens.getRefreshToken());
assertTrue(refreshResponse.isSuccess());
}
}

@Test
public void reusedCodeStillDetachesOwnClientSession() {
String redirectUri = oauth.getRedirectUri();

AuthorizationEndpointResponse login = oauth
.client("test-app", "test-secret")
.redirectUri(redirectUri)
.loginForm()
.doLogin("test-user@localhost", "password");
String code = login.getCode();

AccessTokenResponse tokens = oauth.accessTokenRequest(code).send();
assertTrue(tokens.isSuccess());

events.clear();

// Reusing the own code detaches the client session, so the previously issued refresh token stops working
AccessTokenResponse reuseResponse = oauth.accessTokenRequest(code).send();
Assertions.assertFalse(reuseResponse.isSuccess());
assertEquals(INVALID_GRANT, reuseResponse.getError());

AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(tokens.getRefreshToken());
Assertions.assertFalse(refreshResponse.isSuccess());
}

private static class AuthzCodeRealmConfig implements RealmConfig {

@Override
Expand Down
Loading
0