KEYCLOAK-6867 UserInfoEndpoint should return WWW-Authenticate header for Invalid tokens

As required by the OIDC spec (1) we now return a proper WWW-Authenticate
response header if the given token is invalid.

1) https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError
This commit is contained in:
Thomas Darimont 2019-12-20 15:56:23 +01:00 committed by Pedro Igor
parent 23b794aa51
commit 0219d62f09
2 changed files with 38 additions and 6 deletions

View file

@ -59,6 +59,7 @@ import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -119,6 +120,12 @@ public class UserInfoEndpoint {
return issueUserInfo(accessToken);
}
private ErrorResponseException 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);
}
private Response issueUserInfo(String tokenString) {
EventBuilder event = new EventBuilder(realm, session, clientConnection)
.event(EventType.USER_INFO_REQUEST)
@ -140,7 +147,7 @@ public class UserInfoEndpoint {
token = verifier.verify().getToken();
} catch (VerificationException e) {
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token invalid: " + e.getMessage(), Response.Status.UNAUTHORIZED);
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token verification failed");
}
ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor());
@ -179,7 +186,7 @@ public class UserInfoEndpoint {
if (OIDCAdvancedConfigWrapper.fromClientModel(clientModel).isUseMtlsHokToken()) {
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(token, request, session)) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Client certificate missing, or its thumbprint and one in the refresh token did NOT match", Response.Status.UNAUTHORIZED);
throw newUnauthorizedErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Client certificate missing, or its thumbprint and one in the refresh token did NOT match");
}
}
@ -246,7 +253,7 @@ public class UserInfoEndpoint {
if (userSession == null && offlineUserSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found or doesn't have client attached on it", Response.Status.UNAUTHORIZED);
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found or doesn't have client attached on it");
}
if (userSession != null) {
@ -256,13 +263,13 @@ public class UserInfoEndpoint {
}
event.error(Errors.SESSION_EXPIRED);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired");
}
private void checkTokenIssuedAt(AccessToken token, UserSessionModel userSession, EventBuilder event) throws ErrorResponseException {
if (token.getIssuedAt() + 1 < userSession.getStarted()) {
event.error(Errors.INVALID_TOKEN);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Stale token", Response.Status.UNAUTHORIZED);
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Stale token");
}
}
}

View file

@ -16,11 +16,13 @@
*/
package org.keycloak.testsuite.oidc;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
@ -71,6 +73,8 @@ import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
@ -296,11 +300,17 @@ public class UserInfoTest extends AbstractKeycloakTest {
try {
AccessTokenResponse accessTokenResponse = executeGrantAccessTokenRequest(client);
testingClient.testing().removeUserSessions("test");
String realmName = "test";
testingClient.testing().removeUserSessions(realmName);
Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getToken());
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
String wwwAuthHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
assertNotNull(wwwAuthHeader);
assertThat(wwwAuthHeader, CoreMatchers.containsString("Bearer"));
assertThat(wwwAuthHeader, CoreMatchers.containsString("realm=\"" + realmName + "\""));
assertThat(wwwAuthHeader, CoreMatchers.containsString("error=\"" + OAuthErrorException.INVALID_REQUEST + "\""));
response.close();
@ -329,6 +339,11 @@ public class UserInfoTest extends AbstractKeycloakTest {
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
String wwwAuthHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
assertNotNull(wwwAuthHeader);
assertThat(wwwAuthHeader, CoreMatchers.containsString("Bearer"));
assertThat(wwwAuthHeader, CoreMatchers.containsString("error=\"" + OAuthErrorException.INVALID_TOKEN + "\""));
response.close();
events.expect(EventType.USER_INFO_REQUEST_ERROR)
@ -367,6 +382,11 @@ public class UserInfoTest extends AbstractKeycloakTest {
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
String wwwAuthHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
assertNotNull(wwwAuthHeader);
assertThat(wwwAuthHeader, CoreMatchers.containsString("Bearer"));
assertThat(wwwAuthHeader, CoreMatchers.containsString("error=\"" + OAuthErrorException.INVALID_TOKEN + "\""));
response.close();
events.expect(EventType.USER_INFO_REQUEST_ERROR)
@ -410,6 +430,11 @@ public class UserInfoTest extends AbstractKeycloakTest {
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
String wwwAuthHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
assertNotNull(wwwAuthHeader);
assertThat(wwwAuthHeader, CoreMatchers.containsString("Bearer"));
assertThat(wwwAuthHeader, CoreMatchers.containsString("error=\"" + OAuthErrorException.INVALID_TOKEN + "\""));
events.expect(EventType.USER_INFO_REQUEST_ERROR)
.error(Errors.INVALID_TOKEN)
.client((String) null)