Offline tokens created in Keycloak 14 or earlier will not work on Keycloak 25
closes #31224 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
fab9028caa
commit
1864cf1827
3 changed files with 136 additions and 2 deletions
|
@ -140,7 +140,7 @@ public class IDToken extends JsonWebToken {
|
|||
// Financial API - Part 2: Read and Write API Security Profile
|
||||
// http://openid.net/specs/openid-financial-api-part-2.html#authorization-server
|
||||
@JsonProperty(S_HASH)
|
||||
protected String stateHash;
|
||||
protected String stateHash;
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
|
@ -172,7 +172,7 @@ public class IDToken extends JsonWebToken {
|
|||
@Deprecated
|
||||
@JsonIgnore
|
||||
public String getSessionState() {
|
||||
return sessionId;
|
||||
return getSessionId();
|
||||
}
|
||||
|
||||
public String getAccessTokenHash() {
|
||||
|
|
|
@ -53,4 +53,11 @@ public class RefreshToken extends AccessToken {
|
|||
public TokenCategory getCategory() {
|
||||
return TokenCategory.INTERNAL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionId() {
|
||||
String sessionId = super.getSessionId();
|
||||
// Fallback as offline tokens created in Keycloak 14 or earlier have only the "session_state" claim, but not "sid"
|
||||
return sessionId != null ? sessionId : (String) getOtherClaims().get(IDToken.SESSION_STATE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.oauth;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.crypto.SignatureProvider;
|
||||
import org.keycloak.crypto.SignatureSignerContext;
|
||||
import org.keycloak.jose.jws.JWSBuilder;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.runonserver.FetchOnServer;
|
||||
import org.keycloak.testsuite.runonserver.FetchOnServerWrapper;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* Test for simulating token refresh with the offline tokens created in older Keycloak versions.
|
||||
*
|
||||
* Keycloak supports refresh of the offline tokens, which were created in older Keycloak versions than current Keycloak version. But
|
||||
* testing real migration is sometimes hard to achieve as it requires running of the old Keycloak server, which is sometimes not feasible.
|
||||
*
|
||||
* This test just simulates the refresh with the old offline-token by manually converting offline-token to the offline-token format, which was used by the specified old Keycloak version
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OfflineTokenMigrationTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Page
|
||||
protected LoginPage loginPage;
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
|
||||
}
|
||||
|
||||
// Issue 31224
|
||||
// Test refresh with the offline-token created in Keycloak 14 works
|
||||
@Test
|
||||
public void testOfflineTokenMigrationFromKeycloak14() throws Exception {
|
||||
OfflineTokenConverter convertOfflineTokenToKeycloak14Format = (session, oldOfflineToken) -> {
|
||||
try {
|
||||
RefreshToken refreshToken = session.tokens().decode(oldOfflineToken, RefreshToken.class);
|
||||
String sessionId = refreshToken.getSessionId();
|
||||
String signatureAlgorithm = new JWSInput(oldOfflineToken).getHeader().getAlgorithm().toString();
|
||||
|
||||
Map<String, String> asMap = JsonSerialization.readValue(JsonSerialization.writeValueAsString(refreshToken), Map.class);
|
||||
asMap.remove(IDToken.SESSION_ID);
|
||||
asMap.put(IDToken.SESSION_STATE, sessionId);
|
||||
|
||||
SignatureProvider signatureProvider = session.getProvider(SignatureProvider.class, signatureAlgorithm);
|
||||
SignatureSignerContext signer = signatureProvider.signer();
|
||||
|
||||
String type = "JWT";
|
||||
return new JWSBuilder().type(type).jsonContent(asMap).sign(signer);
|
||||
} catch (Exception ioe) {
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
};
|
||||
|
||||
testOfflineTokenMigration(convertOfflineTokenToKeycloak14Format);
|
||||
}
|
||||
|
||||
private void testOfflineTokenMigration(OfflineTokenConverter offlineTokenConverter) throws Exception {
|
||||
// Send request to obtain offline token
|
||||
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
|
||||
oauth.clientId("direct-grant");
|
||||
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
|
||||
Assert.assertNull(tokenResponse.getErrorDescription());
|
||||
String offlineTokenString = tokenResponse.getRefreshToken();
|
||||
|
||||
// Convert offline token to the format of some old Keycloak version
|
||||
FetchOnServerWrapper<String> fetch = new FetchOnServerWrapper<>() {
|
||||
|
||||
@Override
|
||||
public FetchOnServer getRunOnServer() {
|
||||
return session -> offlineTokenConverter.apply(session, offlineTokenString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<String> getResultClass() {
|
||||
return String.class;
|
||||
}
|
||||
|
||||
};
|
||||
String modifiedOfflineToken = testingClient.server("test").fetch(fetch);
|
||||
getLogger().infof("Modified offline token: %s", modifiedOfflineToken);
|
||||
|
||||
// Check it is possible to successfully refresh with the modified offline token
|
||||
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(modifiedOfflineToken, "password");
|
||||
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
|
||||
Assert.assertEquals(200, response.getStatusCode());
|
||||
}
|
||||
|
||||
public interface OfflineTokenConverter extends Serializable, BiFunction<KeycloakSession, String, String> {
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue