Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private AccessToken transformAccessToken(AccessToken token) {
return token;
}
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session);
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, token.getScope(), session);
AccessToken smallToken = getAccessTokenFromStoredData(token, userSession);
return tokenManager.transformIntrospectionAccessToken(session, smallToken, userSession, clientSessionCtx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public TokenValidation(UserModel user, UserSessionModel userSession, ClientSessi
}

public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm,
RefreshToken oldToken, HttpHeaders headers) throws OAuthErrorException {
RefreshToken oldToken, HttpHeaders headers, String oldTokenScope) throws OAuthErrorException {
UserSessionModel userSession = null;
boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType());

Expand Down Expand Up @@ -215,9 +215,6 @@ public TokenValidation validateToken(KeycloakSession session, UriInfo uriInfo, C
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
}

// Setup clientScopes from refresh token to the context
String oldTokenScope = oldToken.getScope();

// Case when offline token is migrated from previous version
if (oldTokenScope == null && userSession.isOffline()) {
logger.debugf("Migrating offline token of user '%s' for client '%s' of realm '%s'", user.getUsername(), client.getClientId(), realm.getName());
Expand Down Expand Up @@ -379,14 +376,24 @@ public static UserModel lookupUserFromStatelessToken(KeycloakSession session, Re


public AccessTokenResponseBuilder refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient,
String encodedRefreshToken, EventBuilder event, HttpHeaders headers, HttpRequest request) throws OAuthErrorException {
String encodedRefreshToken, EventBuilder event, HttpHeaders headers, HttpRequest request, String scopeParameter) throws OAuthErrorException {
RefreshToken refreshToken = verifyRefreshToken(session, realm, authorizedClient, request, encodedRefreshToken, true);

event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
// Setup clientScopes from refresh token to the context
String oldTokenScope = refreshToken.getScope();
//The requested scope MUST NOT include any scope not originally granted by the resource owner
//if scope parameter is not null, remove every scope that is not part of scope parameter
if (scopeParameter != null && ! scopeParameter.isEmpty()) {
Set<String> scopeParamScopes = Arrays.stream(scopeParameter.split(" ")).collect(Collectors.toSet());
oldTokenScope = Arrays.stream(oldTokenScope.split(" ")).filter(sc -> scopeParamScopes.contains(sc))
.collect(Collectors.joining(" "));
}

TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers);

TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers, oldTokenScope);
AuthenticatedClientSessionModel clientSession = validation.clientSessionCtx.getClientSession();
OIDCAdvancedConfigWrapper clientConfig = OIDCAdvancedConfigWrapper.fromClientModel(authorizedClient);

Expand All @@ -408,7 +415,8 @@ public AccessTokenResponseBuilder refreshAccessToken(KeycloakSession session, Ur
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session,
validation.userSession, validation.clientSessionCtx).accessToken(validation.newToken);
if (clientConfig.isUseRefreshToken()) {
responseBuilder.generateRefreshToken();
//refresh token must have same scope as old refresh token (type, scope, expiration)
responseBuilder.generateRefreshToken(refreshToken.getScope());
}

if (validation.newToken.getAuthorization() != null
Expand Down Expand Up @@ -1093,6 +1101,22 @@ public AccessTokenResponseBuilder generateRefreshToken() {

ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(realm, OAuth2Constants.OFFLINE_ACCESS);
boolean offlineTokenRequested = offlineAccessScope==null ? false : clientSessionCtx.getClientScopeIds().contains(offlineAccessScope.getId());
generateRefreshToken(offlineTokenRequested);
return this;
}

public AccessTokenResponseBuilder generateRefreshToken(String scope) {
if (accessToken == null) {
throw new IllegalStateException("accessToken not set");
}

boolean offlineTokenRequested = Arrays.asList(scope.split(" ")).contains(OAuth2Constants.OFFLINE_ACCESS) ;
generateRefreshToken(offlineTokenRequested);
refreshToken.setScope(scope);
return this;
}

private void generateRefreshToken(boolean offlineTokenRequested) {
if (offlineTokenRequested) {
UserSessionManager sessionManager = new UserSessionManager(session);
if (!sessionManager.isOfflineTokenAllowed(clientSessionCtx)) {
Expand All @@ -1111,7 +1135,6 @@ public AccessTokenResponseBuilder generateRefreshToken() {
}
refreshToken.id(KeycloakModelUtils.generateId());
refreshToken.issuedNow();
return this;
}

private int getExpiration(boolean offline) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,8 @@ public Response refreshTokenGrant() {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
}

String scopeParameter = getRequestedScopes();

try {
session.clientPolicy().triggerOnEvent(new TokenRefreshContext(formParams));
refreshToken = formParams.getFirst(OAuth2Constants.REFRESH_TOKEN);
Expand All @@ -562,7 +564,7 @@ public Response refreshTokenGrant() {
AccessTokenResponse res;
try {
// KEYCLOAK-6771 Certificate Bound Token
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager.refreshAccessToken(session, session.getContext().getUri(), clientConnection, realm, client, refreshToken, event, headers, request, scopeParameter);

checkAndBindMtlsHoKToken(responseBuilder, clientConfig.isUseRefreshToken());
checkAndBindDPoPToken(responseBuilder, clientConfig.isUseRefreshToken() && (client.isPublicClient() || client.isBearerOnly()), Profile.isFeatureEnabled(Profile.Feature.DPOP));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ private Response issueUserInfo() {
// Existence of authenticatedClientSession for our client already handled before
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId());

// Retrieve by latest scope parameter
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionScopeParameter(clientSession, session);
// Retrieve by access token scope parameter
ClientSessionContext clientSessionCtx = DefaultClientSessionContext.fromClientSessionAndScopeParameter(clientSession, token.getScope(), session);

AccessToken userInfo = new AccessToken();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,9 @@ public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String pas
if (refreshToken != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.REFRESH_TOKEN, refreshToken));
}
if (scope != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
}
if (clientId != null && password != null) {
String authorization = BasicAuthHelper.createHeader(clientId, password);
post.setHeader("Authorization", authorization);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public void offlineTokenBrowserFlow() throws Exception {

assertTrue(tokenResponse.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));

String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
String newRefreshTokenString = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId, false);

// Change offset to very big value to ensure offline session expires
setTimeOffset(3000000);
Expand All @@ -281,7 +281,7 @@ public void offlineTokenBrowserFlow() throws Exception {
}

private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
final String sessionId, String userId) {
final String sessionId, String userId, boolean scopeParameterExist) {
// Change offset to big value to ensure userSession expired
setTimeOffset(99999);
assertFalse(oldToken.isActive());
Expand All @@ -301,11 +301,15 @@ private String testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken of

// Assert new refreshToken in the response
String newRefreshToken = response.getRefreshToken();
RefreshToken newRefreshTokenFull = oauth.parseRefreshToken(newRefreshToken);
Assert.assertNotNull(newRefreshToken);
Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());

// Assert scope parameter contains "offline_access"
assertTrue(response.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
// scope parameter contains "offline_access" if not filter via scope parameter
assertTrue(scopeParameterExist ? !refreshedToken.getScope().contains(OAuth2Constants.OFFLINE_ACCESS) : refreshedToken.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
// Assert refresh token scope parameter contains "offline_access"
assertTrue(newRefreshTokenFull.getScope().contains(OAuth2Constants.OFFLINE_ACCESS));
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, newRefreshTokenFull.getType());

Assert.assertEquals(userId, refreshedToken.getSubject());

Expand Down Expand Up @@ -354,10 +358,10 @@ public void offlineTokenDirectGrantFlow() throws Exception {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());

testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId, false);

// Assert same token can be refreshed again
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId, false);
}

@Test
Expand Down Expand Up @@ -389,7 +393,7 @@ public void offlineTokenDirectGrantFlowWithRefreshTokensRevoked() throws Excepti
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());

String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
String offlineTokenString2 = testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId, false);
RefreshToken offlineToken2 = oauth.parseRefreshToken(offlineTokenString2);

// Assert second refresh with same refresh token will fail
Expand Down Expand Up @@ -438,7 +442,7 @@ public void offlineTokenServiceAccountFlow() throws Exception {
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());

testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false);

// Now retrieve another offline token and verify that previous offline token is still valid
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
Expand All @@ -458,8 +462,8 @@ public void offlineTokenServiceAccountFlow() throws Exception {
.assertEvent();

// Refresh with both offline tokens is fine
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId, false);
}

@Test
Expand Down Expand Up @@ -632,7 +636,7 @@ public void onlineOfflineTokenLogout() throws Exception {
assertEquals(200, offlineRefresh.getStatusCode());

// logout online session
CloseableHttpResponse logoutResponse = oauth.scope("").doLogout(response.getRefreshToken(), "secret1");
CloseableHttpResponse logoutResponse = oauth.scope(null).doLogout(response.getRefreshToken(), "secret1");
assertEquals(204, logoutResponse.getStatusLine().getStatusCode());

// assert the online session is gone
Expand Down Expand Up @@ -832,6 +836,7 @@ private void conductOfflineTokenRequest(String expectedRefreshAlg, String expect
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, expectedIdTokenAlg);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client"), expectedAccessAlg);
offlineTokenRequest(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg);
offlineTokenRequestWithScopeParameter(expectedRefreshAlg, expectedAccessAlg, expectedIdTokenAlg);
} finally {
TokenSignatureUtil.changeRealmTokenSignatureProvider(adminClient, Algorithm.RS256);
TokenSignatureUtil.changeClientAccessTokenSignatureProvider(ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client"), Algorithm.RS256);
Expand Down Expand Up @@ -973,7 +978,7 @@ private void offlineTokenRequest(String expectedRefreshAlg, String expectedAcces
Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());

testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false);

// Now retrieve another offline token and decode that previous offline token is still valid
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
Expand All @@ -993,11 +998,63 @@ private void offlineTokenRequest(String expectedRefreshAlg, String expectedAcces
.assertEvent();

// Refresh with both offline tokens is fine
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, false);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId, false);

}

private void offlineTokenRequestWithScopeParameter(String expectedRefreshAlg, String expectedAccessAlg, String expectedIdTokenAlg) throws Exception {
ClientScopeRepresentation phoneScope = adminClient.realm("test").clientScopes().findAll().stream().filter((ClientScopeRepresentation clientScope) ->"phone".equals(clientScope.getName())).findFirst().get();
ClientManager.realm(adminClient.realm("test")).clientId(oauth.getClientId()).addClientScope(phoneScope.getId(),false);
oauth.scope(OAuth2Constants.OFFLINE_ACCESS+" phone");
oauth.clientId("offline-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");

JWSHeader header = null;
String idToken = tokenResponse.getIdToken();
String accessToken = tokenResponse.getAccessToken();
String refreshToken = tokenResponse.getRefreshToken();
if (idToken != null) {
header = new JWSInput(idToken).getHeader();
assertEquals(expectedIdTokenAlg, header.getAlgorithm().name());
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
}
if (accessToken != null) {
header = new JWSInput(accessToken).getHeader();
assertEquals(expectedAccessAlg, header.getAlgorithm().name());
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
}
if (refreshToken != null) {
header = new JWSInput(refreshToken).getHeader();
assertEquals(expectedRefreshAlg, header.getAlgorithm().name());
assertEquals("JWT", header.getType());
assertNull(header.getContentType());
}

AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
String offlineTokenString = tokenResponse.getRefreshToken();
RefreshToken offlineToken = oauth.parseRefreshToken(offlineTokenString);

events.expectClientLogin()
.client("offline-client")
.user(serviceAccountUserId)
.session(token.getSessionState())
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent();

Assert.assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());

//refresh token without sending offline_access scope => access token without it and same refresh token
oauth.scope("phone");
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId, true);
}

@Test
public void refreshTokenUserClientMaxLifespanSmallerThanSession() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
Expand Down
Loading