diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java index d5a0c01789e35..60fcce105c63d 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/OidcTenantConfig.java @@ -987,6 +987,20 @@ public static Token fromAudience(String... audience) { @ConfigItem public Optional header = Optional.empty(); + /** + * Decryption key location. + * JWT tokens can be inner-signed and encrypted by OpenId Connect providers. + * However, it is not always possible to remotely introspect such tokens because + * the providers may not control the private decryption keys. + * In such cases set this property to point to the file containing the decryption private key in + * PEM or JSON Web Key (JWK) format. + * Note that if a 'private_key_jwt' client authentication method is used then the private key + * which is used to sign client authentication JWT tokens will be used to try to decrypt an encrypted ID token + * if this property is not set. + */ + @ConfigItem + public Optional decryptionKeyLocation = Optional.empty(); + /** * Allow the remote introspection of JWT tokens when no matching JWK key is available. * @@ -1102,6 +1116,14 @@ public Optional getAge() { public void setAge(Duration age) { this.age = Optional.of(age); } + + public Optional getDecryptionKeyLocation() { + return decryptionKeyLocation; + } + + public void setDecryptionKeyLocation(String decryptionKeyLocation) { + this.decryptionKeyLocation = Optional.of(decryptionKeyLocation); + } } public static enum ApplicationType { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java index bcc2ea5a4bb61..2421c3b3b6dd3 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/CodeAuthenticationMechanism.java @@ -22,8 +22,10 @@ import org.jboss.logging.Logger; import org.jose4j.jwt.consumer.ErrorCodes; import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.lang.JoseException; import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.logging.Log; import io.quarkus.oidc.AuthorizationCodeTokens; import io.quarkus.oidc.IdTokenCredential; import io.quarkus.oidc.OidcTenantConfig; @@ -39,6 +41,7 @@ import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; import io.smallrye.jwt.build.Jwt; import io.smallrye.jwt.build.JwtClaimsBuilder; import io.smallrye.jwt.util.KeyUtils; @@ -216,7 +219,7 @@ public Uni apply(Void t) { context.put(OidcConstants.ACCESS_TOKEN_VALUE, session.getAccessToken()); context.put(AuthorizationCodeTokens.class.getName(), session); return authenticate(identityProviderManager, context, - new IdTokenCredential(session.getIdToken(), + new IdTokenCredential(decryptIdTokenIfEncryptedByProvider(configContext, session.getIdToken()), isInternalIdToken(session.getIdToken(), configContext))) .call(new Function>() { @Override @@ -268,6 +271,21 @@ public Uni apply(Throwable t) { }); } + private static String decryptIdTokenIfEncryptedByProvider(TenantConfigContext resolvedContext, String token) { + if ((resolvedContext.provider.tokenDecryptionKey != null || resolvedContext.provider.client.getClientJwtKey() != null) + && OidcUtils.isEncryptedToken(token)) { + try { + return OidcUtils.decryptString(token, + resolvedContext.provider.tokenDecryptionKey != null ? resolvedContext.provider.tokenDecryptionKey + : resolvedContext.provider.client.getClientJwtKey(), + KeyEncryptionAlgorithm.RSA_OAEP); + } catch (JoseException ex) { + Log.debugf("Failed to decrypt a token: %s, a token introspection will be attempted instead", ex.getMessage()); + } + } + return token; + } + private boolean isBackChannelLogoutPendingAndValid(TenantConfigContext configContext, String idToken) { TokenVerificationResult backChannelLogoutTokenResult = resolver.getBackChannelLogoutTokens() .remove(configContext.oidcConfig.getTenantId().get()); @@ -523,8 +541,10 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T context.put(OidcConstants.ACCESS_TOKEN_VALUE, tokens.getAccessToken()); context.put(AuthorizationCodeTokens.class.getName(), tokens); + final String idToken = decryptIdTokenIfEncryptedByProvider(configContext, tokens.getIdToken()); + return authenticate(identityProviderManager, context, - new IdTokenCredential(tokens.getIdToken(), internalIdToken)) + new IdTokenCredential(idToken, internalIdToken)) .call(new Function>() { @Override public Uni apply(SecurityIdentity identity) { @@ -534,7 +554,7 @@ public Uni apply(SecurityIdentity identity) { identity.getAttribute(OidcUtils.USER_INFO_ATTRIBUTE))); } return processSuccessfulAuthentication(context, configContext, - tokens, identity); + tokens, idToken, identity); } }) .map(new Function() { @@ -619,19 +639,20 @@ private String generateInternalIdToken(OidcTenantConfig oidcConfig, UserInfo use private Uni processSuccessfulAuthentication(RoutingContext context, TenantConfigContext configContext, AuthorizationCodeTokens tokens, + String idToken, SecurityIdentity securityIdentity) { return removeSessionCookie(context, configContext.oidcConfig) .chain(new Function>() { @Override public Uni apply(Void t) { - JsonObject idToken = OidcUtils.decodeJwtContent(tokens.getIdToken()); + JsonObject idTokenJson = OidcUtils.decodeJwtContent(idToken); - if (!idToken.containsKey("exp") || !idToken.containsKey("iat")) { + if (!idTokenJson.containsKey("exp") || !idTokenJson.containsKey("iat")) { LOG.debug("ID Token is required to contain 'exp' and 'iat' claims"); throw new AuthenticationCompletionException(); } - long maxAge = idToken.getLong("exp") - idToken.getLong("iat"); + long maxAge = idTokenJson.getLong("exp") - idTokenJson.getLong("iat"); if (configContext.oidcConfig.token.lifespanGrace.isPresent()) { maxAge += configContext.oidcConfig.token.lifespanGrace.getAsInt(); } @@ -824,14 +845,16 @@ public Uni apply(final AuthorizationCodeTokens tokens, final T context.put(AuthorizationCodeTokens.class.getName(), tokens); context.put(REFRESH_TOKEN_GRANT_RESPONSE, Boolean.TRUE); + final String idToken = decryptIdTokenIfEncryptedByProvider(configContext, tokens.getIdToken()); + return authenticate(identityProviderManager, context, - new IdTokenCredential(tokens.getIdToken())) + new IdTokenCredential(idToken)) .call(new Function>() { @Override public Uni apply(SecurityIdentity identity) { // after a successful refresh, rebuild the identity and update the cookie return processSuccessfulAuthentication(context, configContext, - tokens, identity); + tokens, idToken, identity); } }) .map(new Function() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java index dbe879ef0e441..f461b0a77b35e 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java @@ -53,8 +53,9 @@ public class OidcProvider implements Closeable { final OidcTenantConfig oidcConfig; final String issuer; final String[] audience; + final Key tokenDecryptionKey; - public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks) { + public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, JsonWebKeySet jwks, Key tokenDecryptionKey) { this.client = client; this.oidcConfig = oidcConfig; this.asymmetricKeyResolver = jwks == null ? null @@ -62,14 +63,16 @@ public OidcProvider(OidcProviderClient client, OidcTenantConfig oidcConfig, Json this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); + this.tokenDecryptionKey = tokenDecryptionKey; } - public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig) { + public OidcProvider(String publicKeyEnc, OidcTenantConfig oidcConfig, Key tokenDecryptionKey) { this.client = null; this.oidcConfig = oidcConfig; this.asymmetricKeyResolver = new LocalPublicKeyResolver(publicKeyEnc); this.issuer = checkIssuerProp(); this.audience = checkAudienceProp(); + this.tokenDecryptionKey = tokenDecryptionKey; } private String checkIssuerProp() { diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java index 5aab55d763619..d75293842154b 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProviderClient.java @@ -180,4 +180,8 @@ private static OIDCException responseException(HttpResponse resp) { public void close() { client.close(); } + + public Key getClientJwtKey() { + return clientJwtKey; + } } diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java index db5ab5769a780..234717da967da 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcRecorder.java @@ -1,6 +1,8 @@ package io.quarkus.oidc.runtime; +import java.security.Key; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -10,6 +12,8 @@ import java.util.function.Supplier; import org.jboss.logging.Logger; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.PublicJsonWebKey; import io.quarkus.arc.Arc; import io.quarkus.oidc.OIDCException; @@ -25,6 +29,8 @@ import io.quarkus.runtime.TlsConfig; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; +import io.smallrye.jwt.algorithm.KeyEncryptionAlgorithm; +import io.smallrye.jwt.util.KeyUtils; import io.smallrye.mutiny.Uni; import io.vertx.core.Vertx; import io.vertx.core.net.ProxyOptions; @@ -141,7 +147,7 @@ private Uni createTenantContext(Vertx vertx, OidcTenantConf if (!oidcConfig.tenantEnabled) { LOG.debugf("'%s' tenant configuration is disabled", tenantId); - return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null), oidcConfig)); + return Uni.createFrom().item(new TenantConfigContext(new OidcProvider(null, null, null, null), oidcConfig)); } if (oidcConfig.getPublicKey().isPresent()) { @@ -219,7 +225,8 @@ private static TenantConfigContext createTenantContextFromPublicKey(OidcTenantCo LOG.debug("'public-key' property for the local token verification is set," + " no connection to the OIDC server will be created"); - return new TenantConfigContext(new OidcProvider(oidcConfig.publicKey.get(), oidcConfig), oidcConfig); + return new TenantConfigContext( + new OidcProvider(oidcConfig.publicKey.get(), oidcConfig, readTokenDecryptionKey(oidcConfig)), oidcConfig); } public void setSecurityEventObserved(boolean isSecurityEventObserved) { @@ -248,17 +255,49 @@ public Uni apply(OidcProviderClient client) { @Override public OidcProvider apply(JsonWebKeySet jwks) { - return new OidcProvider(client, oidcConfig, jwks); + return new OidcProvider(client, oidcConfig, jwks, + readTokenDecryptionKey(oidcConfig)); } }); } else { - return Uni.createFrom().item(new OidcProvider(client, oidcConfig, null)); + return Uni.createFrom() + .item(new OidcProvider(client, oidcConfig, null, readTokenDecryptionKey(oidcConfig))); } } }); } + private static Key readTokenDecryptionKey(OidcTenantConfig oidcConfig) { + if (oidcConfig.token.decryptionKeyLocation.isPresent()) { + try { + Key key = null; + + String keyContent = KeyUtils.readKeyContent(oidcConfig.token.decryptionKeyLocation.get()); + if (keyContent != null) { + List keys = KeyUtils.loadJsonWebKeys(keyContent); + if (keys != null && keys.size() == 1 && + (keys.get(0).getAlgorithm() == null + || keys.get(0).getAlgorithm() == KeyEncryptionAlgorithm.RSA_OAEP.getAlgorithm()) + && ("enc".equals(keys.get(0).getUse()) || keys.get(0).getUse() == null)) { + key = PublicJsonWebKey.class.cast(keys.get(0)).getPrivateKey(); + } + } + if (key == null) { + key = KeyUtils.decodeDecryptionPrivateKey(keyContent); + } + return key; + } catch (Exception ex) { + throw new ConfigurationException( + String.format("Token decryption key for tenant %s can not be read from %s", + oidcConfig.tenantId.get(), oidcConfig.token.decryptionKeyLocation.get()), + ex); + } + } else { + return null; + } + } + protected static Uni getJsonWebSetUni(OidcProviderClient client, OidcTenantConfig oidcConfig) { if (!oidcConfig.isDiscoveryEnabled().orElse(true)) { final long connectionDelayInMillisecs = OidcCommonUtils.getConnectionDelayInMillis(oidcConfig); diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java index 23ca7aade9a8a..11c35fb10e30c 100644 --- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java +++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcUtils.java @@ -1,6 +1,7 @@ package io.quarkus.oidc.runtime; import java.nio.charset.StandardCharsets; +import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -22,6 +23,7 @@ import org.jose4j.jwe.JsonWebEncryption; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.lang.JoseException; import io.quarkus.oidc.AccessTokenCredential; import io.quarkus.oidc.AuthorizationCodeTokens; @@ -76,6 +78,10 @@ private OidcUtils() { } + public static boolean isEncryptedToken(String token) { + return new StringTokenizer(token, ".").countTokens() == 5; + } + public static boolean isOpaqueToken(String token) { return new StringTokenizer(token, ".").countTokens() != 3; } @@ -423,14 +429,18 @@ public static String encryptString(String jweString, SecretKey key) throws Excep return jwe.getCompactSerialization(); } - public static JsonObject decryptJson(String jweString, SecretKey key) throws Exception { + public static JsonObject decryptJson(String jweString, Key key) throws Exception { return new JsonObject(decryptString(jweString, key)); } - public static String decryptString(String jweString, SecretKey key) throws Exception { + public static String decryptString(String jweString, Key key) throws Exception { + return decryptString(jweString, key, KeyEncryptionAlgorithm.A256KW); + } + + public static String decryptString(String jweString, Key key, KeyEncryptionAlgorithm algorithm) throws JoseException { JsonWebEncryption jwe = new JsonWebEncryption(); jwe.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT, - KeyEncryptionAlgorithm.A256KW.getAlgorithm())); + algorithm.getAlgorithm())); jwe.setKey(key); jwe.setCompactSerialization(jweString); return jwe.getPlaintextString(); diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowEncryptedIdTokenResource.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowEncryptedIdTokenResource.java new file mode 100644 index 0000000000000..d9d04b65e8240 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CodeFlowEncryptedIdTokenResource.java @@ -0,0 +1,32 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.jwt.JsonWebToken; + +import io.quarkus.oidc.IdToken; +import io.quarkus.security.Authenticated; + +@Path("/code-flow-encrypted-id-token") +public class CodeFlowEncryptedIdTokenResource { + + @Inject + @IdToken + JsonWebToken idToken; + + @GET + @Authenticated + @Path("/code-flow-encrypted-id-token-jwk") + public String accessJwk() { + return "user: " + idToken.getName(); + } + + @GET + @Authenticated + @Path("/code-flow-encrypted-id-token-pem") + public String accessPem() { + return "user: " + idToken.getName(); + } +} diff --git a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java index 4e9d05c7b30e9..ad09117d4d1ca 100644 --- a/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java +++ b/integration-tests/oidc-wiremock/src/main/java/io/quarkus/it/keycloak/CustomTenantResolver.java @@ -17,6 +17,12 @@ public String resolve(RoutingContext context) { if (path.endsWith("code-flow") || path.endsWith("code-flow/logout")) { return "code-flow"; } + if (path.endsWith("code-flow-encrypted-id-token-jwk")) { + return "code-flow-encrypted-id-token-jwk"; + } + if (path.endsWith("code-flow-encrypted-id-token-pem")) { + return "code-flow-encrypted-id-token-pem"; + } if (path.endsWith("code-flow-form-post")) { return "code-flow-form-post"; } diff --git a/integration-tests/oidc-wiremock/src/main/resources/application.properties b/integration-tests/oidc-wiremock/src/main/resources/application.properties index ee8f844055a2f..3419d65070328 100644 --- a/integration-tests/oidc-wiremock/src/main/resources/application.properties +++ b/integration-tests/oidc-wiremock/src/main/resources/application.properties @@ -20,6 +20,26 @@ quarkus.oidc.code-flow.logout.extra-params.client_id=${quarkus.oidc.code-flow.cl quarkus.oidc.code-flow.credentials.secret=secret quarkus.oidc.code-flow.application-type=web-app +quarkus.oidc.code-flow-encrypted-id-token-jwk.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow-encrypted-id-token-jwk.client-id=quarkus-web-app +quarkus.oidc.code-flow-encrypted-id-token-jwk.credentials.secret=secret +quarkus.oidc.code-flow-encrypted-id-token-jwk.application-type=web-app +quarkus.oidc.code-flow-encrypted-id-token-jwk.discovery-enabled=false +quarkus.oidc.code-flow-encrypted-id-token-jwk.authorization-path=/ +quarkus.oidc.code-flow-encrypted-id-token-jwk.token-path=${keycloak.url}/realms/quarkus/encrypted-id-token +quarkus.oidc.code-flow-encrypted-id-token-jwk.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs +quarkus.oidc.code-flow-encrypted-id-token-jwk.token.decryption-key-location=privateKey.jwk + +quarkus.oidc.code-flow-encrypted-id-token-pem.auth-server-url=${keycloak.url}/realms/quarkus/ +quarkus.oidc.code-flow-encrypted-id-token-pem.client-id=quarkus-web-app +quarkus.oidc.code-flow-encrypted-id-token-pem.credentials.secret=secret +quarkus.oidc.code-flow-encrypted-id-token-pem.application-type=web-app +quarkus.oidc.code-flow-encrypted-id-token-pem.discovery-enabled=false +quarkus.oidc.code-flow-encrypted-id-token-pem.authorization-path=/ +quarkus.oidc.code-flow-encrypted-id-token-pem.token-path=${keycloak.url}/realms/quarkus/encrypted-id-token +quarkus.oidc.code-flow-encrypted-id-token-pem.jwks-path=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs +quarkus.oidc.code-flow-encrypted-id-token-pem.token.decryption-key-location=privateKey.pem + quarkus.oidc.code-flow-form-post.auth-server-url=${keycloak.url}/realms/quarkus-form-post/ quarkus.oidc.code-flow-form-post.client-id=quarkus-web-app quarkus.oidc.code-flow-form-post.credentials.secret=secret @@ -105,3 +125,5 @@ quarkus.http.auth.permission.logout.policy=authenticated quarkus.http.auth.permission.backchannellogout.paths=/back-channel-logout quarkus.http.auth.permission.backchannellogout.policy=permit + +quarkus.native.additional-build-args=-H:IncludeResources=private.*\\.* diff --git a/integration-tests/oidc-wiremock/src/main/resources/privateKey.jwk b/integration-tests/oidc-wiremock/src/main/resources/privateKey.jwk new file mode 100644 index 0000000000000..a5c70b8e55f88 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/resources/privateKey.jwk @@ -0,0 +1,12 @@ +{ + "kty":"RSA", + "kid":"1", + "n":"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ", + "e":"AQAB", + "d":"AvIDTlsK_priQLTwEQf5IVf2Xl638Q7dHdXyDC-oAAPmv1GcqRVH7Wm5oAPW_CZQfWhV55WRVaJzP8AhksyD5NcslH79hQZT4NT6xgApGYecrvmseuZ4dfR-e1cxXTRNBxaoXvwSiv4LuOPHmC8XGX712AhOoCGKiZp1WFqqkKwTpkgJEApJFVb-XRIKQa0YaRKpJsJ534pLMwTh7LoPLM4BCaBVbRfHzH2H5L3TSJP718kyCuxg3z2p9Y7zIOLTmgFdeR0_kd_xKUFZ2ByN3SKlC0IWlLUSiMPsGYExRpZTMZHKyD939gv-2_Z-bOYfKlYNIvAmQH_8CcX2I039LQ", + "p":"104AjPaxZoi_BiMBODlChnZOvRJT071PdkeZ283uyrdW8qqKD9q8FTMgUXzKoboHtUiHbJbLOobPmPDh93839rq7dTdCNzNVOuLmE-V3_bmaShdzvxEIazwPf6AvjbEZAc-zu2RS4SNkp1LbzgSl9nINSlF7t6Lkl6T28PYULys", + "q":"om5ooyzxa4ZJ-dU0ODsEb-Bmz6xwb27xF9aEhBYJprHeoNs2QM1D64_A39weD9MYwBux4-ivshCJ0dVKEbDujJRLnzf-ssrasA6CFyaaCT4DKtq1oWb9rcG-2LQd5Bm9PttrUrSUNqitr085IYikaLEz7UU6gtXPoC8UOcJ4cSc", + "dp":"DeWE95Q8oweUfMrpmz1m49LjBiUWsAX6CQJaFevWy9LFk-gZ_Sf7F8sy_M93LLUbJkJGK2YYO_DTmWWC0Dyv2gb3bntglLuFdsWKYCJhekjugnW9DMoGpxU7Utt99kFGAe3sBd5V0x47sukQMt3t8FgwL2nO-G1VH8yP-8GGT_0", + "dq":"TGBeE1wuqMCcSD1YMJiPnYuGzF_o_nzMIMldxj4Wi6tXY4uwFwhtx3Xw21JFUGuSV8KuAtyGwNPF-kSwb2Eiyjdw140c1jVMXzxzLy-XfoEKPDxa62niHrHba0pGQ9tWgRfrfxgqGQl3odc-peX6aL_qCsdim-KtnkSE3iPzPkE", + "qi":"Jzp5KnT24y0wOoPUn_11S3ZcYl0i03dkaH4c5zR02G1MJG9K017juurx2aXVTctOzrj7O226EUiL1Qbq3QtnWFDDGY6vNZuqzJM7AMXsvp1djq_6fEVhxCIOgfJbmhb3mkG82rxn4et9o_TNr6mvEmHzG15sHbvZbAnn4GeqToY" +} diff --git a/integration-tests/oidc-wiremock/src/main/resources/privateKey.pem b/integration-tests/oidc-wiremock/src/main/resources/privateKey.pem new file mode 100644 index 0000000000000..e644230c0ef51 --- /dev/null +++ b/integration-tests/oidc-wiremock/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCInDfeXV5UCwah +GVLKj4UKKZ45z4BplvNDaIRIDc2S6jdMVvXMiSa3STOWdZUa3jx2RoiXXdV15+H2 +GrD+v3I0XzFmpfV+nQ+cJZzkz9Nf+G3l+b3LROluJWayN5ExZmfiOvcOwWi7FD1o +FdQmK7bpXS93a+sQJ54Ct4VPia2gdjXcPZZV4m9uhzmYQlynnQk2HFEInlSRjyNH +/B9STZPPPj58CPnFBoFxxeeoky7/ULoyOgISIHsbV3XdtU9F4h4AZEpUkgXqiCgI +3wlus6Fz/jyVPi7KNuCd3bht5q24pah2ZZiFpy0WhtEZijnnnVyAx7wGjGlVDi2j +fRwYDSqNAgMBAAECggEAAvIDTlsK/priQLTwEQf5IVf2Xl638Q7dHdXyDC+oAAPm +v1GcqRVH7Wm5oAPW/CZQfWhV55WRVaJzP8AhksyD5NcslH79hQZT4NT6xgApGYec +rvmseuZ4dfR+e1cxXTRNBxaoXvwSiv4LuOPHmC8XGX712AhOoCGKiZp1WFqqkKwT +pkgJEApJFVb+XRIKQa0YaRKpJsJ534pLMwTh7LoPLM4BCaBVbRfHzH2H5L3TSJP7 +18kyCuxg3z2p9Y7zIOLTmgFdeR0/kd/xKUFZ2ByN3SKlC0IWlLUSiMPsGYExRpZT +MZHKyD939gv+2/Z+bOYfKlYNIvAmQH/8CcX2I039LQKBgQDXTgCM9rFmiL8GIwE4 +OUKGdk69ElPTvU92R5nbze7Kt1byqooP2rwVMyBRfMqhuge1SIdslss6hs+Y8OH3 +fzf2urt1N0I3M1U64uYT5Xf9uZpKF3O/EQhrPA9/oC+NsRkBz7O7ZFLhI2SnUtvO +BKX2cg1KUXu3ouSXpPbw9hQvKwKBgQCibmijLPFrhkn51TQ4OwRv4GbPrHBvbvEX +1oSEFgmmsd6g2zZAzUPrj8Df3B4P0xjAG7Hj6K+yEInR1UoRsO6MlEufN/6yytqw +DoIXJpoJPgMq2rWhZv2twb7YtB3kGb0+22tStJQ2qK2vTzkhiKRosTPtRTqC1c+g +LxQ5wnhxJwKBgA3lhPeUPKMHlHzK6Zs9ZuPS4wYlFrAF+gkCWhXr1svSxZPoGf0n ++xfLMvzPdyy1GyZCRitmGDvw05llgtA8r9oG9257YJS7hXbFimAiYXpI7oJ1vQzK +BqcVO1LbffZBRgHt7AXeVdMeO7LpEDLd7fBYMC9pzvhtVR/Mj/vBhk/9AoGATGBe +E1wuqMCcSD1YMJiPnYuGzF/o/nzMIMldxj4Wi6tXY4uwFwhtx3Xw21JFUGuSV8Ku +AtyGwNPF+kSwb2Eiyjdw140c1jVMXzxzLy+XfoEKPDxa62niHrHba0pGQ9tWgRfr +fxgqGQl3odc+peX6aL/qCsdim+KtnkSE3iPzPkECgYAnOnkqdPbjLTA6g9Sf/XVL +dlxiXSLTd2RofhznNHTYbUwkb0rTXuO66vHZpdVNy07OuPs7bboRSIvVBurdC2dY +UMMZjq81m6rMkzsAxey+nV2Or/p8RWHEIg6B8luaFveaQbzavGfh632j9M2vqa8S +YfMbXmwdu9lsCefgZ6pOhg== +-----END RSA PRIVATE KEY----- diff --git a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java index c661942f30646..e6d1902e859d0 100644 --- a/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java +++ b/integration-tests/oidc-wiremock/src/test/java/io/quarkus/it/keycloak/CodeFlowAuthorizationTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.net.URI; @@ -64,6 +65,37 @@ public void testCodeFlow() throws IOException { } } + @Test + public void testCodeFlowEncryptedIdToken() throws IOException { + doTestCodeFlowEncryptedIdToken("code-flow-encrypted-id-token-jwk"); + doTestCodeFlowEncryptedIdToken("code-flow-encrypted-id-token-pem"); + } + + private void doTestCodeFlowEncryptedIdToken(String tenant) throws IOException { + try (final WebClient webClient = createWebClient()) { + webClient.getOptions().setRedirectEnabled(true); + HtmlPage page = webClient.getPage("http://localhost:8081/code-flow-encrypted-id-token/" + tenant); + + HtmlForm form = page.getFormByName("form"); + form.getInputByName("username").type("alice"); + form.getInputByName("password").type("alice"); + + page = form.getInputByValue("login").click(); + + assertEquals("user: alice", page.getBody().asText()); + Cookie sessionCookie = getSessionCookie(webClient, tenant); + assertNotNull(sessionCookie); + // default session cookie format: "idtoken|accesstoken|refreshtoken" + assertTrue(OidcUtils.isEncryptedToken(sessionCookie.getValue().split("\\|")[0])); + + // repeat the call with the session cookie containing the encrypted id token + page = webClient.getPage("http://localhost:8081/code-flow-encrypted-id-token/" + tenant); + assertEquals("user: alice", page.getBody().asText()); + + webClient.getCookieManager().clearCookies(); + } + } + @Test public void testCodeFlowFormPost() throws IOException { defineCodeFlowLogoutStub(); diff --git a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java index 4190e78fe26fb..1d743b83d2cef 100644 --- a/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java +++ b/test-framework/oidc-server/src/main/java/io/quarkus/test/oidc/server/OidcWiremockTestResource.java @@ -140,6 +140,7 @@ public Map start() { // Code Flow Authorization Mock defineCodeFlowAuthorizationMockTokenStub(); + defineCodeFlowAuthorizationMockEncryptedTokenStub(); // Login Page server.stubFor( @@ -255,6 +256,31 @@ private void defineCodeFlowAuthorizationMockTokenStub() { "}"))); } + private void defineCodeFlowAuthorizationMockEncryptedTokenStub() { + server.stubFor(WireMock.post("/auth/realms/quarkus/encrypted-id-token") + .withRequestBody(containing("authorization_code")) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/json") + .withBody("{\n" + + " \"access_token\": \"" + + getAccessToken("alice", getAdminRoles()) + "\",\n" + + " \"refresh_token\": \"07e08903-1263-4dd1-9fd1-4a59b0db5283\",\n" + + " \"id_token\": \"" + getEncryptedIdToken("alice", getAdminRoles()) + + "\"\n" + + "}"))); + } + + public static String getEncryptedIdToken(String userName, Set groups) { + return Jwt.preferredUserName(userName) + .groups(groups) + .issuer("quarkus.test.oidc.token.issuer") + .audience("quarkus.test.oidc.token.audience") + .subject("123456") + .jws() + .keyId("1") + .innerSign("privateKey.jwk").encrypt("publicKey.jwk"); + } + public static X509Certificate getCertificate() { try { return new X509Util().fromBase64Der(ENCODED_X5C); diff --git a/test-framework/oidc-server/src/main/resources/publicKey.jwk b/test-framework/oidc-server/src/main/resources/publicKey.jwk new file mode 100644 index 0000000000000..6bff0eb3f0293 --- /dev/null +++ b/test-framework/oidc-server/src/main/resources/publicKey.jwk @@ -0,0 +1,6 @@ +{ + "kid": "1", + "kty":"RSA", + "n":"iJw33l1eVAsGoRlSyo-FCimeOc-AaZbzQ2iESA3Nkuo3TFb1zIkmt0kzlnWVGt48dkaIl13Vdefh9hqw_r9yNF8xZqX1fp0PnCWc5M_TX_ht5fm9y0TpbiVmsjeRMWZn4jr3DsFouxQ9aBXUJiu26V0vd2vrECeeAreFT4mtoHY13D2WVeJvboc5mEJcp50JNhxRCJ5UkY8jR_wfUk2Tzz4-fAj5xQaBccXnqJMu_1C6MjoCEiB7G1d13bVPReIeAGRKVJIF6ogoCN8JbrOhc_48lT4uyjbgnd24beatuKWodmWYhactFobRGYo5551cgMe8BoxpVQ4to30cGA0qjQ", + "e":"AQAB" + } \ No newline at end of file