From 8a930f73a80b0f5bf28647d78da987073236b9a0 Mon Sep 17 00:00:00 2001 From: Farah Juma Date: Mon, 12 Jun 2023 16:59:04 -0400 Subject: [PATCH] [ELY-2564] Add the ability to disable OIDC access token typ claim validation via a system property --- http/oidc/pom.xml | 20 +++ .../org/wildfly/security/http/oidc/Oidc.java | 1 + .../security/http/oidc/TokenValidator.java | 18 ++- .../http/oidc/TypClaimValidationBaseTest.java | 116 ++++++++++++++++++ .../oidc/TypClaimValidationDisabledTest.java | 60 +++++++++ .../oidc/TypClaimValidationEnabledTest.java | 50 ++++++++ 6 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationBaseTest.java create mode 100644 http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationDisabledTest.java create mode 100644 http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationEnabledTest.java diff --git a/http/oidc/pom.xml b/http/oidc/pom.xml index 1ec97179c85..568abc525ab 100644 --- a/http/oidc/pom.xml +++ b/http/oidc/pom.xml @@ -133,6 +133,11 @@ slf4j-jboss-logmanager test + + net.minidev + json-smart + test + net.sourceforge.htmlunit htmlunit @@ -148,6 +153,21 @@ mockwebserver test + + com.nimbusds + nimbus-jose-jwt + test + + + org.glassfish + javax.json + test + + + org.jmockit + jmockit + test + diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java index 1f5925df1ef..78e1fc7c1bc 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/Oidc.java @@ -102,6 +102,7 @@ public class Oidc { static final String KEYCLOAK_CLIENT_CLUSTER_HOST = "client_cluster_host"; static final String KEYCLOAK_QUERY_BEARER_TOKEN = "k_query_bearer_token"; static final String DEFAULT_TOKEN_SIGNATURE_ALGORITHM = "RS256"; + public static final String DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME = "wildfly.elytron.oidc.disable.typ.claim.validation"; // keycloak-specific request parameter used to specify the identifier of the identity provider that should be used to authenticate a user diff --git a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java index a549331b842..e1feae3f140 100644 --- a/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java +++ b/http/oidc/src/main/java/org/wildfly/security/http/oidc/TokenValidator.java @@ -20,6 +20,7 @@ import static org.wildfly.security.http.oidc.ElytronMessages.log; import static org.wildfly.security.http.oidc.IDToken.AT_HASH; +import static org.wildfly.security.http.oidc.Oidc.DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME; import static org.wildfly.security.http.oidc.Oidc.INVALID_AT_HASH_CLAIM; import static org.wildfly.security.http.oidc.Oidc.INVALID_ISSUED_FOR_CLAIM; import static org.wildfly.security.http.oidc.Oidc.INVALID_TYPE_CLAIM; @@ -27,8 +28,10 @@ import static org.wildfly.security.jose.jwk.JWKUtil.BASE64_URL; import java.nio.charset.StandardCharsets; +import java.security.AccessController; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PrivilegedAction; import java.util.Arrays; import javax.crypto.SecretKey; @@ -52,6 +55,17 @@ */ public class TokenValidator { + static final boolean DISABLE_TYP_CLAIM_VALIDATION_PROPERTY; + + static { + DISABLE_TYP_CLAIM_VALIDATION_PROPERTY = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Boolean run() { + return Boolean.parseBoolean(System.getProperty(DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME, "false")); + } + }); + } + private static final int HEADER_INDEX = 0; private JwtConsumerBuilder jwtConsumerBuilder; private OidcClientConfiguration clientConfiguration; @@ -98,7 +112,9 @@ public AccessToken parseAndVerifyToken(final String bearerToken) throws OidcExce try { JwtContext jwtContext = setVerificationKey(bearerToken, jwtConsumerBuilder); jwtConsumerBuilder.setRequireSubject(); - jwtConsumerBuilder.registerValidator(new TypeValidator("Bearer")); + if (! DISABLE_TYP_CLAIM_VALIDATION_PROPERTY) { + jwtConsumerBuilder.registerValidator(new TypeValidator("Bearer")); + } if (clientConfiguration.isVerifyTokenAudience()) { jwtConsumerBuilder.setExpectedAudience(clientConfiguration.getResourceName()); } else { diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationBaseTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationBaseTest.java new file mode 100644 index 00000000000..22ffea0cd54 --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationBaseTest.java @@ -0,0 +1,116 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.security.http.oidc; + +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.RSASSASigner; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; + +import javax.json.Json; +import javax.json.JsonObjectBuilder; + +import mockit.Mock; +import mockit.MockUp; + +/** + * Base test class for typ claim validation tests. + * + * @author Farah Juma + */ +public class TypClaimValidationBaseTest { + + public static String ISSUER_URL = "http://localhost:8080/realms/myrealm"; + public static String SUBJECT = "bf8ce366-0a74-4628-bd9a-1e69084ae558"; + + /** + * The issuerUrl gets set using OIDC discovery. Since this test class isn't + * making use of the Keycloak OpenID provider, we are mocking the return + * value for the issuer URL. + * + * @param issuerUrl + */ + protected static void mockIssuerUrl(String issuerUrl) { + Class classToMock; + try { + classToMock = Class.forName("org.wildfly.security.http.oidc.OidcClientConfiguration", + true, OidcClientConfiguration.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new NoClassDefFoundError(e.getMessage()); + } + new MockUp(classToMock) { + @Mock + public String getIssuerUrl() { + return issuerUrl; + } + }; + } + + protected static AccessToken testTokenValidationWithoutTypClaim() throws Exception { + KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); + HardcodedPublicKeyLocator hardcodedPublicKeyLocator = new HardcodedPublicKeyLocator(keyPair.getPublic()); + + OidcClientConfiguration clientConfiguration = new OidcClientConfiguration(); + clientConfiguration.setClientId("clientWithoutTyp"); + clientConfiguration.setPublicKeyLocator(hardcodedPublicKeyLocator); + clientConfiguration.setProviderUrl(ISSUER_URL); + clientConfiguration.setPublicClient(true); + clientConfiguration.setPrincipalAttribute("preferred_username"); + clientConfiguration.setSSLRequired(Oidc.SSLRequired.EXTERNAL); + + TokenValidator tokenValidator = TokenValidator.builder(clientConfiguration).build(); + return tokenValidator.parseAndVerifyToken(createJwt(keyPair, 60, "1")); + } + + private static String createJwt(KeyPair keyPair, int expirationOffset, String kid) throws Exception { + PrivateKey privateKey = keyPair.getPrivate(); + JWSSigner signer = new RSASSASigner(privateKey); + JsonObjectBuilder claimsBuilder = createClaims(expirationOffset); + + JWSHeader.Builder headerBuilder = new JWSHeader.Builder(JWSAlgorithm.RS256) + .type(new JOSEObjectType("jwt")); + if (kid != null) { + headerBuilder.keyID(kid); + } + + JWSObject jwsObject = new JWSObject(headerBuilder.build(), new Payload(claimsBuilder.build().toString())); + jwsObject.sign(signer); + return jwsObject.serialize(); + } + + private static JsonObjectBuilder createClaims(int expirationOffset) { + // typ claim not included + return Json.createObjectBuilder() + .add("sub", SUBJECT) + .add("iss", ISSUER_URL) + .add("aud", "account") + .add("exp", (System.currentTimeMillis() / 1000) + expirationOffset) + .add("azp", "app") + .add("scope", "profile email") + .add("preferred_username", "alice"); + } + +} diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationDisabledTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationDisabledTest.java new file mode 100644 index 00000000000..034e10c462a --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationDisabledTest.java @@ -0,0 +1,60 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.security.http.oidc; + +import static org.junit.Assert.assertEquals; +import static org.wildfly.common.Assert.assertNotNull; +import static org.wildfly.security.http.oidc.Oidc.DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests for disabling typ claim validation. + * + * @author Farah Juma + */ +public class TypClaimValidationDisabledTest extends TypClaimValidationBaseTest { + + private static String DISABLE_TYP_CLAIM_VALIDATION_PROPERTY; + + @BeforeClass + public static void setUp() { + mockIssuerUrl(ISSUER_URL); + DISABLE_TYP_CLAIM_VALIDATION_PROPERTY = System.setProperty(DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME, "true"); + } + + @AfterClass + public static void tearDown() { + if (DISABLE_TYP_CLAIM_VALIDATION_PROPERTY == null) { + System.clearProperty(DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME); + } else { + System.setProperty(DISABLE_TYP_CLAIM_VALIDATION_PROPERTY_NAME, DISABLE_TYP_CLAIM_VALIDATION_PROPERTY); + } + } + + @Test + public void testTokenWithoutTypClaimWithTypClaimValidationDisabled() throws Exception { + AccessToken accessToken = testTokenValidationWithoutTypClaim(); + assertNotNull(accessToken); + assertEquals(ISSUER_URL, accessToken.getIssuer()); + assertEquals("bf8ce366-0a74-4628-bd9a-1e69084ae558", accessToken.getSubject()); + } +} diff --git a/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationEnabledTest.java b/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationEnabledTest.java new file mode 100644 index 00000000000..5590193eed3 --- /dev/null +++ b/http/oidc/src/test/java/org/wildfly/security/http/oidc/TypClaimValidationEnabledTest.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2023 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.wildfly.security.http.oidc; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Tests for typ claim validation. + * + * @author Farah Juma + */ +public class TypClaimValidationEnabledTest extends TypClaimValidationBaseTest { + + private static String ISSUER_URL = "http://localhost:8080/realms/myrealm"; + + @BeforeClass + public static void setUp() { + mockIssuerUrl(ISSUER_URL); + } + + @Test + public void testTokenWithoutTypClaimWithTypClaimValidationEnabled() throws Exception { + try { + testTokenValidationWithoutTypClaim(); + fail("Expected exception not thrown"); + } catch (OidcException e) { + assertTrue(e.getMessage().contains("Invalid bearer token")); + } + } +}