Move verify and parse tokens to AbstractOAuthClient (#37663)

Closes #37660

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2025-03-03 08:51:41 +01:00 committed by GitHub
parent 69721ba1b5
commit 83ef1e3de0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 154 additions and 111 deletions

View file

@ -2,6 +2,7 @@ package org.keycloak.test.examples;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.representations.AccessToken;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectUser;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
@ -85,6 +86,22 @@ public class OAuthClientTest {
Assertions.assertFalse(refreshResponse.isSuccess());
}
@Test
public void testParseToken() {
AccessTokenResponse accessTokenResponse = oauth.doPasswordGrantRequest(user.getUsername(), user.getPassword());
AccessToken accessToken = oauth.parseToken(accessTokenResponse.getAccessToken(), AccessToken.class);
Assertions.assertEquals(user.getUsername(), accessToken.getPreferredUsername());
}
@Test
public void testVerifyToken() {
AccessTokenResponse accessTokenResponse = oauth.doPasswordGrantRequest(user.getUsername(), user.getPassword());
AccessToken accessToken = oauth.verifyToken(accessTokenResponse.getAccessToken(), AccessToken.class);
Assertions.assertEquals(user.getUsername(), accessToken.getPreferredUsername());
}
public static class OAuthUserConfig implements UserConfig {
@Override

View file

@ -1,6 +1,11 @@
package org.keycloak.testsuite.util.oauth;
import org.apache.http.impl.client.CloseableHttpClient;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AuthorizationResponseToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken;
import org.openqa.selenium.WebDriver;
import java.util.Map;
@ -28,6 +33,8 @@ public abstract class AbstractOAuthClient<T> {
protected StateParamProvider state;
protected String nonce;
private final KeyManager keyManager = new KeyManager(this);
private final TokensManager tokensManager = new TokensManager(keyManager);
protected HttpClientManager httpClientManager;
protected WebDriver driver;
@ -123,6 +130,30 @@ public abstract class AbstractOAuthClient<T> {
return tokenRevocationRequest(token).send();
}
public <J extends JsonWebToken> J parseToken(String token, Class<J> clazz) {
return tokensManager.parseToken(token, clazz);
}
public RefreshToken parseRefreshToken(String refreshToken) {
return tokensManager.parseToken(refreshToken, RefreshToken.class);
}
public AccessToken verifyToken(String token) {
return tokensManager.verifyToken(token, AccessToken.class);
}
public IDToken verifyIDToken(String token) {
return tokensManager.verifyToken(token, IDToken.class);
}
public AuthorizationResponseToken verifyAuthorizationResponseToken(String token) {
return tokensManager.verifyToken(token, AuthorizationResponseToken.class);
}
public <J extends JsonWebToken> J verifyToken(String token, Class<J> clazz) {
return tokensManager.verifyToken(token, clazz);
}
public T baseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return (T) this;
@ -141,10 +172,18 @@ public abstract class AbstractOAuthClient<T> {
return httpClientManager;
}
public KeyManager keys() {
return keyManager;
}
public Endpoints getEndpoints() {
return new Endpoints(baseUrl, config.getRealm());
}
public String getRealm() {
return config.getRealm();
}
public String getRedirectUri() {
return config.getRedirectUri();
}

View file

@ -7,9 +7,9 @@ import java.io.IOException;
public class JwksRequest {
private final OAuthClient client;
private final AbstractOAuthClient<?> client;
public JwksRequest(OAuthClient client) {
JwksRequest(AbstractOAuthClient<?> client) {
this.client = client;
}

View file

@ -9,7 +9,7 @@ public class JwksResponse extends AbstractHttpResponse {
private JSONWebKeySet jwks;
public JwksResponse(CloseableHttpResponse response) throws IOException {
JwksResponse(CloseableHttpResponse response) throws IOException {
super(response);
}

View file

@ -14,10 +14,10 @@ import java.util.Map;
public class KeyManager {
private final OAuthClient client;
private final AbstractOAuthClient<?> client;
private final Map<String, JSONWebKeySet> publicKeys = new HashMap<>();
public KeyManager(OAuthClient client) {
KeyManager(AbstractOAuthClient<?> client) {
this.client = client;
}

View file

@ -0,0 +1,52 @@
package org.keycloak.testsuite.util.oauth;
import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.ServerECDSASignatureVerifierContext;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.representations.JsonWebToken;
public class TokensManager {
private final KeyManager keyManager;
TokensManager(KeyManager keyManager) {
this.keyManager = keyManager;
}
public <T extends JsonWebToken> T verifyToken(String token, Class<T> clazz) {
try {
TokenVerifier<T> verifier = TokenVerifier.create(token, clazz);
String kid = verifier.getHeader().getKeyId();
String algorithm = verifier.getHeader().getAlgorithm().name();
KeyWrapper key = keyManager.getPublicKey(algorithm, kid);
AsymmetricSignatureVerifierContext verifierContext;
switch (algorithm) {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
verifierContext = new ServerECDSASignatureVerifierContext(key);
break;
default:
verifierContext = new AsymmetricSignatureVerifierContext(key);
}
verifier.verifierContext(verifierContext);
verifier.verify();
return verifier.getToken();
} catch (VerificationException e) {
throw new RuntimeException("Failed to decode token", e);
}
}
public <T extends JsonWebToken> T parseToken(String token, Class<T> clazz) {
try {
return new JWSInput(token).readJsonContent(clazz);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,35 @@
package org.keycloak.testsuite.util;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.ServerECDSASignatureSignerContext;
import org.keycloak.crypto.SignatureSignerContext;
import java.security.PrivateKey;
public class SignatureSignerUtil {
public static SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm) {
return createSigner(privateKey, kid, algorithm, null);
}
public static SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm, String curve) {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(algorithm);
keyWrapper.setKid(kid);
keyWrapper.setPrivateKey(privateKey);
keyWrapper.setCurve(curve);
SignatureSignerContext signer;
switch (algorithm) {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
signer = new ServerECDSASignatureSignerContext(keyWrapper);
break;
default:
signer = new AsymmetricSignatureSignerContext(keyWrapper);
}
return signer;
}
}

View file

@ -27,32 +27,16 @@ import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.VerificationException;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.AsymmetricSignatureSignerContext;
import org.keycloak.crypto.AsymmetricSignatureVerifierContext;
import org.keycloak.crypto.KeyWrapper;
import org.keycloak.crypto.ServerECDSASignatureSignerContext;
import org.keycloak.crypto.ServerECDSASignatureVerifierContext;
import org.keycloak.crypto.SignatureSignerContext;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.grants.ciba.channel.AuthenticationChannelResponse;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AuthorizationResponseToken;
import org.keycloak.representations.ClaimsRepresentation;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.testsuite.util.DroneUtils;
import org.keycloak.testsuite.util.WaitUtils;
import org.keycloak.util.BasicAuthHelper;
@ -62,13 +46,10 @@ import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.PageFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@ -84,17 +65,11 @@ import static org.keycloak.testsuite.util.UIUtils.clickLink;
*/
public class OAuthClient extends AbstractOAuthClient<OAuthClient> {
private static final Logger logger = LoggerFactory.getLogger(OAuthClient.class);
public static String SERVER_ROOT;
public static String AUTH_SERVER_ROOT;
public static String APP_ROOT;
public static String APP_AUTH_ROOT;
static {
updateURLs(getAuthServerContextRoot());
}
@ -115,7 +90,6 @@ public class OAuthClient extends AbstractOAuthClient<OAuthClient> {
updateAppRootRealm("master");
}
private KeyManager keyManager = new KeyManager(this);
private String idTokenHint;
@ -530,74 +504,6 @@ public class OAuthClient extends AbstractOAuthClient<OAuthClient> {
}
}
// TODO Extract into separate util to verify tokens
public AccessToken verifyToken(String token) {
return verifyToken(token, AccessToken.class);
}
public IDToken verifyIDToken(String token) {
return verifyToken(token, IDToken.class);
}
public AuthorizationResponseToken verifyAuthorizationResponseToken(String token) {
return verifyToken(token, AuthorizationResponseToken.class);
}
public RefreshToken parseRefreshToken(String refreshToken) {
try {
return new JWSInput(refreshToken).readJsonContent(RefreshToken.class);
} catch (Exception e) {
throw new RunOnServerException(e);
}
}
public <T extends JsonWebToken> T verifyToken(String token, Class<T> clazz) {
try {
TokenVerifier<T> verifier = TokenVerifier.create(token, clazz);
String kid = verifier.getHeader().getKeyId();
String algorithm = verifier.getHeader().getAlgorithm().name();
KeyWrapper key = keyManager.getPublicKey(algorithm, kid);
AsymmetricSignatureVerifierContext verifierContext;
switch (algorithm) {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
verifierContext = new ServerECDSASignatureVerifierContext(key);
break;
default:
verifierContext = new AsymmetricSignatureVerifierContext(key);
}
verifier.verifierContext(verifierContext);
verifier.verify();
return verifier.getToken();
} catch (VerificationException e) {
throw new RuntimeException("Failed to decode token", e);
}
}
public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm) {
return createSigner(privateKey, kid, algorithm, null);
}
public SignatureSignerContext createSigner(PrivateKey privateKey, String kid, String algorithm, String curve) {
KeyWrapper keyWrapper = new KeyWrapper();
keyWrapper.setAlgorithm(algorithm);
keyWrapper.setKid(kid);
keyWrapper.setPrivateKey(privateKey);
keyWrapper.setCurve(curve);
SignatureSignerContext signer;
switch (algorithm) {
case Algorithm.ES256:
case Algorithm.ES384:
case Algorithm.ES512:
signer = new ServerECDSASignatureSignerContext(keyWrapper);
break;
default:
signer = new AsymmetricSignatureSignerContext(keyWrapper);
}
return signer;
}
public String getClientId() {
return config.getClientId();
}
@ -739,15 +645,6 @@ public class OAuthClient extends AbstractOAuthClient<OAuthClient> {
return this;
}
public KeyManager keys() {
return keyManager;
}
public String getRealm() {
return config.getRealm();
}
public OAuthClient codeVerifier(String codeVerifier) {
this.codeVerifier = codeVerifier;
return this;

View file

@ -175,6 +175,7 @@ import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientPolicyBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfileBuilder;
import org.keycloak.testsuite.util.ClientPoliciesUtil.ClientProfilesBuilder;
import org.keycloak.testsuite.util.MutualTLSUtils;
import org.keycloak.testsuite.util.SignatureSignerUtil;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
import org.keycloak.testsuite.util.ServerURLs;
@ -494,7 +495,7 @@ public abstract class AbstractClientPoliciesTest extends AbstractKeycloakTest {
protected String createSignedRequestToken(String clientId, PrivateKey privateKey, PublicKey publicKey, String algorithm) {
JsonWebToken jwt = createRequestToken(clientId, getRealmInfoUrl());
String kid = KeyUtils.createKeyId(publicKey);
SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm);
SignatureSignerContext signer = SignatureSignerUtil.createSigner(privateKey, kid, algorithm);
return new JWSBuilder().kid(kid).jsonContent(jwt).sign(signer);
}

View file

@ -117,6 +117,7 @@ import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResou
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.KeystoreUtils;
import org.keycloak.testsuite.util.SignatureSignerUtil;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.testsuite.util.oauth.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
@ -925,7 +926,7 @@ public abstract class AbstractClientAuthSignedJWTTest extends AbstractKeycloakTe
if (kid == null) {
kid = KeyUtils.createKeyId(publicKey);
}
SignatureSignerContext signer = oauth.createSigner(privateKey, kid, algorithm, curve);
SignatureSignerContext signer = SignatureSignerUtil.createSigner(privateKey, kid, algorithm, curve);
String ret = new JWSBuilder().kid(kid).jsonContent(jwt).sign(signer);
return ret;
}

View file

@ -45,6 +45,7 @@ import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.KeystoreUtils;
import org.keycloak.testsuite.util.SignatureSignerUtil;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import java.security.KeyPair;
@ -619,7 +620,7 @@ public class ClientAuthSignedJWTTest extends AbstractClientAuthSignedJWTTest {
PrivateKey privateKey = keyPair.getPrivate();
JsonWebToken assertion = createRequestToken(app2.getClientId(), getRealmInfoUrl());
SignatureSignerContext signer = oauth.createSigner(privateKey, null, Algorithm.ES512);
SignatureSignerContext signer = SignatureSignerUtil.createSigner(privateKey, null, Algorithm.ES512);
String jws = new JWSBuilder().jsonContent(assertion).sign(signer);
List<NameValuePair> parameters = new LinkedList<>();