KEYCLOAK-15719 CORS headers missing on userinfo error response
This commit is contained in:
parent
cb12fed96e
commit
456cdc51f2
2 changed files with 79 additions and 27 deletions
|
@ -44,7 +44,6 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
|||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.services.CorsErrorResponseException;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.UserInfoRequestContext;
|
||||
|
@ -89,6 +88,7 @@ public class UserInfoEndpoint {
|
|||
private final org.keycloak.protocol.oidc.TokenManager tokenManager;
|
||||
private final AppAuthManager appAuthManager;
|
||||
private final RealmModel realm;
|
||||
private Cors cors;
|
||||
|
||||
public UserInfoEndpoint(org.keycloak.protocol.oidc.TokenManager tokenManager, RealmModel realm) {
|
||||
this.realm = realm;
|
||||
|
@ -126,18 +126,20 @@ public class UserInfoEndpoint {
|
|||
return issueUserInfo(accessToken);
|
||||
}
|
||||
|
||||
private ErrorResponseException newUnauthorizedErrorResponseException(String oauthError, String errorMessage) {
|
||||
// This method won't add allowedOrigins to the cors. Assumption is that allowedOrigins are already set to the "cors" object when this method is called
|
||||
private CorsErrorResponseException newUnauthorizedErrorResponseException(String oauthError, String errorMessage) {
|
||||
// See: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError
|
||||
response.getOutputHeaders().put(HttpHeaders.WWW_AUTHENTICATE, Collections.singletonList(String.format("Bearer realm=\"%s\", error=\"%s\", error_description=\"%s\"", realm.getName(), oauthError, errorMessage)));
|
||||
return new ErrorResponseException(oauthError, errorMessage, Response.Status.UNAUTHORIZED);
|
||||
return new CorsErrorResponseException(cors, oauthError, errorMessage, Response.Status.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
private Response issueUserInfo(String tokenString) {
|
||||
cors = Cors.add(request).auth().allowedMethods(request.getHttpMethod()).auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
|
||||
|
||||
try {
|
||||
session.clientPolicy().triggerOnEvent(new UserInfoRequestContext(tokenString));
|
||||
} catch (ClientPolicyException cpe) {
|
||||
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||
throw new CorsErrorResponseException(cors.allowAllOrigins(), cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||
}
|
||||
|
||||
EventBuilder event = new EventBuilder(realm, session, clientConnection)
|
||||
|
@ -146,11 +148,11 @@ public class UserInfoEndpoint {
|
|||
|
||||
if (tokenString == null) {
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Token not provided", Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "Token not provided", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
AccessToken token;
|
||||
ClientModel clientModel;
|
||||
ClientModel clientModel = null;
|
||||
try {
|
||||
TokenVerifier<AccessToken> verifier = TokenVerifier.create(tokenString, AccessToken.class).withDefaultChecks()
|
||||
.realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
|
||||
|
@ -163,20 +165,25 @@ public class UserInfoEndpoint {
|
|||
clientModel = realm.getClientByClientId(token.getIssuedFor());
|
||||
if (clientModel == null) {
|
||||
event.error(Errors.CLIENT_NOT_FOUND);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "Client not found", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
cors.allowedOrigins(session, clientModel);
|
||||
|
||||
TokenVerifier.createWithoutSignature(token)
|
||||
.withChecks(NotBeforeCheck.forModel(clientModel))
|
||||
.verify();
|
||||
} catch (VerificationException e) {
|
||||
if (clientModel == null) {
|
||||
cors.allowAllOrigins();
|
||||
}
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token verification failed");
|
||||
}
|
||||
|
||||
if (!clientModel.getProtocol().equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
||||
event.error(Errors.INVALID_CLIENT);
|
||||
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
session.getContext().setClient(clientModel);
|
||||
|
@ -185,7 +192,7 @@ public class UserInfoEndpoint {
|
|||
|
||||
if (!clientModel.isEnabled()) {
|
||||
event.error(Errors.CLIENT_DISABLED);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
UserSessionModel userSession = findValidSession(token, event, clientModel);
|
||||
|
@ -193,7 +200,7 @@ public class UserInfoEndpoint {
|
|||
UserModel userModel = userSession.getUser();
|
||||
if (userModel == null) {
|
||||
event.error(Errors.USER_NOT_FOUND);
|
||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST);
|
||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
event.user(userModel)
|
||||
|
@ -268,7 +275,7 @@ public class UserInfoEndpoint {
|
|||
|
||||
event.success();
|
||||
|
||||
return Cors.add(request, responseBuilder).auth().allowedOrigins(session, clientModel).build();
|
||||
return cors.builder(responseBuilder).build();
|
||||
}
|
||||
|
||||
|
||||
|
@ -303,7 +310,7 @@ public class UserInfoEndpoint {
|
|||
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired");
|
||||
}
|
||||
|
||||
private void checkTokenIssuedAt(AccessToken token, UserSessionModel userSession, EventBuilder event) throws ErrorResponseException {
|
||||
private void checkTokenIssuedAt(AccessToken token, UserSessionModel userSession, EventBuilder event) throws CorsErrorResponseException {
|
||||
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
|
||||
event.error(Errors.INVALID_TOKEN);
|
||||
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Stale token");
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.keycloak.testsuite.oauth;
|
||||
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
|
@ -30,7 +31,7 @@ public class UserInfoEndpointCorsTest extends AbstractKeycloakTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void userInfoCorsRequestWithValidUrl() throws Exception {
|
||||
public void userInfoCorsValidRequestWithValidUrl() throws Exception {
|
||||
|
||||
oauth.realm("test");
|
||||
oauth.clientId("test-app2");
|
||||
|
@ -38,19 +39,54 @@ public class UserInfoEndpointCorsTest extends AbstractKeycloakTest {
|
|||
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest(null, "test-user@localhost", "password");
|
||||
|
||||
WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(AdminClientUtil.createResteasyClient());
|
||||
Response userInfoResponse = userInfoTarget.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessTokenResponse.getAccessToken())
|
||||
.header("Origin", VALID_CORS_URL) // manually trigger CORS handling
|
||||
.get();
|
||||
ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient();
|
||||
try {
|
||||
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();
|
||||
|
||||
UserInfoClientUtil.testSuccessfulUserInfoResponse(userInfoResponse, "test-user@localhost", "test-user@localhost");
|
||||
UserInfoClientUtil.testSuccessfulUserInfoResponse(userInfoResponse, "test-user@localhost", "test-user@localhost");
|
||||
|
||||
assertCors(userInfoResponse);
|
||||
assertCors(userInfoResponse);
|
||||
} finally {
|
||||
resteasyClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
// KEYCLOAK-15719 error response should still contain CORS headers
|
||||
@Test
|
||||
public void userInfoCorsInvalidRequestWithValidUrl() 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");
|
||||
|
||||
// Set time offset to make sure that userInfo request will be invalid due the expired token
|
||||
setTimeOffset(600);
|
||||
|
||||
ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient();
|
||||
try {
|
||||
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);
|
||||
} finally {
|
||||
resteasyClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userInfoCorsRequestWithInvalidUrlShouldFail() throws Exception {
|
||||
public void userInfoCorsValidRequestWithInvalidUrlShouldFail() throws Exception {
|
||||
|
||||
oauth.realm("test");
|
||||
oauth.clientId("test-app2");
|
||||
|
@ -58,23 +94,32 @@ public class UserInfoEndpointCorsTest extends AbstractKeycloakTest {
|
|||
|
||||
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doGrantAccessTokenRequest(null, "test-user@localhost", "password");
|
||||
|
||||
WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(AdminClientUtil.createResteasyClient());
|
||||
Response userInfoResponse = userInfoTarget.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessTokenResponse.getAccessToken())
|
||||
.header("Origin", INVALID_CORS_URL) // manually trigger CORS handling
|
||||
.get();
|
||||
ResteasyClient resteasyClient = AdminClientUtil.createResteasyClient();
|
||||
try {
|
||||
WebTarget userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(resteasyClient);
|
||||
Response userInfoResponse = userInfoTarget.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, "bearer " + accessTokenResponse.getAccessToken())
|
||||
.header("Origin", INVALID_CORS_URL) // manually trigger CORS handling
|
||||
.get();
|
||||
|
||||
assertNotCors(userInfoResponse);
|
||||
UserInfoClientUtil.testSuccessfulUserInfoResponse(userInfoResponse, "test-user@localhost", "test-user@localhost");
|
||||
|
||||
assertNotCors(userInfoResponse);
|
||||
} finally {
|
||||
resteasyClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
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"));
|
||||
response.close();
|
||||
}
|
||||
|
||||
private static void assertNotCors(Response response) {
|
||||
assertNull(response.getHeaders().get("Access-Control-Allow-Credentials"));
|
||||
assertNull(response.getHeaders().get("Access-Control-Allow-Origin"));
|
||||
assertNull(response.getHeaders().get("Access-Control-Expose-Headers"));
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue