diff --git a/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java b/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java index 792a74a215..ff05880861 100755 --- a/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java +++ b/server-spi-private/src/main/java/org/keycloak/services/cors/Cors.java @@ -83,6 +83,8 @@ public interface Cors extends Provider { public Cors exposedHeaders(String... exposedHeaders); + public Cors addExposedHeaders(String... exposedHeaders); + public Response build(); public boolean build(HttpResponse response); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java index ff129355f9..179ec0e47d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/UserInfoEndpoint.java @@ -223,7 +223,7 @@ public class UserInfoEndpoint { 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(); if (userModel == null) { diff --git a/services/src/main/java/org/keycloak/services/cors/DefaultCors.java b/services/src/main/java/org/keycloak/services/cors/DefaultCors.java index 18deca82b3..45b3dd869a 100755 --- a/services/src/main/java/org/keycloak/services/cors/DefaultCors.java +++ b/services/src/main/java/org/keycloak/services/cors/DefaultCors.java @@ -121,6 +121,16 @@ public class DefaultCors implements Cors { 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 public Response build() { if (builder == null) { diff --git a/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java b/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java index adcf763835..86403e4a0d 100644 --- a/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java +++ b/services/src/main/java/org/keycloak/services/util/UserSessionUtil.java @@ -30,6 +30,11 @@ public class UserSessionUtil { public static UserSessionModel findValidSession(KeycloakSession session, RealmModel realm, AccessToken token, EventBuilder event, ClientModel client) { 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) { return createTransientSessionForClient(session, realm, token, client, event); } diff --git a/services/src/main/java/org/keycloak/utils/OAuth2Error.java b/services/src/main/java/org/keycloak/utils/OAuth2Error.java index 19fbaf9b25..71fd1cbf8b 100644 --- a/services/src/main/java/org/keycloak/utils/OAuth2Error.java +++ b/services/src/main/java/org/keycloak/utils/OAuth2Error.java @@ -125,7 +125,6 @@ public class OAuth2Error { try { Constructor constructor = clazz.getConstructor(new Class[] { Response.class }); - cors.ifPresent(_cors -> { _cors.build(builder::header); }); if (json) { OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(error, errorDescription); @@ -137,8 +136,10 @@ public class OAuth2Error { bearer.setErrorDescription(errorDescription); WWWAuthenticate wwwAuthenticate = new WWWAuthenticate(bearer); wwwAuthenticate.build(builder::header); + cors.ifPresent(_cors -> _cors.addExposedHeaders(WWW_AUTHENTICATE)); builder.entity("").type(MediaType.TEXT_PLAIN_UTF_8_TYPE); } + cors.ifPresent(_cors -> { _cors.build(builder::header); }); return constructor.newInstance(builder.build()); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java index e26ae1b432..9ff1a5cfa7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/UserInfoEndpointCorsTest.java @@ -2,6 +2,7 @@ package org.keycloak.testsuite.oauth; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.junit.Test; +import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.util.AdminClientUtil; @@ -14,6 +15,8 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; 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.assertNull; 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) { assertEquals("true", response.getHeaders().getFirst("Access-Control-Allow-Credentials")); 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(); }