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:
parent
23b794aa51
commit
0219d62f09
2 changed files with 38 additions and 6 deletions
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue