KEYCLOAK-15719 CORS headers missing on userinfo error response

This commit is contained in:
mposolda 2021-02-11 09:13:27 +01:00 committed by Marek Posolda
parent cb12fed96e
commit 456cdc51f2
2 changed files with 79 additions and 27 deletions

View file

@ -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");

View file

@ -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();
}
}