From a4f83c569d5da0dc046cb497ff4bc117b36dd3e2 Mon Sep 17 00:00:00 2001 From: Takashi Norimatsu Date: Fri, 8 Oct 2021 15:57:03 +0900 Subject: [PATCH] KEYCLOAK-19510 Nested JWT JOSE header needs to set JWT to cty field --- .../java/org/keycloak/jose/jwe/JWEHeader.java | 8 ++++++++ .../main/java/org/keycloak/util/TokenUtil.java | 2 +- .../oidc/AuthorizationTokenEncryptionTest.java | 18 ++++++++++++++++++ .../testsuite/oidc/IdTokenEncryptionTest.java | 18 ++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java index 12bd526d6e..55cb782080 100644 --- a/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java +++ b/core/src/main/java/org/keycloak/jose/jwe/JWEHeader.java @@ -67,6 +67,14 @@ public class JWEHeader implements JOSEHeader { this.keyId = keyId; } + public JWEHeader(String algorithm, String encryptionAlgorithm, String compressionAlgorithm, String keyId, String contentType) { + this.algorithm = algorithm; + this.encryptionAlgorithm = encryptionAlgorithm; + this.compressionAlgorithm = compressionAlgorithm; + this.keyId = keyId; + this.contentType = contentType; + } + public String getAlgorithm() { return algorithm; } diff --git a/core/src/main/java/org/keycloak/util/TokenUtil.java b/core/src/main/java/org/keycloak/util/TokenUtil.java index 80fc4053a5..bcee045fc8 100644 --- a/core/src/main/java/org/keycloak/util/TokenUtil.java +++ b/core/src/main/java/org/keycloak/util/TokenUtil.java @@ -154,7 +154,7 @@ public class TokenUtil { } public static String jweKeyEncryptionEncode(Key encryptionKEK, byte[] contentBytes, String algAlgorithm, String encAlgorithm, String kid, JWEAlgorithmProvider jweAlgorithmProvider, JWEEncryptionProvider jweEncryptionProvider) throws JWEException { - JWEHeader jweHeader = new JWEHeader(algAlgorithm, encAlgorithm, null, kid); + JWEHeader jweHeader = new JWEHeader(algAlgorithm, encAlgorithm, null, kid, "JWT"); return jweKeyEncryptionEncode(encryptionKEK, contentBytes, jweHeader, jweAlgorithmProvider, jweEncryptionProvider); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java index df366a756f..b6fd1e8ee4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/AuthorizationTokenEncryptionTest.java @@ -20,13 +20,16 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.AesCbcHmacShaContentEncryptionProvider; import org.keycloak.crypto.AesGcmContentEncryptionProvider; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.RsaCekManagementProvider; +import org.keycloak.jose.JOSEHeader; import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; @@ -43,8 +46,10 @@ import org.keycloak.testsuite.client.resources.TestOIDCEndpointsApplicationResou import org.keycloak.testsuite.pages.*; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.TokenSignatureUtil; +import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -199,6 +204,10 @@ public class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakT byte[] decodedString = TokenUtil.jweKeyEncryptionVerifyAndDecode(decryptionKEK, jweStr, algorithmProvider, encryptionProvider); String authorizationTokenString = new String(decodedString, "UTF-8"); + // a nested JWT (signed and encrypted JWT) needs to set "JWT" to its JOSE Header's "cty" field + JWEHeader jweHeader = (JWEHeader) getHeader(parts[0]); + Assert.assertEquals("JWT", jweHeader.getContentType()); + // verify JWS AuthorizationResponseToken authorizationToken = oauth.verifyAuthorizationResponseToken(authorizationTokenString); Assert.assertEquals("test-app", authorizationToken.getAudience()[0]); @@ -245,6 +254,15 @@ public class AuthorizationTokenEncryptionTest extends AbstractTestRealmKeycloakT return jweEncryptionProvider; } + private JOSEHeader getHeader(String base64Header) { + try { + byte[] decodedHeader = Base64Url.decode(base64Header); + return JsonSerialization.readValue(decodedHeader, JWEHeader.class); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + @Test @UncaughtServerErrorExpected public void testAuthorizationEncryptionWithoutEncryptionKEK() throws MalformedURLException, URISyntaxException { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/IdTokenEncryptionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/IdTokenEncryptionTest.java index b8574eb675..9b55200d4c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/IdTokenEncryptionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/IdTokenEncryptionTest.java @@ -22,13 +22,16 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.PemUtils; import org.keycloak.crypto.AesCbcHmacShaContentEncryptionProvider; import org.keycloak.crypto.AesGcmContentEncryptionProvider; import org.keycloak.crypto.Algorithm; import org.keycloak.crypto.RsaCekManagementProvider; +import org.keycloak.jose.JOSEHeader; import org.keycloak.jose.jwe.JWEConstants; import org.keycloak.jose.jwe.JWEException; +import org.keycloak.jose.jwe.JWEHeader; import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider; import org.keycloak.jose.jwe.enc.JWEEncryptionProvider; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; @@ -52,8 +55,10 @@ import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.util.TokenSignatureUtil; +import org.keycloak.util.JsonSerialization; import org.keycloak.util.TokenUtil; +import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.PrivateKey; import java.util.List; @@ -220,6 +225,10 @@ public class IdTokenEncryptionTest extends AbstractTestRealmKeycloakTest { Map keyPair = oidcClientEndpointsResource.getKeysAsPem(); PrivateKey decryptionKEK = PemUtils.decodePrivateKey(keyPair.get("privateKey")); + // a nested JWT (signed and encrypted JWT) needs to set "JWT" to its JOSE Header's "cty" field + JWEHeader jweHeader = (JWEHeader) getHeader(parts[0]); + Assert.assertEquals("JWT", jweHeader.getContentType()); + // verify and decrypt JWE JWEAlgorithmProvider algorithmProvider = getJweAlgorithmProvider(algAlgorithm); JWEEncryptionProvider encryptionProvider = getJweEncryptionProvider(encAlgorithm); @@ -271,6 +280,15 @@ public class IdTokenEncryptionTest extends AbstractTestRealmKeycloakTest { return jweEncryptionProvider; } + private JOSEHeader getHeader(String base64Header) { + try { + byte[] decodedHeader = Base64Url.decode(base64Header); + return JsonSerialization.readValue(decodedHeader, JWEHeader.class); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + @Test @UncaughtServerErrorExpected public void testIdTokenEncryptionWithoutEncryptionKEK() {