KEYCLOAK-16800 userinfo fails with 500 Internal Server Error for service account token

This commit is contained in:
rmartinc 2021-02-04 14:18:18 +01:00 committed by Marek Posolda
parent bfaab76b5f
commit 056b52fbbe
4 changed files with 78 additions and 9 deletions

View file

@ -24,6 +24,7 @@ import org.keycloak.TokenCategory;
import org.keycloak.TokenVerifier; import org.keycloak.TokenVerifier;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.crypto.SignatureProvider; import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureSignerContext; import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext; import org.keycloak.crypto.SignatureVerifierContext;
@ -35,13 +36,15 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext; import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager.NotBeforeCheck;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.TokenManager.NotBeforeCheck;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.services.CorsErrorResponseException; import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
@ -53,6 +56,8 @@ import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.Cors;
import org.keycloak.services.util.DefaultClientSessionContext; import org.keycloak.services.util.DefaultClientSessionContext;
import org.keycloak.services.util.MtlsHoKTokenUtil; import org.keycloak.services.util.MtlsHoKTokenUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.utils.MediaType; import org.keycloak.utils.MediaType;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@ -278,8 +283,30 @@ public class UserInfoEndpoint {
return cors.builder(responseBuilder).build(); return cors.builder(responseBuilder).build();
} }
private UserSessionModel createTransientSessionForClient(AccessToken token, ClientModel client) {
// create a transient session
UserModel user = TokenManager.lookupUserFromStatelessToken(session, realm, token);
if (user == null) {
throw newUnauthorizedErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found");
}
UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, user, user.getUsername(), clientConnection.getRemoteAddr(),
ServiceAccountConstants.CLIENT_AUTH, false, null, null, UserSessionModel.SessionPersistenceState.TRANSIENT);
// attach an auth session for the client
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
authSession.setAuthenticatedUser(userSession.getUser());
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
AuthenticationManager.setClientScopesInSession(authSession);
TokenManager.attachAuthenticationSession(session, userSession, authSession);
return userSession;
}
private UserSessionModel findValidSession(AccessToken token, EventBuilder event, ClientModel client) { private UserSessionModel findValidSession(AccessToken token, EventBuilder event, ClientModel client) {
if (token.getSessionState() == null) {
return createTransientSessionForClient(token, client);
}
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId()); UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
UserSessionModel offlineUserSession = null; UserSessionModel offlineUserSession = null;
if (AuthenticationManager.isSessionValid(realm, userSession)) { if (AuthenticationManager.isSessionValid(realm, userSession)) {

View file

@ -66,6 +66,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken; import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.runonserver.RunOnServerException; import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
@ -864,6 +865,18 @@ public class OAuthClient {
} }
} }
public UserInfo doUserInfoRequest(String accessToken) {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
HttpGet get = new HttpGet(getUserInfoUrl());
get.setHeader("Authorization", "Bearer " + accessToken);
try (CloseableHttpResponse response = client.execute(get)) {
return JsonSerialization.readValue(response.getEntity().getContent(), UserInfo.class);
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
public void closeClient(CloseableHttpClient client) { public void closeClient(CloseableHttpClient client) {
try { try {
client.close(); client.close();
@ -1118,6 +1131,11 @@ public class OAuthClient {
return b.build(realm).toString(); return b.build(realm).toString();
} }
public String getUserInfoUrl() {
UriBuilder b = OIDCLoginProtocolService.userInfoUrl(UriBuilder.fromUri(baseUrl));
return b.build(realm).toString();
}
public OAuthClient baseUrl(String baseUrl) { public OAuthClient baseUrl(String baseUrl) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
return this; return this;

View file

@ -21,13 +21,11 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
@ -42,6 +40,7 @@ import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper; import org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@ -50,24 +49,22 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@ -489,4 +486,31 @@ public class ServiceAccountTest extends AbstractKeycloakTest {
events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("service-account-cl-refresh-on").assertEvent(); events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("service-account-cl-refresh-on").assertEvent();
} }
@Test
public void userInfoForServiceAccountWithoutRefreshTokenImpl() throws Exception {
oauth.clientId("service-account-cl");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
assertEquals(200, response.getStatusCode());
assertNull(response.getRefreshToken());
UserInfo info = oauth.doUserInfoRequest(response.getAccessToken());
assertEquals(200, response.getStatusCode());
assertEquals("service-account-service-account-cl", info.getPreferredUsername());
}
@Test
public void userInfoForServiceAccountWithRefreshTokenImpl() throws Exception {
oauth.clientId("service-account-cl-refresh-on");
OAuthClient.AccessTokenResponse response = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
assertEquals(200, response.getStatusCode());
assertNotNull(response.getRefreshToken());
UserInfo info = oauth.doUserInfoRequest(response.getAccessToken());
assertEquals(200, response.getStatusCode());
assertEquals("service-account-service-account-cl-refresh-on", info.getPreferredUsername());
HttpResponse logoutResponse = oauth.doLogout(response.getRefreshToken(), "secret1");
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());
}
} }

View file

@ -73,10 +73,10 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import java.net.URI; import java.net.URI;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
@ -734,8 +734,8 @@ public class UserInfoTest extends AbstractKeycloakTest {
} }
private void testRolesInUserInfoResponse(UserInfo userInfo) { private void testRolesInUserInfoResponse(UserInfo userInfo) {
Map<String, Set<String>> realmAccess = (Map<String, Set<String>>) userInfo.getOtherClaims().get("realm_access"); Map<String, Collection<String>> realmAccess = (Map<String, Collection<String>>) userInfo.getOtherClaims().get("realm_access");
Map<String, Map<String, Set<String>>> resourceAccess = (Map<String, Map<String, Set<String>>>) userInfo.getOtherClaims().get("resource_access"); Map<String, Map<String, Collection<String>>> resourceAccess = (Map<String, Map<String, Collection<String>>>) userInfo.getOtherClaims().get("resource_access");
org.hamcrest.MatcherAssert.assertThat(realmAccess.get("roles"), CoreMatchers.hasItems("offline_access", "user")); org.hamcrest.MatcherAssert.assertThat(realmAccess.get("roles"), CoreMatchers.hasItems("offline_access", "user"));
org.hamcrest.MatcherAssert.assertThat(resourceAccess.get("test-app").get("roles"), CoreMatchers.hasItems("customer-user")); org.hamcrest.MatcherAssert.assertThat(resourceAccess.get("test-app").get("roles"), CoreMatchers.hasItems("customer-user"));