Cors modifications for UserInfo endpoint
Closes #26782 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
67f6f2f657
commit
bc82929e3a
6 changed files with 53 additions and 2 deletions
|
@ -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);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue