Cors modifications for UserInfo endpoint

Closes #26782

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-02-06 13:04:59 +01:00 committed by Marek Posolda
parent 67f6f2f657
commit bc82929e3a
6 changed files with 53 additions and 2 deletions

View file

@ -83,6 +83,8 @@ public interface Cors extends Provider {
public Cors exposedHeaders(String... exposedHeaders); public Cors exposedHeaders(String... exposedHeaders);
public Cors addExposedHeaders(String... exposedHeaders);
public Response build(); public Response build();
public boolean build(HttpResponse response); public boolean build(HttpResponse response);

View file

@ -223,7 +223,7 @@ public class UserInfoEndpoint {
throw error.invalidToken("Client disabled"); throw error.invalidToken("Client disabled");
} }
UserSessionModel userSession = UserSessionUtil.findValidSession(session, realm, token, event, clientModel); UserSessionModel userSession = UserSessionUtil.findValidSession(session, realm, token, event, clientModel, error);
UserModel userModel = userSession.getUser(); UserModel userModel = userSession.getUser();
if (userModel == null) { if (userModel == null) {

View file

@ -121,6 +121,16 @@ public class DefaultCors implements Cors {
return this; return this;
} }
@Override
public Cors addExposedHeaders(String... exposedHeaders) {
if (this.exposedHeaders == null) {
this.exposedHeaders(exposedHeaders);
} else {
this.exposedHeaders.addAll(Arrays.asList(exposedHeaders));
}
return this;
}
@Override @Override
public Response build() { public Response build() {
if (builder == null) { if (builder == null) {

View file

@ -30,6 +30,11 @@ public class UserSessionUtil {
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) { public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) {
OAuth2Error error = new OAuth2Error().json(false).realm(realm); OAuth2Error error = new OAuth2Error().json(false).realm(realm);
return findValidSession(session, realm, token, event, client, error);
}
public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm,
AccessToken token, EventBuilder event, ClientModel client, OAuth2Error error) {
if (token.getSessionState() == null) { if (token.getSessionState() == null) {
return createTransientSessionForClient(session, realm, token, client, event); return createTransientSessionForClient(session, realm, token, client, event);
} }

View file

@ -125,7 +125,6 @@ public class OAuth2Error {
try { try {
Constructor<? extends WebApplicationException> constructor = clazz.getConstructor(new Class[] { Response.class }); Constructor<? extends WebApplicationException> constructor = clazz.getConstructor(new Class[] { Response.class });
cors.ifPresent(_cors -> { _cors.build(builder::header); });
if (json) { if (json) {
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription); OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription);
@ -137,8 +136,10 @@ public class OAuth2Error {
bearer.setErrorDescription(errorDescription); bearer.setErrorDescription(errorDescription);
WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer); WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer);
wwwAuthenticate.build(builder::header); wwwAuthenticate.build(builder::header);
cors.ifPresent(_cors -> _cors.addExposedHeaders(WWW_AUTHENTICATE));
builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE); builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE);
} }
cors.ifPresent(_cors -> { _cors.build(builder::header); });
return constructor.newInstance(builder.build()); return constructor.newInstance(builder.build());
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.oauth;
import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.junit.Test; import org.junit.Test;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.AdminClientUtil;
@ -14,6 +15,8 @@ import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import java.util.List; import java.util.List;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
@ -110,9 +113,39 @@ public class UserInfoEndpointCorsTest extends AbstractKeycloakTest {
} }
} }
@Test
public void userInfoCorsInvalidSession() throws Exception {
oauth.realm("test");
oauth.clientId("test-app2");
oauth.redirectUri(VALID_CORS_URL + "/realms/master/app");
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest(null, "test-user@localhost", "password");
// remove the session in keycloak
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken());
adminClient.realm("test").deleteSession(accessToken.getSessionState());
try (ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient()) {
WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(resteasyClient);
Response userInfoResponse = userInfoTarget.request()
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessTokenResponse.getAccessToken())
.header("Origin", VALID_CORS_URL) // manually trigger CORS handling
.get();
// We should have errorResponse, but CORS headers should be there as origin was valid
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), userInfoResponse.getStatus());
assertCors(userInfoResponse);
}
}
private static void assertCors(Response response) { private static void assertCors(Response response) {
assertEquals("true", response.getHeaders().getFirst("Access-Control-Allow-Credentials")); assertEquals("true", response.getHeaders().getFirst("Access-Control-Allow-Credentials"));
assertEquals(VALID_CORS_URL, response.getHeaders().getFirst("Access-Control-Allow-Origin")); assertEquals(VALID_CORS_URL, response.getHeaders().getFirst("Access-Control-Allow-Origin"));
assertThat((String) response.getHeaders().getFirst("Access-Control-Expose-Headers"), containsString("Access-Control-Allow-Methods"));
if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) {
assertThat((String) response.getHeaders().getFirst("Access-Control-Expose-Headers"), containsString("WWW-Authenticate"));
}
response.close(); response.close();
} }