KEYCLOAK-5139 refresh token does not work with pairwise subject identifiers

This commit is contained in:
Martin Hardselius 2017-07-05 12:32:43 +02:00
parent 32b16717a7
commit 8cb8678525
4 changed files with 189 additions and 9 deletions

View file

@ -120,15 +120,6 @@ public class TokenManager {
} }
public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken, HttpHeaders headers) throws OAuthErrorException { public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, AccessToken oldToken, HttpHeaders headers) throws OAuthErrorException {
UserModel user = session.users().getUserById(oldToken.getSubject(), realm);
if (user == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user");
}
if (!user.isEnabled()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
}
UserSessionModel userSession = null; UserSessionModel userSession = null;
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) { if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
@ -156,6 +147,15 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline user session not found", "Offline user session not found");
} }
UserModel user = userSession.getUser();
if (user == null) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", "Unknown user");
}
if (!user.isEnabled()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
}
ClientModel client = session.getContext().getClient(); ClientModel client = session.getContext().getClient();
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId()); AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());

View file

@ -28,6 +28,8 @@ import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException; import org.keycloak.client.registration.HttpErrorException;
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.IDToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.UserInfo; import org.keycloak.representations.UserInfo;
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation; import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
import org.keycloak.representations.idm.ClientInitialAccessPresentation; import org.keycloak.representations.idm.ClientInitialAccessPresentation;
@ -41,6 +43,7 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou
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.UserInfoClientUtil; import org.keycloak.testsuite.util.UserInfoClientUtil;
import org.keycloak.testsuite.util.UserManager;
import javax.ws.rs.client.Client; import javax.ws.rs.client.Client;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -49,6 +52,8 @@ import java.util.Base64;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrationTest { public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrationTest {
@ -77,6 +82,14 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati
return response; return response;
} }
public OIDCClientRepresentation createPairwise() throws ClientRegistrationException {
// Create pairwise client
OIDCClientRepresentation clientRep = createRep();
clientRep.setSubjectType("pairwise");
OIDCClientRepresentation pairwiseClient = reg.oidc().create(clientRep);
return pairwiseClient;
}
private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) { private void assertCreateFail(OIDCClientRepresentation client, int expectedStatusCode, String expectedErrorContains) {
try { try {
@ -351,6 +364,109 @@ public class OIDCPairwiseClientRegistrationTest extends AbstractClientRegistrati
} }
} }
@Test
public void refreshPairwiseToken() throws Exception {
// Create pairwise client
OIDCClientRepresentation pairwiseClient = createPairwise();
// Login to pairwise client
OAuthClient.AccessTokenResponse accessTokenResponse = login(pairwiseClient, "test-user@localhost", "password");
// Verify tokens
oauth.verifyRefreshToken(accessTokenResponse.getAccessToken());
IDToken idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken());
oauth.verifyRefreshToken(accessTokenResponse.getRefreshToken());
// Refresh token
OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret());
// Verify refreshed tokens
oauth.verifyToken(refreshTokenResponse.getAccessToken());
RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshTokenResponse.getRefreshToken());
IDToken refreshedIdToken = oauth.verifyIDToken(refreshTokenResponse.getIdToken());
// If an ID Token is returned as a result of a token refresh request, the following requirements apply:
// its iss Claim Value MUST be the same as in the ID Token issued when the original authentication occurred
Assert.assertEquals(idToken.getIssuer(), refreshedRefreshToken.getIssuer());
// its sub Claim Value MUST be the same as in the ID Token issued when the original authentication occurred
Assert.assertEquals(idToken.getSubject(), refreshedRefreshToken.getSubject());
// its iat Claim MUST represent the time that the new ID Token is issued
Assert.assertEquals(refreshedIdToken.getIssuedAt(), refreshedRefreshToken.getIssuedAt());
// its aud Claim Value MUST be the same as in the ID Token issued when the original authentication occurred
Assert.assertArrayEquals(idToken.getAudience(), refreshedRefreshToken.getAudience());
// if the ID Token contains an auth_time Claim, its value MUST represent the time of the original authentication
// - not the time that the new ID token is issued
Assert.assertEquals(idToken.getAuthTime(), refreshedIdToken.getAuthTime());
// its azp Claim Value MUST be the same as in the ID Token issued when the original authentication occurred; if
// no azp Claim was present in the original ID Token, one MUST NOT be present in the new ID Token
Assert.assertEquals(idToken.getIssuedFor(), refreshedIdToken.getIssuedFor());
}
@Test
public void refreshPairwiseTokenDeletedUser() throws Exception {
String userId = createUser(REALM_NAME, "delete-me@localhost", "password");
// Create pairwise client
OIDCClientRepresentation pairwiseClient = createPairwise();
// Login to pairwise client
oauth.clientId(pairwiseClient.getClientId());
oauth.clientId(pairwiseClient.getClientId());
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("delete-me@localhost", "password");
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret());
assertEquals(200, accessTokenResponse.getStatusCode());
// Delete user
adminClient.realm(REALM_NAME).users().delete(userId);
OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret());
assertEquals(400, refreshTokenResponse.getStatusCode());
assertEquals("invalid_grant", refreshTokenResponse.getError());
assertNull(refreshTokenResponse.getAccessToken());
assertNull(refreshTokenResponse.getIdToken());
assertNull(refreshTokenResponse.getRefreshToken());
}
@Test
public void refreshPairwiseTokenDisabledUser() throws Exception {
createUser(REALM_NAME, "disable-me@localhost", "password");
// Create pairwise client
OIDCClientRepresentation pairwiseClient = createPairwise();
// Login to pairwise client
oauth.clientId(pairwiseClient.getClientId());
oauth.clientId(pairwiseClient.getClientId());
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin("disable-me@localhost", "password");
OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(loginResponse.getCode(), pairwiseClient.getClientSecret());
assertEquals(200, accessTokenResponse.getStatusCode());
try {
UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(false);
OAuthClient.AccessTokenResponse refreshTokenResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken(), pairwiseClient.getClientSecret());
assertEquals(400, refreshTokenResponse.getStatusCode());
assertEquals("invalid_grant", refreshTokenResponse.getError());
assertNull(refreshTokenResponse.getAccessToken());
assertNull(refreshTokenResponse.getIdToken());
assertNull(refreshTokenResponse.getRefreshToken());
} finally {
UserManager.realm(adminClient.realm(REALM_NAME)).username("disable-me@localhost").enabled(true);
}
}
private OAuthClient.AccessTokenResponse login(OIDCClientRepresentation client, String username, String password) {
oauth.clientId(client.getClientId());
OAuthClient.AuthorizationEndpointResponse loginResponse = oauth.doLogin(username, password);
return oauth.doAccessTokenRequest(loginResponse.getCode(), client.getClientSecret());
}
private String getPayload(String token) { private String getPayload(String token) {
String payloadBase64 = token.split("\\.")[1]; String payloadBase64 = token.split("\\.")[1];
return new String(Base64.getDecoder().decode(payloadBase64)); return new String(Base64.getDecoder().decode(payloadBase64));

View file

@ -27,15 +27,18 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
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.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.RealmManager; import org.keycloak.testsuite.util.RealmManager;
import org.keycloak.testsuite.util.UserManager;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
import javax.ws.rs.client.Client; import javax.ws.rs.client.Client;
@ -488,6 +491,61 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
} }
@Test
public void refreshTokenUserDisabled() throws Exception {
oauth.doLogin("test-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
String refreshTokenString = response.getRefreshToken();
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
events.expectCodeToToken(codeId, sessionId).assertEvent();
try {
UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(false);
response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(refreshToken.getId(), sessionId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
} finally {
UserManager.realm(adminClient.realm("test")).username("test-user@localhost").enabled(true);
}
}
@Test
public void refreshTokenUserDeleted() throws Exception {
String userId = createUser("test", "temp-user@localhost", "password");
oauth.doLogin("temp-user@localhost", "password");
EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
String refreshTokenString = response.getRefreshToken();
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
events.expectCodeToToken(codeId, sessionId).user(userId).assertEvent();
UserManager.realm(adminClient.realm("test")).username("temp-user@localhost").enabled(false);
response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
assertEquals(400, response.getStatusCode());
assertEquals("invalid_grant", response.getError());
events.expectRefresh(refreshToken.getId(), sessionId).user(userId).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
}
protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) { protected Response executeRefreshToken(WebTarget refreshTarget, String refreshToken) {
String header = BasicAuthHelper.createHeader("test-app", "password"); String header = BasicAuthHelper.createHeader("test-app", "password");
Form form = new Form(); Form form = new Form();

View file

@ -61,6 +61,12 @@ public class UserManager {
userResource.update(user); userResource.update(user);
} }
public void enabled(Boolean enabled) {
UserRepresentation user = userResource.toRepresentation();
user.setEnabled(enabled);
userResource.update(user);
}
private UserRepresentation initializeRequiredActions() { private UserRepresentation initializeRequiredActions() {
UserRepresentation user = userResource.toRepresentation(); UserRepresentation user = userResource.toRepresentation();