Skip to content

Commit 029bb77

Browse files
authored
Merge pull request #48525 from sberyozkin/oidc_client_reg_public_key
Simplify public key registration with OIDC client registration
2 parents 2fbc20f + fbb7056 commit 029bb77

File tree

3 files changed

+107
-60
lines changed

3 files changed

+107
-60
lines changed

extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/ClientMetadata.java

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,26 @@
22

33
import static io.quarkus.jsonp.JsonProviderHolder.jsonProvider;
44

5+
import java.security.PublicKey;
6+
import java.security.interfaces.ECPublicKey;
7+
import java.security.interfaces.EdECPublicKey;
8+
import java.security.interfaces.RSAPublicKey;
59
import java.util.List;
610
import java.util.Map;
11+
import java.util.Set;
712

13+
import jakarta.json.JsonArrayBuilder;
814
import jakarta.json.JsonObject;
915
import jakarta.json.JsonObjectBuilder;
1016

17+
import org.jose4j.jwk.JsonWebKey.OutputControlLevel;
18+
import org.jose4j.jwk.PublicJsonWebKey;
19+
import org.jose4j.lang.JoseException;
20+
21+
import io.quarkus.oidc.client.registration.runtime.OidcClientRegistrationException;
1122
import io.quarkus.oidc.common.runtime.AbstractJsonObject;
1223
import io.quarkus.oidc.common.runtime.OidcConstants;
24+
import io.smallrye.jwt.algorithm.SignatureAlgorithm;
1325

1426
public class ClientMetadata extends AbstractJsonObject {
1527

@@ -100,6 +112,70 @@ public Builder postLogoutUri(String postLogoutUri) {
100112
return this;
101113
}
102114

115+
public Builder grantType(String grantType) {
116+
return grantTypes(Set.of(grantType));
117+
}
118+
119+
public Builder grantTypes(Set<String> grantTypes) {
120+
if (built) {
121+
throw new IllegalStateException();
122+
}
123+
JsonArrayBuilder arrayBuilder = jsonProvider().createArrayBuilder();
124+
for (String grantType : grantTypes) {
125+
arrayBuilder.add(grantType);
126+
}
127+
builder.add(OidcConstants.CLIENT_METADATA_GRANT_TYPES, arrayBuilder.build());
128+
return this;
129+
}
130+
131+
public Builder tokenEndpointAuthMethod(String tokenEndpointAuthMethod) {
132+
if (built) {
133+
throw new IllegalStateException();
134+
}
135+
builder.add(OidcConstants.CLIENT_METADATA_TOKEN_ENDPOINT_AUTH_METHOD, tokenEndpointAuthMethod);
136+
return this;
137+
}
138+
139+
public Builder jwk(PublicKey publicKey) {
140+
return jwks(Set.of(publicKey));
141+
}
142+
143+
public Builder jwks(Set<PublicKey> publicKeys) {
144+
if (built) {
145+
throw new IllegalStateException();
146+
}
147+
JsonArrayBuilder keysBuilder = jsonProvider().createArrayBuilder();
148+
for (PublicKey publicKey : publicKeys) {
149+
JsonObjectBuilder jwkBuilder = jsonProvider().createObjectBuilder();
150+
for (Map.Entry<String, Object> entry : convertPublicKeyToJwk(publicKey).entrySet()) {
151+
jwkBuilder.add(entry.getKey(), entry.getValue().toString());
152+
}
153+
jwkBuilder.add("use", "sig");
154+
jwkBuilder.add("alg", getAlgorithm(publicKey));
155+
keysBuilder.add(jwkBuilder);
156+
}
157+
JsonObjectBuilder jwksBuilder = jsonProvider().createObjectBuilder();
158+
jwksBuilder.add("keys", keysBuilder);
159+
builder.add(OidcConstants.CLIENT_METADATA_JWKS, jwksBuilder);
160+
return this;
161+
}
162+
163+
public Builder jwk(Map<String, String> keyProperties) {
164+
if (built) {
165+
throw new IllegalStateException();
166+
}
167+
JsonArrayBuilder keysBuilder = jsonProvider().createArrayBuilder();
168+
JsonObjectBuilder jwkBuilder = jsonProvider().createObjectBuilder();
169+
for (Map.Entry<String, String> entry : keyProperties.entrySet()) {
170+
jwkBuilder.add(entry.getKey(), entry.getValue());
171+
}
172+
keysBuilder.add(jwkBuilder);
173+
JsonObjectBuilder jwksBuilder = jsonProvider().createObjectBuilder();
174+
jwksBuilder.add("keys", keysBuilder);
175+
builder.add(OidcConstants.CLIENT_METADATA_JWKS, jwksBuilder);
176+
return this;
177+
}
178+
103179
public Builder extraProps(Map<String, String> extraProps) {
104180
if (built) {
105181
throw new IllegalStateException();
@@ -108,6 +184,26 @@ public Builder extraProps(Map<String, String> extraProps) {
108184
return this;
109185
}
110186

187+
private static Map<String, Object> convertPublicKeyToJwk(PublicKey key) {
188+
try {
189+
return PublicJsonWebKey.Factory.newPublicJwk(key).toParams(OutputControlLevel.PUBLIC_ONLY);
190+
} catch (JoseException ex) {
191+
throw new OidcClientRegistrationException(ex);
192+
}
193+
}
194+
195+
private static String getAlgorithm(PublicKey publicKey) {
196+
if (publicKey instanceof RSAPublicKey) {
197+
return SignatureAlgorithm.RS256.getAlgorithm();
198+
} else if (publicKey instanceof ECPublicKey) {
199+
return SignatureAlgorithm.ES256.getAlgorithm();
200+
} else if (publicKey instanceof EdECPublicKey) {
201+
return SignatureAlgorithm.EDDSA.getAlgorithm();
202+
} else {
203+
throw new OidcClientRegistrationException("Unrecognized public key algorithm: " + publicKey.getAlgorithm());
204+
}
205+
}
206+
111207
public ClientMetadata build() {
112208
built = true;
113209
return new ClientMetadata(builder.build());

extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public final class OidcConstants {
8686

8787
public static final String CLIENT_METADATA_CLIENT_NAME = "client_name";
8888
public static final String CLIENT_METADATA_REDIRECT_URIS = "redirect_uris";
89+
public static final String CLIENT_METADATA_GRANT_TYPES = "grant_types";
90+
public static final String CLIENT_METADATA_JWKS = "jwks";
91+
public static final String CLIENT_METADATA_TOKEN_ENDPOINT_AUTH_METHOD = "token_endpoint_auth_method";
8992
public static final String CLIENT_METADATA_POST_LOGOUT_URIS = "post_logout_redirect_uris";
9093
public static final String CLIENT_METADATA_SECRET_EXPIRES_AT = "client_secret_expires_at";
9194
public static final String CLIENT_METADATA_ID_ISSUED_AT = "client_id_issued_at";
Lines changed: 8 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
package io.quarkus.it.keycloak;
22

33
import java.io.IOException;
4-
import java.math.BigInteger;
54
import java.nio.file.Files;
65
import java.nio.file.Path;
76
import java.security.KeyPair;
87
import java.security.KeyPairGenerator;
9-
import java.security.MessageDigest;
10-
import java.security.NoSuchAlgorithmException;
11-
import java.security.interfaces.RSAPublicKey;
12-
import java.util.Arrays;
138

149
import jakarta.enterprise.event.Observes;
1510
import jakarta.inject.Singleton;
1611

1712
import org.eclipse.microprofile.config.inject.ConfigProperty;
18-
import org.jose4j.base64url.Base64Url;
1913

2014
import io.quarkus.logging.Log;
2115
import io.quarkus.oidc.client.registration.ClientMetadata;
@@ -43,7 +37,7 @@ public class ClientAuthWithSignedJwtCreator {
4337
void observe(@Observes StartupEvent event, OidcClientRegistration clientRegistration,
4438
@ConfigProperty(name = "keycloak.url") String keycloakUrl) {
4539
generateRsaKeyPair();
46-
var requestClientMetadata = new ClientMetadata(createClientMetadataJson(keycloakUrl));
40+
var requestClientMetadata = createClientMetadata();
4741
var registeredClient = clientRegistration.registerClient(requestClientMetadata).await().indefinitely();
4842
this.createdClientMetadata = registeredClient.metadata();
4943
var signedJwt = createSignedJwt(keycloakUrl, this.createdClientMetadata.getClientId());
@@ -76,38 +70,13 @@ private String createSignedJwt(String keycloakUrl, String clientId) {
7670
.sign(keyPair.getPrivate());
7771
}
7872

79-
private String createClientMetadataJson(String keycloakUrl) {
80-
RSAPublicKey rsaKey = (RSAPublicKey) keyPair.getPublic();
81-
String modulus = Base64Url.encode(toIntegerBytes(rsaKey.getModulus()));
82-
String publicExponent = Base64Url.encode(toIntegerBytes(rsaKey.getPublicExponent()));
83-
return """
84-
{
85-
"redirect_uris" : [ "http://localhost:8081/protected/jwt-bearer-token-file" ],
86-
"token_endpoint_auth_method" : "private_key_jwt",
87-
"grant_types" : [ "client_credentials", "authorization_code" ],
88-
"client_name" : "signed-jwt-test",
89-
"client_uri" : "%1$s/auth/realms/quarkus/app",
90-
"jwks" : {
91-
"keys" : [ {
92-
"kid" : "%4$s",
93-
"kty" : "RSA",
94-
"alg" : "RS256",
95-
"use" : "sig",
96-
"e" : "%3$s",
97-
"n" : "%2$s"
98-
} ]
99-
}
100-
}
101-
"""
102-
.formatted(keycloakUrl, modulus, publicExponent, createKeyId());
103-
}
104-
105-
private String createKeyId() {
106-
try {
107-
return Base64Url.encode(MessageDigest.getInstance("SHA-256").digest(keyPair.getPrivate().getEncoded()));
108-
} catch (NoSuchAlgorithmException e) {
109-
throw new RuntimeException("Failed to generate key id", e);
110-
}
73+
private ClientMetadata createClientMetadata() {
74+
return ClientMetadata.builder()
75+
.redirectUri("http://localhost:8081/protected/jwt-bearer-token-file")
76+
.tokenEndpointAuthMethod("private_key_jwt")
77+
.clientName("signed-jwt-test")
78+
.jwk(keyPair.getPublic())
79+
.build();
11180
}
11281

11382
private static KeyPair generateRsaKeyPair() {
@@ -119,25 +88,4 @@ private static KeyPair generateRsaKeyPair() {
11988
throw new RuntimeException("Failed to generate RSA key pair", e);
12089
}
12190
}
122-
123-
private static byte[] toIntegerBytes(final BigInteger bigInt) {
124-
final int bitlen = bigInt.bitLength();
125-
// following code comes from the Keycloak project
126-
127-
final int bytelen = (bitlen + 7) / 8;
128-
final byte[] array = bigInt.toByteArray();
129-
if (array.length == bytelen) {
130-
// expected number of bytes, return them
131-
return array;
132-
} else if (bytelen < array.length) {
133-
// if array is greater is because the sign bit (it can be only 1 byte more), remove it
134-
return Arrays.copyOfRange(array, array.length - bytelen, array.length);
135-
} else {
136-
// if array is smaller fill it with zeros
137-
final byte[] resizedBytes = new byte[bytelen];
138-
System.arraycopy(array, 0, resizedBytes, bytelen - array.length, array.length);
139-
return resizedBytes;
140-
}
141-
}
142-
14391
}

0 commit comments

Comments
 (0)