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.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.crypto.SignatureProvider;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.crypto.SignatureVerifierContext;
@ -35,13 +36,15 @@ import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.protocol.oidc.TokenManager.NotBeforeCheck;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
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.services.CorsErrorResponseException;
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.util.DefaultClientSessionContext;
import org.keycloak.services.util.MtlsHoKTokenUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import org.keycloak.utils.MediaType;
import javax.ws.rs.GET;
@ -278,8 +283,30 @@ public class UserInfoEndpoint {
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) {
if (token.getSessionState() == null) {
return createTransientSessionForClient(token, client);
}
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
UserSessionModel offlineUserSession = null;
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.JsonWebToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.runonserver.RunOnServerException;
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) {
try {
client.close();
@ -1118,6 +1131,11 @@ public class OAuthClient {
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) {
this.baseUrl = baseUrl;
return this;

View file

@ -21,13 +21,11 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
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.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
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.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.client.resources.TestApplicationResourceUrls;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TokenSignatureUtil;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.testsuite.util.WaitUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
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();
}
@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 java.net.URI;
import java.security.PublicKey;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -734,8 +734,8 @@ public class UserInfoTest extends AbstractKeycloakTest {
}
private void testRolesInUserInfoResponse(UserInfo userInfo) {
Map<String, Set<String>> realmAccess = (Map<String, Set<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, Collection<String>> realmAccess = (Map<String, Collection<String>>) userInfo.getOtherClaims().get("realm_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(resourceAccess.get("test-app").get("roles"), CoreMatchers.hasItems("customer-user"));