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
|
@ -172,7 +172,7 @@ public class IDToken extends JsonWebToken {
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public String getSessionState() {
|
public String getSessionState() {
|
||||||
return sessionId;
|
return getSessionId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAccessTokenHash() {
|
public String getAccessTokenHash() {
|
||||||
|
|
|
@ -53,4 +53,11 @@ public class RefreshToken extends AccessToken {
|
||||||
public TokenCategory getCategory() {
|
public TokenCategory getCategory() {
|
||||||
return TokenCategory.INTERNAL;
|
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