KEYCLOAK-5139 refresh token does not work with pairwise subject identifiers
This commit is contained in:
parent
32b16717a7
commit
8cb8678525
4 changed files with 189 additions and 9 deletions
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in a new issue