Last active
July 22, 2024 15:52
-
-
Save thomasdarimont/52152ed68486c65b50a04fcf7bd9bbde to your computer and use it in GitHub Desktop.
Retrieve and verify AccessToken with Keycloak Client.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package de.tdlabs.keycloak.client; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import org.keycloak.OAuth2Constants; | |
import org.keycloak.RSATokenVerifier; | |
import org.keycloak.admin.client.Keycloak; | |
import org.keycloak.admin.client.KeycloakBuilder; | |
import org.keycloak.common.VerificationException; | |
import org.keycloak.jose.jws.JWSHeader; | |
import org.keycloak.representations.AccessToken; | |
import org.keycloak.representations.AccessTokenResponse; | |
import org.keycloak.representations.idm.KeysMetadataRepresentation.KeyMetadataRepresentation; | |
import java.math.BigInteger; | |
import java.net.URL; | |
import java.security.KeyFactory; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.PublicKey; | |
import java.security.spec.InvalidKeySpecException; | |
import java.security.spec.RSAPublicKeySpec; | |
import java.security.spec.X509EncodedKeySpec; | |
import java.util.Base64; | |
import java.util.Base64.Decoder; | |
import java.util.List; | |
import java.util.Map; | |
public class KeycloakClientAuthExample { | |
public static void main(String[] args) { | |
KeycloakClientFacade facade = new KeycloakClientFacade( // | |
"http://localhost:8081/auth" // | |
, "admin-client-demo" // | |
, "demo-client-1" // | |
, "da6947c2-6559-4c37-b219-d37bb72ec2fa" // | |
); | |
// Get raw AccessToken string for client credentials grant | |
System.out.println(facade.getAccessTokenString()); | |
// Get decoded AccessToken for client credentials grant | |
// System.out.println(facade.getAccessToken()); | |
// Get decoded AccessToken for password credentials grant | |
AccessToken accessToken = facade.getAccessToken("tester", "test"); | |
System.out.println(accessToken.getSubject()); | |
} | |
static class KeycloakClientFacade { | |
private final String serverUrl; | |
private final String realmId; | |
private final String clientId; | |
private final String clientSecret; | |
public KeycloakClientFacade(String serverUrl, String realmId, String clientId, String clientSecret) { | |
this.serverUrl = serverUrl; | |
this.realmId = realmId; | |
this.clientId = clientId; | |
this.clientSecret = clientSecret; | |
} | |
public AccessToken getAccessToken() { | |
return getAccessToken(newKeycloakBuilderWithClientCredentials().build()); | |
} | |
public String getAccessTokenString() { | |
return getAccessTokenString(newKeycloakBuilderWithClientCredentials().build()); | |
} | |
public AccessToken getAccessToken(String username, String password) { | |
return getAccessToken(newKeycloakBuilderWithPasswordCredentials(username, password).build()); | |
} | |
public String getAccessTokenString(String username, String password) { | |
return getAccessTokenString(newKeycloakBuilderWithPasswordCredentials(username, password).build()); | |
} | |
private AccessToken getAccessToken(Keycloak keycloak) { | |
return extractAccessTokenFrom(keycloak, getAccessTokenString(keycloak)); | |
} | |
private String getAccessTokenString(Keycloak keycloak) { | |
AccessTokenResponse tokenResponse = getAccessTokenResponse(keycloak); | |
return tokenResponse == null ? null : tokenResponse.getToken(); | |
} | |
private AccessToken extractAccessTokenFrom(Keycloak keycloak, String token) { | |
if (token == null) { | |
return null; | |
} | |
try { | |
RSATokenVerifier verifier = RSATokenVerifier.create(token); | |
PublicKey publicKey = getRealmPublicKey(keycloak, verifier.getHeader()); | |
return verifier.realmUrl(getRealmUrl()) // | |
.publicKey(publicKey) // | |
.verify() // | |
.getToken(); | |
} catch (VerificationException e) { | |
return null; | |
} | |
} | |
private KeycloakBuilder newKeycloakBuilderWithPasswordCredentials(String username, String password) { | |
return newKeycloakBuilderWithClientCredentials() // | |
.username(username) // | |
.password(password) // | |
.grantType(OAuth2Constants.PASSWORD); | |
} | |
private KeycloakBuilder newKeycloakBuilderWithClientCredentials() { | |
return KeycloakBuilder.builder() // | |
.realm(realmId) // | |
.serverUrl(serverUrl)// | |
.clientId(clientId) // | |
.clientSecret(clientSecret) // | |
.grantType(OAuth2Constants.CLIENT_CREDENTIALS); | |
} | |
private AccessTokenResponse getAccessTokenResponse(Keycloak keycloak) { | |
try { | |
return keycloak.tokenManager().getAccessToken(); | |
} catch (Exception ex) { | |
return null; | |
} | |
} | |
public String getRealmUrl() { | |
return serverUrl + "/realms/" + realmId; | |
} | |
public String getRealmCertsUrl() { | |
return getRealmUrl() + "/protocol/openid-connect/certs"; | |
} | |
private PublicKey getRealmPublicKey(Keycloak keycloak, JWSHeader jwsHeader) { | |
// Variant 1: use openid-connect /certs endpoint | |
return retrievePublicKeyFromCertsEndpoint(jwsHeader); | |
// Variant 2: use the Public Key referenced by the "kid" in the JWSHeader | |
// in order to access realm public key we need at least realm role... e.g. view-realm | |
// return retrieveActivePublicKeyFromKeysEndpoint(keycloak, jwsHeader); | |
// Variant 3: use the active RSA Public Key exported by the PublicRealmResource representation | |
// return retrieveActivePublicKeyFromPublicRealmEndpoint(); | |
} | |
private PublicKey retrievePublicKeyFromCertsEndpoint(JWSHeader jwsHeader) { | |
try { | |
ObjectMapper om = new ObjectMapper(); | |
@SuppressWarnings("unchecked") | |
Map<String, Object> certInfos = om.readValue(new URL(getRealmCertsUrl()).openStream(), Map.class); | |
List<Map<String, Object>> keys = (List<Map<String, Object>>) certInfos.get("keys"); | |
Map<String, Object> keyInfo = null; | |
for (Map<String, Object> key : keys) { | |
String kid = (String) key.get("kid"); | |
if (jwsHeader.getKeyId().equals(kid)) { | |
keyInfo = key; | |
break; | |
} | |
} | |
if (keyInfo == null) { | |
return null; | |
} | |
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); | |
String modulusBase64 = (String) keyInfo.get("n"); | |
String exponentBase64 = (String) keyInfo.get("e"); | |
// see org.keycloak.jose.jwk.JWKBuilder#rs256 | |
Decoder urlDecoder = Base64.getUrlDecoder(); | |
BigInteger modulus = new BigInteger(1, urlDecoder.decode(modulusBase64)); | |
BigInteger publicExponent = new BigInteger(1, urlDecoder.decode(exponentBase64)); | |
return keyFactory.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return null; | |
} | |
private PublicKey retrieveActivePublicKeyFromPublicRealmEndpoint() { | |
try { | |
ObjectMapper om = new ObjectMapper(); | |
@SuppressWarnings("unchecked") | |
Map<String, Object> realmInfo = om.readValue(new URL(getRealmUrl()).openStream(), Map.class); | |
return toPublicKey((String) realmInfo.get("public_key")); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
return null; | |
} | |
private PublicKey retrieveActivePublicKeyFromKeysEndpoint(Keycloak keycloak, JWSHeader jwsHeader) { | |
List<KeyMetadataRepresentation> keys = keycloak.realm(realmId).keys().getKeyMetadata().getKeys(); | |
String publicKeyString = null; | |
for (KeyMetadataRepresentation key : keys) { | |
if (key.getKid().equals(jwsHeader.getKeyId())) { | |
publicKeyString = key.getPublicKey(); | |
break; | |
} | |
} | |
return toPublicKey(publicKeyString); | |
} | |
public PublicKey toPublicKey(String publicKeyString) { | |
try { | |
byte[] publicBytes = Base64.getDecoder().decode(publicKeyString); | |
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); | |
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); | |
return keyFactory.generatePublic(keySpec); | |
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) { | |
return null; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>de.tdlabs.training</groupId> | |
<artifactId>idm-keycloak-client-example</artifactId> | |
<version>0.0.1-SNAPSHOT</version> | |
<properties> | |
<keycloak.version>2.5.1.Final</keycloak.version> | |
<resteasy.version>3.0.14.Final</resteasy.version> | |
<maven.compiler.source>1.8</maven.compiler.source> | |
<maven.compiler.target>1.8</maven.compiler.target> | |
</properties> | |
<dependencies> | |
<dependency> | |
<groupId>org.keycloak</groupId> | |
<artifactId>keycloak-admin-client</artifactId> | |
<version>${keycloak.version}</version> | |
</dependency> | |
<dependency> | |
<groupId>org.jboss.resteasy</groupId> | |
<artifactId>resteasy-client</artifactId> | |
<version>${resteasy.version}</version> | |
</dependency> | |
<dependency> | |
<groupId>org.jboss.resteasy</groupId> | |
<artifactId>resteasy-jackson2-provider</artifactId> | |
<version>${resteasy.version}</version> | |
</dependency> | |
</dependencies> | |
</project> |
@augi,
val om = new ObjectMapper(), what does val means here?
@srinivaskancharla It is a class from Jackson library, it serves for JSON deserialization.
@augi, could you please give me the full package details to import/download.
Hi Thomas,
thx for your example. If have two questions, which i hope you can answer.
- Why you have included resteasy-client in your pom? As far as i have seen, its not used.
- How can i use KeycloakRestTemplate now, to calling an other service with the received token.
Thx in advanced.
br
Michael
Thanks for the code
I've improved two methods
private PublicKey retrievePublicKeyFromCertsEndpoint() {
try {
ObjectMapper om = new ObjectMapper();
JSONWebKeySet jwks = om.readValue(new URL(getRealmCertsUrl()).openStream(), JSONWebKeySet.class);
JWK jwk = jwks.getKeys()[0];
return JWKParser.create(jwk).toPublicKey();
} catch (Exception e) {
log.error("Exception", e);
}
return null;
}
and
private AccessToken extractAccessTokenFrom(String token) {
if (token == null) {
return null;
}
try {
PublicKey publicKey = getRealmPublicKey();
TokenVerifier tokenVerifier = TokenVerifier.create(token, AccessToken.class);
return (AccessToken) tokenVerifier.publicKey(publicKey).verify().getToken();
} catch (VerificationException e) {
log.debug("VerificationException: ", e);
return null;
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Also please note that Public Key can be easily read in this way: