From 1a53bc24d5804c826a06c9577771e487f9462cc6 Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Wed, 29 Jul 2026 16:49:27 +0200 Subject: [PATCH 1/8] Support array audience claim as specified by RFC-7519. --- CHANGELOG.md | 2 + .../io/curity/oauth/AbstractJwtValidator.java | 28 ++++---- src/main/java/io/curity/oauth/JsonUtils.java | 34 +++++++++- .../io/curity/oauth/JwtValidatorWithCert.java | 20 ++++-- .../io/curity/oauth/JwtValidatorWithJwk.java | 13 +++- .../io/curity/oauth/JwtAudienceClaimTest.java | 57 +++++++++++++++++ .../java/io/curity/oauth/JwtWithCertTest.java | 45 ++----------- .../java/io/curity/oauth/JwtWithJwksTest.java | 49 ++------------ .../io/curity/oauth/TestKeyStoreHelper.java | 64 +++++++++++++++++++ 9 files changed, 208 insertions(+), 104 deletions(-) create mode 100644 src/test/java/io/curity/oauth/JwtAudienceClaimTest.java create mode 100644 src/test/java/io/curity/oauth/TestKeyStoreHelper.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 847a930..1babf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Support `aud` claim when it's specified as an array rather than a string. + ## 4.0.0 (2023-11-27) - Added support for EdDSA signatures. diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index 959f7bb..4ba7165 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2016 Curity AB. - * + * * 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. @@ -30,6 +30,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,16 +43,21 @@ abstract class AbstractJwtValidator implements JwtValidator private final Map _decodedJwtBodyByEncodedBody = new HashMap<>(1); private final Map _decodedJwtHeaderByEncodedHeader = new HashMap<>(1); private final JsonReaderFactory _jsonReaderFactory; - private final String _audience; + private final Set _audiences; private final String _issuer; - AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory) + protected AbstractJwtValidator(String issuer, Set audiences, JsonReaderFactory jsonReaderFactory) { _issuer = issuer; - _audience = audience; + _audiences = Set.copyOf(audiences); _jsonReaderFactory = jsonReaderFactory; } + protected AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory) + { + this(issuer, Set.of(audience), jsonReaderFactory); + } + public final JsonData validate(String jwt) throws TokenValidationException { String[] jwtParts = jwt.split("\\."); @@ -73,15 +79,15 @@ public final JsonData validate(String jwt) throws TokenValidationException long exp = JsonUtils.getLong(jwtBody, "exp"); long iat = JsonUtils.getLong(jwtBody, "iat"); - String aud = JsonUtils.getString(jwtBody, "aud"); + Set aud = JsonUtils.getStringOrStrings(jwtBody, "aud"); String iss = JsonUtils.getString(jwtBody, "iss"); - assert aud != null && aud.length() > 0 : "aud claim is not present in JWT"; - assert iss != null && iss.length() > 0 : "iss claim is not present in JWT"; + assert !aud.isEmpty() : "aud claim is not present or is invalid in JWT"; + assert iss != null && !iss.isEmpty() : "iss claim is not present in JWT"; - if (!aud.equals(_audience)) + if (aud.stream().noneMatch(_audiences::contains)) { - throw new InvalidAudienceException(_audience, aud); + throw new InvalidAudienceException(String.join(", ", _audiences), String.join(", ", aud)); } if (!iss.equals(_issuer)) diff --git a/src/main/java/io/curity/oauth/JsonUtils.java b/src/main/java/io/curity/oauth/JsonUtils.java index 800bce3..9819061 100644 --- a/src/main/java/io/curity/oauth/JsonUtils.java +++ b/src/main/java/io/curity/oauth/JsonUtils.java @@ -16,6 +16,7 @@ package io.curity.oauth; +import javax.json.JsonArray; import javax.json.JsonNumber; import javax.json.JsonObject; import javax.json.JsonReaderFactory; @@ -27,6 +28,7 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; final class JsonUtils { @@ -53,10 +55,18 @@ static String getString(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) .filter(it -> it.getValueType() == JsonValue.ValueType.STRING) - .map(it -> ((JsonString) it).getString()) + .map(it -> stringValue((JsonString) it)) .orElse(null); } + static Set getStringOrStrings(JsonObject jsonObject, String name) + { + return Optional.ofNullable(jsonObject.get(name)) + .filter(it -> it.getValueType() == JsonValue.ValueType.STRING || it.getValueType() == JsonValue.ValueType.ARRAY) + .map(JsonUtils::stringOrStringArrayValue) + .orElse(Collections.emptySet()); + } + static long getLong(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) @@ -64,4 +74,26 @@ static long getLong(JsonObject jsonObject, String name) .map(it -> ((JsonNumber) it).longValue()) .orElse(Long.MIN_VALUE); } + + private static String stringValue(JsonValue value) + { + return ((JsonString) value).getString(); + } + + private static Set stringOrStringArrayValue(JsonValue value) + { + if (value.getValueType() == JsonValue.ValueType.STRING) + { + return Set.of(stringValue(value)); + } + if (value.getValueType() == JsonValue.ValueType.ARRAY) + { + return ((JsonArray) value).stream() + .map(JsonUtils::stringValue) + .collect(Collectors.toSet()); + } + + // be extremely lenient and ignore the value + return Set.of(); + } } diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java index 35db2b8..ef44033 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java @@ -20,6 +20,7 @@ import java.security.PublicKey; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Logger; final class JwtValidatorWithCert extends AbstractJwtValidator @@ -28,19 +29,30 @@ final class JwtValidatorWithCert extends AbstractJwtValidator private final Map _keys; + JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys) + { + this(issuer, audiences, publicKeys, JsonUtils.createDefaultReaderFactory()); + } + JwtValidatorWithCert(String issuer, String audience, Map publicKeys) { - this(issuer, audience, publicKeys, JsonUtils.createDefaultReaderFactory()); + this(issuer, Set.of(audience), publicKeys); } - JwtValidatorWithCert(String issuer, String audience, Map publicKeys, + JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys, JsonReaderFactory jsonReaderFactory) { - super(issuer, audience, jsonReaderFactory); - + super(issuer, audiences, jsonReaderFactory); + _keys = publicKeys; } + JwtValidatorWithCert(String issuer, String audience, Map publicKeys, + JsonReaderFactory jsonReaderFactory) + { + this(issuer, Set.of(audience), publicKeys, jsonReaderFactory); + } + @Override protected Optional getPublicKey(JwtHeader jwtHeader) { diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java index 8260a6a..b494299 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java @@ -22,6 +22,7 @@ import java.security.PublicKey; import java.security.spec.InvalidKeySpecException; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -31,14 +32,20 @@ final class JwtValidatorWithJwk extends AbstractJwtValidator private final JwkManager _jwkManager; - JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, Set audiences, String issuer, JsonReaderFactory jsonReaderFactory) { - super(issuer, audience, jsonReaderFactory); - + super(issuer, audiences, jsonReaderFactory); + _jwkManager = new JwkManager(minKidReloadTime, webKeysClient, jsonReaderFactory); } + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, + JsonReaderFactory jsonReaderFactory) + { + this(minKidReloadTime, webKeysClient, Set.of(audience), issuer, jsonReaderFactory); + } + @Override protected Optional getPublicKey(JwtHeader jwtHeader) { diff --git a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java new file mode 100644 index 0000000..0f2d4fb --- /dev/null +++ b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 Curity AB. + * + * 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 io.curity.oauth; + +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.Test; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.Map; + +import static junit.framework.TestCase.assertNotNull; + +public class JwtAudienceClaimTest +{ + private static final String ISSUER = "test:issuer"; + private static final String AUDIENCE = "foo:audience"; + private static final String KEY_ALIAS = "se.curity.test"; + + @Test + public void testAudienceClaimAsJsonArray() throws Exception + { + KeyStore keyStore = TestKeyStoreHelper.loadKeyStore(); + PrivateKey privateKey = TestKeyStoreHelper.getPrivateKey(keyStore, KEY_ALIAS); + Certificate cert = TestKeyStoreHelper.getCertificate(keyStore, KEY_ALIAS); + + JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, "RS256", privateKey, cert); + String tokenWithArrayAudience = issuer.issueToken("testsubject", AUDIENCE + " other:audience", 200, + Collections.emptyMap()); + + byte[] x5tS256 = DigestUtils.sha256(cert.getEncoded()); + String b64x5tS256 = org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(x5tS256); + Map keys = Map.of(b64x5tS256, cert.getPublicKey()); + + JwtValidator validator = new JwtValidatorWithCert(ISSUER, AUDIENCE, keys); + JsonData result = validator.validate(tokenWithArrayAudience); + + assertNotNull(result); + } +} diff --git a/src/test/java/io/curity/oauth/JwtWithCertTest.java b/src/test/java/io/curity/oauth/JwtWithCertTest.java index 60004f2..79e1f1f 100644 --- a/src/test/java/io/curity/oauth/JwtWithCertTest.java +++ b/src/test/java/io/curity/oauth/JwtWithCertTest.java @@ -26,12 +26,7 @@ import javax.json.JsonObject; import javax.json.JsonString; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.net.URL; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; @@ -55,9 +50,6 @@ public class JwtWithCertTest private final String EXTRA_CLAIM = "TEST_KEY"; private final String EXTRA_CLAIM_VALUE = "TEST_VALUE"; - private final String PATH_TO_KEY = "/Se.Curity.Test.p12"; - private final String KEY_PWD = "Password1"; - private String _testToken; private KeyStore _keyStore; @@ -75,10 +67,10 @@ public static Object[] keysToTest() { @Before public void before() throws Exception { - loadKeyStore(); + _keyStore = TestKeyStoreHelper.loadKeyStore(); - PrivateKey key = getPrivateKey(); - Certificate cert = getCertificate(); + PrivateKey key = TestKeyStoreHelper.getPrivateKey(_keyStore, _keyAlias); + Certificate cert = TestKeyStoreHelper.getCertificate(_keyStore, _keyAlias); if (!_algorithm.equals("EdDSA")) { // Create test token on the fly @@ -139,7 +131,7 @@ private Map prepareKeyMap() throws Exception { Map keys = new HashMap<>(); - Certificate cert = getCertificate(); + Certificate cert = TestKeyStoreHelper.getCertificate(_keyStore, _keyAlias); PublicKey key = cert.getPublicKey(); @@ -150,33 +142,4 @@ private Map prepareKeyMap() throws Exception return keys; } - - private void loadKeyStore() - throws Exception - { - URL url = getClass().getResource(PATH_TO_KEY); - assert url != null; - File certFile = new File(url.getFile()); - - InputStream keyIS = new FileInputStream(certFile); - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(keyIS, KEY_PWD.toCharArray()); - - keyIS.close(); - - this._keyStore=keyStore; - } - - private PrivateKey getPrivateKey() - throws Exception - { - return (PrivateKey)this._keyStore.getKey(_keyAlias, KEY_PWD.toCharArray()); - - } - - private Certificate getCertificate() throws KeyStoreException { - //Get key by alias (found in the p12 file using: - //keytool -list -keystore test-root-ca.p12 -storepass foobar -storetype PKCS12 - return this._keyStore.getCertificate(_keyAlias); - } } diff --git a/src/test/java/io/curity/oauth/JwtWithJwksTest.java b/src/test/java/io/curity/oauth/JwtWithJwksTest.java index 5e59676..ca48cd9 100644 --- a/src/test/java/io/curity/oauth/JwtWithJwksTest.java +++ b/src/test/java/io/curity/oauth/JwtWithJwksTest.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2016 Curity AB. - * + * * 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. @@ -27,14 +27,8 @@ import javax.json.JsonReaderFactory; import javax.json.JsonString; import javax.json.spi.JsonProvider; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.net.URL; import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.PrivateKey; -import java.security.cert.Certificate; import java.security.interfaces.EdECPrivateKey; import java.util.Collections; import java.util.HashMap; @@ -58,10 +52,6 @@ public class JwtWithJwksTest private final String EXTRA_CLAIM = "TEST_KEY"; private final String EXTRA_CLAIM_VALUE = "TEST_VALUE"; - // Used for signing tokens - private final String PATH_TO_KEY = "/Se.Curity.Test.p12"; - private final String KEY_PWD = "Password1"; - private String _testToken; private KeyStore _keyStore; @@ -82,9 +72,9 @@ public static Object[] keysToTest() { @Before public void before() throws Exception { - loadKeyStore(); + _keyStore = TestKeyStoreHelper.loadKeyStore(); - PrivateKey key = getPrivateKey(); + PrivateKey key = TestKeyStoreHelper.getPrivateKey(_keyStore, _keyAlias); if (!_algorithm.equals("EdDSA")) { // Create test token on the fly @@ -157,33 +147,4 @@ private Map prepareKeyMap() keys.put("1716999904","{\"keys\":[{\"kty\":\"OKP\",\"kid\":\"1716999904\",\"use\":\"sig\",\"alg\":\"EdDSA\",\"crv\":\"Ed448\",\"x\":\"lDc565Rydl9MUCoOB9JpGV3pUSHm7FvuiuEMvrvRkS7PeYL41rPU6s2rMdLeHiXfSxvR1veh4C0A\",\"x5t\":\"1IRTBLLQeiL2YZLB1VDDvCTGozc\"}]}"); return keys; } - - private void loadKeyStore() - throws Exception - { - URL url = getClass().getResource(PATH_TO_KEY); - assert url != null; - File certFile = new File(url.getFile()); - - InputStream keyIS = new FileInputStream(certFile); - KeyStore keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(keyIS, KEY_PWD.toCharArray()); - - keyIS.close(); - - this._keyStore=keyStore; - } - - private PrivateKey getPrivateKey() - throws Exception - { - return (PrivateKey)this._keyStore.getKey(_keyAlias, KEY_PWD.toCharArray()); - - } - - private Certificate getCertificate() throws KeyStoreException { - //Get key by alias (found in the p12 file using: - //keytool -list -keystore test-root-ca.p12 -storepass foobar -storetype PKCS12 - return this._keyStore.getCertificate(_keyAlias); - } } diff --git a/src/test/java/io/curity/oauth/TestKeyStoreHelper.java b/src/test/java/io/curity/oauth/TestKeyStoreHelper.java new file mode 100644 index 0000000..aefb952 --- /dev/null +++ b/src/test/java/io/curity/oauth/TestKeyStoreHelper.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2016 Curity AB. + * + * 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 io.curity.oauth; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; + +final class TestKeyStoreHelper +{ + private static final String PATH_TO_KEY = "/Se.Curity.Test.p12"; + private static final String KEY_PWD = "Password1"; + + private TestKeyStoreHelper() + { + } + + static KeyStore loadKeyStore() throws Exception + { + return loadKeyStore(TestKeyStoreHelper.class, PATH_TO_KEY); + } + + static KeyStore loadKeyStore(Class testClass, String keyPath) throws Exception + { + URL url = testClass.getResource(keyPath); + assert url != null; + File certFile = new File(url.getFile()); + + try (InputStream keyIS = new FileInputStream(certFile)) + { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(keyIS, KEY_PWD.toCharArray()); + return keyStore; + } + } + + static PrivateKey getPrivateKey(KeyStore keyStore, String keyAlias) throws Exception + { + return (PrivateKey) keyStore.getKey(keyAlias, KEY_PWD.toCharArray()); + } + + static Certificate getCertificate(KeyStore keyStore, String keyAlias) throws Exception + { + return keyStore.getCertificate(keyAlias); + } +} From dde8b6765d148ee8b96c35d0da1d64cc1b1c534b Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Wed, 29 Jul 2026 17:22:32 +0200 Subject: [PATCH 2/8] Improved tests for audience claim. --- .../io/curity/oauth/AbstractJwtValidator.java | 2 +- .../io/curity/oauth/JwtValidatorWithCert.java | 2 - .../io/curity/oauth/JwtAudienceClaimTest.java | 113 ++++++++++++++++-- .../java/io/curity/oauth/JwtTokenIssuer.java | 33 ++--- 4 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index 4ba7165..8665a37 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -236,7 +236,7 @@ private JwtHeader decodeJwtHeader(String header) }); } - class JwtHeader + static final class JwtHeader { private final JsonObject _jsonObject; diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java index ef44033..de4e7dd 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java @@ -21,11 +21,9 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.logging.Logger; final class JwtValidatorWithCert extends AbstractJwtValidator { - private static final Logger _logger = Logger.getLogger(JwtValidatorWithCert.class.getName()); private final Map _keys; diff --git a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java index 0f2d4fb..dca6af8 100644 --- a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java +++ b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java @@ -24,34 +24,133 @@ import java.security.PublicKey; import java.security.cert.Certificate; import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Function; import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertThrows; public class JwtAudienceClaimTest { private static final String ISSUER = "test:issuer"; - private static final String AUDIENCE = "foo:audience"; private static final String KEY_ALIAS = "se.curity.test"; @Test - public void testAudienceClaimAsJsonArray() throws Exception + public void testValidatorAcceptsOneAudienceAndTokenHasValidAudienceString() throws Exception + { + assertTokenValidation("some:audience", + keys -> new JwtValidatorWithCert(ISSUER, "some:audience", keys), true); + } + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasValidAudienceArray() throws Exception + { + assertTokenValidation(List.of("some:audience", "other:audience"), + keys -> new JwtValidatorWithCert(ISSUER, "some:audience", keys), true); + } + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasInvalidAudienceString() throws Exception + { + assertTokenValidation("some:audience", + keys -> new JwtValidatorWithCert(ISSUER, "other:audience", keys), false); + } + + @Test + public void testValidatorAcceptsOneAudienceAndTokenHasOnlyInvalidAudiencesInArray() throws Exception + { + assertTokenValidation(List.of("invalid:audience", "other:invalid:audience"), + keys -> new JwtValidatorWithCert(ISSUER, "valid:audience", keys), false); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasValidAudienceString() throws Exception + { + assertTokenValidation("valid:audience", + keys -> new JwtValidatorWithCert(ISSUER, Set.of("valid:audience", "something:else"), keys), true); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasValidAudiencesArray() throws Exception + { + assertTokenValidation(List.of("x", "y", "a"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), true); + + assertTokenValidation(List.of("x", "b", "y"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), true); + + assertTokenValidation(List.of("a", "b"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), true); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasInvalidAudienceString() throws Exception + { + assertTokenValidation("invalid:audience", + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), false); + } + + @Test + public void testValidatorAcceptsManyAudiencesAndTokenHasOnlyInvalidAudiencesArray() throws Exception + { + assertTokenValidation(List.of("invalid:audience", "other:audience"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), false); + } + + private static void assertTokenValidation(String tokenAudience, + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception + { + // the token is issued with an array of audiences if there's more than one audience, + // or with a string audience if there's only one. + assertTokenValidation(List.of(tokenAudience), validatorFactory, shouldBeValid); + } + + private static void assertTokenValidation(List tokenAudiences, + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception + { + TokenFixture fixture = createTokenFixture(tokenAudiences); + try (JwtValidator validator = validatorFactory.apply(fixture.keys)) + { + if (shouldBeValid) + { + JsonData result = validator.validate(fixture.token); + assertNotNull(result); + } + else + { + assertThrows(TokenValidationException.class, () -> validator.validate(fixture.token)); + } + } + } + + private static TokenFixture createTokenFixture(List tokenAudiences) throws Exception { KeyStore keyStore = TestKeyStoreHelper.loadKeyStore(); PrivateKey privateKey = TestKeyStoreHelper.getPrivateKey(keyStore, KEY_ALIAS); Certificate cert = TestKeyStoreHelper.getCertificate(keyStore, KEY_ALIAS); JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, "RS256", privateKey, cert); - String tokenWithArrayAudience = issuer.issueToken("testsubject", AUDIENCE + " other:audience", 200, - Collections.emptyMap()); + String tokenWithArrayAudience = issuer.issueToken("testsubject", tokenAudiences, 200, Collections.emptyMap()); byte[] x5tS256 = DigestUtils.sha256(cert.getEncoded()); String b64x5tS256 = org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(x5tS256); Map keys = Map.of(b64x5tS256, cert.getPublicKey()); + return new TokenFixture(tokenWithArrayAudience, keys); + } - JwtValidator validator = new JwtValidatorWithCert(ISSUER, AUDIENCE, keys); - JsonData result = validator.validate(tokenWithArrayAudience); + private static class TokenFixture + { + private final String token; + private final Map keys; - assertNotNull(result); + private TokenFixture(String token, Map keys) + { + this.token = token; + this.keys = keys; + } } } diff --git a/src/test/java/io/curity/oauth/JwtTokenIssuer.java b/src/test/java/io/curity/oauth/JwtTokenIssuer.java index 4b6443f..db7b76a 100644 --- a/src/test/java/io/curity/oauth/JwtTokenIssuer.java +++ b/src/test/java/io/curity/oauth/JwtTokenIssuer.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2016 Curity AB. - * + * * 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. @@ -29,15 +29,14 @@ import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; import javax.json.JsonWriter; -import java.security.PrivateKey; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; import java.io.StringWriter; import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.time.Instant; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -99,13 +98,10 @@ private JwtTokenIssuer(String issuer, int skewTolerance, String algorithm, Priva String issueToken(String subject, String audience, int lifetimeInMinutes, Map attributes) throws Exception { - String[] audiences = stringToArray(audience); - - return issueToken(subject, Arrays.asList(audiences), lifetimeInMinutes, attributes); + return issueToken(subject, List.of(audience), lifetimeInMinutes, attributes); } - private String issueToken(String subject, List audiences, int lifetimeInMinutes, Map - attributes) + String issueToken(String subject, List audiences, int lifetimeInMinutes, Map attributes) throws Exception { Map claims = new LinkedHashMap<>(); @@ -197,7 +193,7 @@ else if (this._keyId != null) _logger.trace(message); } - + _logger.trace(serializedToken); } @@ -211,20 +207,13 @@ else if (this._keyId != null) } } - private String[] stringToArray(String str) - { - String[] ret; - ret = str.split(" "); - return ret; - } - - private Object arrayOrString(List data) + private Object arrayOrString(List data) { if (data.size() == 1) { return data.get(0); } - + return data; } From 4f62ece955d6ea14a5f1d381fbc75b2d1f0f6321 Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Wed, 29 Jul 2026 17:33:20 +0200 Subject: [PATCH 3/8] Support for allowing multiple issuers. --- CHANGELOG.md | 3 +- .../io/curity/oauth/AbstractJwtValidator.java | 22 ++++-- .../io/curity/oauth/JwtValidatorWithCert.java | 32 ++++++-- .../io/curity/oauth/JwtValidatorWithJwk.java | 18 ++++- .../io/curity/oauth/ValidationExceptions.java | 2 +- .../io/curity/oauth/JwtAudienceClaimTest.java | 35 +-------- .../io/curity/oauth/JwtIssuerClaimTest.java | 77 +++++++++++++++++++ .../curity/oauth/JwtTokenFixtureHelper.java | 66 ++++++++++++++++ 8 files changed, 205 insertions(+), 50 deletions(-) create mode 100644 src/test/java/io/curity/oauth/JwtIssuerClaimTest.java create mode 100644 src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 1babf34..49db588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Unreleased -- Support `aud` claim when it's specified as an array rather than a string. +- Support `aud` claim in JWT when it's specified as an array rather than a string. +- Allow JWT validators to configure a Set of accepted audiences, not just one. ## 4.0.0 (2023-11-27) diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index 8665a37..af950ab 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -44,18 +44,28 @@ abstract class AbstractJwtValidator implements JwtValidator private final Map _decodedJwtHeaderByEncodedHeader = new HashMap<>(1); private final JsonReaderFactory _jsonReaderFactory; private final Set _audiences; - private final String _issuer; + private final Set _issuers; - protected AbstractJwtValidator(String issuer, Set audiences, JsonReaderFactory jsonReaderFactory) + protected AbstractJwtValidator(Set issuers, Set audiences, JsonReaderFactory jsonReaderFactory) { - _issuer = issuer; + _issuers = Set.copyOf(issuers); _audiences = Set.copyOf(audiences); _jsonReaderFactory = jsonReaderFactory; } protected AbstractJwtValidator(String issuer, String audience, JsonReaderFactory jsonReaderFactory) { - this(issuer, Set.of(audience), jsonReaderFactory); + this(Set.of(issuer), Set.of(audience), jsonReaderFactory); + } + + protected AbstractJwtValidator(String issuer, Set audiences, JsonReaderFactory jsonReaderFactory) + { + this(Set.of(issuer), audiences, jsonReaderFactory); + } + + protected AbstractJwtValidator(Set issuers, String audience, JsonReaderFactory jsonReaderFactory) + { + this(issuers, Set.of(audience), jsonReaderFactory); } public final JsonData validate(String jwt) throws TokenValidationException @@ -90,9 +100,9 @@ public final JsonData validate(String jwt) throws TokenValidationException throw new InvalidAudienceException(String.join(", ", _audiences), String.join(", ", aud)); } - if (!iss.equals(_issuer)) + if (!_issuers.contains(iss)) { - throw new InvalidIssuerException(_issuer, iss); + throw new InvalidIssuerException(String.join(", ", _issuers), iss); } Instant now = Instant.now(); diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java index de4e7dd..7a3a4e1 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithCert.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithCert.java @@ -27,28 +27,50 @@ final class JwtValidatorWithCert extends AbstractJwtValidator private final Map _keys; + JwtValidatorWithCert(Set issuers, Set audiences, Map publicKeys) + { + this(issuers, audiences, publicKeys, JsonUtils.createDefaultReaderFactory()); + } + JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys) { - this(issuer, audiences, publicKeys, JsonUtils.createDefaultReaderFactory()); + this(Set.of(issuer), audiences, publicKeys); + } + + JwtValidatorWithCert(Set issuers, String audience, Map publicKeys) + { + this(issuers, Set.of(audience), publicKeys); } JwtValidatorWithCert(String issuer, String audience, Map publicKeys) { - this(issuer, Set.of(audience), publicKeys); + this(Set.of(issuer), audience, publicKeys); } - JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys, + JwtValidatorWithCert(Set issuers, Set audiences, Map publicKeys, JsonReaderFactory jsonReaderFactory) { - super(issuer, audiences, jsonReaderFactory); + super(issuers, audiences, jsonReaderFactory); _keys = publicKeys; } + JwtValidatorWithCert(String issuer, Set audiences, Map publicKeys, + JsonReaderFactory jsonReaderFactory) + { + this(Set.of(issuer), audiences, publicKeys, jsonReaderFactory); + } + + JwtValidatorWithCert(Set issuers, String audience, Map publicKeys, + JsonReaderFactory jsonReaderFactory) + { + this(issuers, Set.of(audience), publicKeys, jsonReaderFactory); + } + JwtValidatorWithCert(String issuer, String audience, Map publicKeys, JsonReaderFactory jsonReaderFactory) { - this(issuer, Set.of(audience), publicKeys, jsonReaderFactory); + this(Set.of(issuer), audience, publicKeys, jsonReaderFactory); } @Override diff --git a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java index b494299..ca0fbc3 100644 --- a/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java +++ b/src/main/java/io/curity/oauth/JwtValidatorWithJwk.java @@ -32,18 +32,30 @@ final class JwtValidatorWithJwk extends AbstractJwtValidator private final JwkManager _jwkManager; - JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, Set audiences, String issuer, + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, Set audiences, Set issuers, JsonReaderFactory jsonReaderFactory) { - super(issuer, audiences, jsonReaderFactory); + super(issuers, audiences, jsonReaderFactory); _jwkManager = new JwkManager(minKidReloadTime, webKeysClient, jsonReaderFactory); } + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, Set audiences, String issuer, + JsonReaderFactory jsonReaderFactory) + { + this(minKidReloadTime, webKeysClient, audiences, Set.of(issuer), jsonReaderFactory); + } + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, String issuer, JsonReaderFactory jsonReaderFactory) { - this(minKidReloadTime, webKeysClient, Set.of(audience), issuer, jsonReaderFactory); + this(minKidReloadTime, webKeysClient, Set.of(audience), Set.of(issuer), jsonReaderFactory); + } + + JwtValidatorWithJwk(long minKidReloadTime, WebKeysClient webKeysClient, String audience, Set issuers, + JsonReaderFactory jsonReaderFactory) + { + this(minKidReloadTime, webKeysClient, Set.of(audience), issuers, jsonReaderFactory); } @Override diff --git a/src/main/java/io/curity/oauth/ValidationExceptions.java b/src/main/java/io/curity/oauth/ValidationExceptions.java index a406861..fc7ddfb 100644 --- a/src/main/java/io/curity/oauth/ValidationExceptions.java +++ b/src/main/java/io/curity/oauth/ValidationExceptions.java @@ -103,7 +103,7 @@ class UnknownAlgorithmException extends InvalidTokenFormatException class InvalidIssuerException extends TokenValidationException { - private static final String _formattedMessage = "Issuer %s does not match expected one %s"; + private static final String _formattedMessage = "Issuer %s does not match expected one of %s"; InvalidIssuerException(String expectedIssuer, String actualIssuer) { diff --git a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java index dca6af8..70d27ea 100644 --- a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java +++ b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java @@ -16,14 +16,9 @@ package io.curity.oauth; -import org.apache.commons.codec.digest.DigestUtils; import org.junit.Test; -import java.security.KeyStore; -import java.security.PrivateKey; import java.security.PublicKey; -import java.security.cert.Certificate; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -35,7 +30,6 @@ public class JwtAudienceClaimTest { private static final String ISSUER = "test:issuer"; - private static final String KEY_ALIAS = "se.curity.test"; @Test public void testValidatorAcceptsOneAudienceAndTokenHasValidAudienceString() throws Exception @@ -112,7 +106,7 @@ private static void assertTokenValidation(List tokenAudiences, Function, JwtValidator> validatorFactory, boolean shouldBeValid) throws Exception { - TokenFixture fixture = createTokenFixture(tokenAudiences); + JwtTokenFixtureHelper.TokenFixture fixture = JwtTokenFixtureHelper.createTokenFixture(ISSUER, tokenAudiences); try (JwtValidator validator = validatorFactory.apply(fixture.keys)) { if (shouldBeValid) @@ -126,31 +120,4 @@ private static void assertTokenValidation(List tokenAudiences, } } } - - private static TokenFixture createTokenFixture(List tokenAudiences) throws Exception - { - KeyStore keyStore = TestKeyStoreHelper.loadKeyStore(); - PrivateKey privateKey = TestKeyStoreHelper.getPrivateKey(keyStore, KEY_ALIAS); - Certificate cert = TestKeyStoreHelper.getCertificate(keyStore, KEY_ALIAS); - - JwtTokenIssuer issuer = new JwtTokenIssuer(ISSUER, "RS256", privateKey, cert); - String tokenWithArrayAudience = issuer.issueToken("testsubject", tokenAudiences, 200, Collections.emptyMap()); - - byte[] x5tS256 = DigestUtils.sha256(cert.getEncoded()); - String b64x5tS256 = org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(x5tS256); - Map keys = Map.of(b64x5tS256, cert.getPublicKey()); - return new TokenFixture(tokenWithArrayAudience, keys); - } - - private static class TokenFixture - { - private final String token; - private final Map keys; - - private TokenFixture(String token, Map keys) - { - this.token = token; - this.keys = keys; - } - } } diff --git a/src/test/java/io/curity/oauth/JwtIssuerClaimTest.java b/src/test/java/io/curity/oauth/JwtIssuerClaimTest.java new file mode 100644 index 0000000..13a8970 --- /dev/null +++ b/src/test/java/io/curity/oauth/JwtIssuerClaimTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 Curity AB. + * + * 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 io.curity.oauth; + +import org.junit.Test; + +import java.security.PublicKey; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertThrows; + +public class JwtIssuerClaimTest +{ + private static final String AUDIENCE = "test:audience"; + + @Test + public void testValidatorAcceptsOneIssuerAndTokenHasValidIssuer() throws Exception + { + assertTokenValidation("some:issuer", keys -> new JwtValidatorWithCert("some:issuer", AUDIENCE, keys), true); + } + + @Test + public void testValidatorAcceptsOneIssuerAndTokenHasInvalidIssuer() throws Exception + { + assertTokenValidation("other:issuer", keys -> new JwtValidatorWithCert("some:issuer", AUDIENCE, keys), false); + } + + @Test + public void testValidatorAcceptsManyIssuersAndTokenHasValidIssuer() throws Exception + { + assertTokenValidation("valid:issuer", + keys -> new JwtValidatorWithCert(Set.of("valid:issuer", "other:issuer"), AUDIENCE, keys), true); + } + + @Test + public void testValidatorAcceptsManyIssuersAndTokenHasInvalidIssuer() throws Exception + { + assertTokenValidation("invalid:issuer", + keys -> new JwtValidatorWithCert(Set.of("valid:issuer", "other:issuer"), AUDIENCE, keys), false); + } + + private static void assertTokenValidation(String tokenIssuer, + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception + { + JwtTokenFixtureHelper.TokenFixture fixture = JwtTokenFixtureHelper.createTokenFixture(tokenIssuer, AUDIENCE); + try (JwtValidator validator = validatorFactory.apply(fixture.keys)) + { + if (shouldBeValid) + { + JsonData result = validator.validate(fixture.token); + assertNotNull(result); + } + else + { + assertThrows(TokenValidationException.class, () -> validator.validate(fixture.token)); + } + } + } +} diff --git a/src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java b/src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java new file mode 100644 index 0000000..e128754 --- /dev/null +++ b/src/test/java/io/curity/oauth/JwtTokenFixtureHelper.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2016 Curity AB. + * + * 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 io.curity.oauth; + +import org.apache.commons.codec.digest.DigestUtils; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +final class JwtTokenFixtureHelper +{ + private static final String KEY_ALIAS = "se.curity.test"; + + private JwtTokenFixtureHelper() {} + + static TokenFixture createTokenFixture(String issuer, String audience) throws Exception + { + return createTokenFixture(issuer, List.of(audience)); + } + + static TokenFixture createTokenFixture(String issuer, List audiences) throws Exception + { + KeyStore keyStore = TestKeyStoreHelper.loadKeyStore(); + PrivateKey privateKey = TestKeyStoreHelper.getPrivateKey(keyStore, KEY_ALIAS); + Certificate cert = TestKeyStoreHelper.getCertificate(keyStore, KEY_ALIAS); + + JwtTokenIssuer tokenIssuer = new JwtTokenIssuer(issuer, "RS256", privateKey, cert); + String token = tokenIssuer.issueToken("testsubject", audiences, 200, Collections.emptyMap()); + + byte[] x5tS256 = DigestUtils.sha256(cert.getEncoded()); + String b64x5tS256 = org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString(x5tS256); + Map keys = Map.of(b64x5tS256, cert.getPublicKey()); + return new TokenFixture(token, keys); + } + + static final class TokenFixture + { + final String token; + final Map keys; + + private TokenFixture(String token, Map keys) + { + this.token = token; + this.keys = keys; + } + } +} From b1cb4d5a9469bb4fb8c99373cfd28e70b1d1bcf2 Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Wed, 29 Jul 2026 17:39:58 +0200 Subject: [PATCH 4/8] Support Java 15+. --- CHANGELOG.md | 1 + pom.xml | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49db588..d5053a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Support `aud` claim in JWT when it's specified as an array rather than a string. - Allow JWT validators to configure a Set of accepted audiences, not just one. +- Minimum Java version requirement changed to 15. ## 4.0.0 (2023-11-27) diff --git a/pom.xml b/pom.xml index ffdd5fa..70383f5 100644 --- a/pom.xml +++ b/pom.xml @@ -73,10 +73,11 @@ org.apache.maven.plugins maven-compiler-plugin - 3.1 + 3.8.1 - 11 - 11 + + 15 + 15 true false @@ -269,7 +270,7 @@ - 11 + 15 From 37cdd18e1488f6c2e130ba715d549f62c33c9d1a Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Thu, 30 Jul 2026 10:47:31 +0200 Subject: [PATCH 5/8] Made OAuth JWT filter config accept multiple issuers and audiences. Improved FilterHelper and added test for it. --- CHANGELOG.md | 3 +- readme.md | 32 +-- .../java/io/curity/oauth/FilterHelper.java | 23 +- src/main/java/io/curity/oauth/JsonUtils.java | 18 +- .../java/io/curity/oauth/OAuthFilter.java | 37 +-- .../java/io/curity/oauth/OAuthJwtFilter.java | 20 +- .../io/curity/oauth/FilterHelperTest.java | 230 ++++++++++++++++++ 7 files changed, 313 insertions(+), 50 deletions(-) create mode 100644 src/test/java/io/curity/oauth/FilterHelperTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d5053a0..911288d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## Unreleased - Support `aud` claim in JWT when it's specified as an array rather than a string. -- Allow JWT validators to configure a Set of accepted audiences, not just one. +- Allow JWT validators to configure a Set of accepted audiences and issuers, not just one. +- OAuthJwtFilter configuration now accepts whitespace-separated values for `audience` and `issuer`. - Minimum Java version requirement changed to 15. ## 4.0.0 (2023-11-27) diff --git a/readme.md b/readme.md index cc37db4..e340908 100644 --- a/readme.md +++ b/readme.md @@ -62,24 +62,26 @@ depending on the format of the token your OAuth server is using. ### Init-params for the `OAuthJwtFilter` -Configuration Setting Name | Description ----------------------------|---------------- -oauthHost | Hostname of the OAuth server. -oauthPort | Port of the OAuth server. -jsonWebKeysPath | Path to the JWKS endpoint on the OAuth server. -scope | A space separated list of scopes required to access the API. -minKidReloadTimeInSeconds | Minimum time to reload the webKeys cache used by the Filter. +| Configuration Setting Name | Description | +|----------------------------|----------------------------------------------------------------------------------------------| +| oauthHost | Hostname of the OAuth server. | +| oauthPort | Port of the OAuth server. | +| jsonWebKeysPath | Path to the JWKS endpoint on the OAuth server. | +| scope | A space separated list of scopes required to access the API. | +| minKidReloadTimeInSeconds | Minimum time to reload the webKeys cache used by the Filter. | +| issuer | A space separated list of issuers to accept. If not specified, all issuers are accepted. | +| audience | A space separated list of audiences to accept. If not specified, all audiences are accepted. | ### Init-params for the `OAuthOpaqueFilter` -Configuration Setting Name | Description ----------------------------|---------------- -oauthHost | Hostname of the OAuth server. -oauthPort | Port of the OAuth server. -introspectionPath | Path to the introspection endpoint on the OAuth server. -scope | A space separated list of scopes required to access the API. -clientId | Your application's client id to use for introspection. -clientSecret | Your application's client secret. +| Configuration Setting Name | Description | +|----------------------------|--------------------------------------------------------------| +| oauthHost | Hostname of the OAuth server. | +| oauthPort | Port of the OAuth server. | +| introspectionPath | Path to the introspection endpoint on the OAuth server. | +| scope | A space separated list of scopes required to access the API. | +| clientId | Your application's client id to use for introspection. | +| clientSecret | Your application's client secret. | ## Providing external services (`io.curity.oauth.HttpClientProvider` and `javax.json.spi.JsonProvider`) diff --git a/src/main/java/io/curity/oauth/FilterHelper.java b/src/main/java/io/curity/oauth/FilterHelper.java index 0beb82a..9c7ed4c 100644 --- a/src/main/java/io/curity/oauth/FilterHelper.java +++ b/src/main/java/io/curity/oauth/FilterHelper.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2016 Curity AB. - * + * * 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. @@ -18,8 +18,10 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.UnavailableException; + import java.util.Enumeration; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -67,15 +69,24 @@ static T getInitParamValue(String name, Map initParams, } static Optional getOptionalInitParamValue(String name, Map initParams, - Function converter) throws UnavailableException + Function converter) + throws UnavailableException { Optional value = getSingleValue(name, initParams); return value.flatMap(s -> Optional.ofNullable(converter.apply(s))); } - private static Optional getSingleValue(String name, Map initParams) throws - UnavailableException + static List getWhitespaceSeparatedInitParamValues(String name, Map initParams) + throws UnavailableException + { + Optional value = getSingleValue(name, initParams); + + return value.map(v -> List.of(v.split("\\s+"))).orElseGet(List::of); + } + + private static Optional getSingleValue(String name, Map initParams) + throws UnavailableException { return Optional.ofNullable(initParams.get(name)).map(Object::toString); } diff --git a/src/main/java/io/curity/oauth/JsonUtils.java b/src/main/java/io/curity/oauth/JsonUtils.java index 9819061..751fc2e 100644 --- a/src/main/java/io/curity/oauth/JsonUtils.java +++ b/src/main/java/io/curity/oauth/JsonUtils.java @@ -51,14 +51,30 @@ static Set getScopes(JsonObject jsonObject) return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(presentedScopes))); } + /** + * Get the string value of a JSON property. + * + * @param jsonObject The JSON object containing the property. + * @param name The name of the property. + * @return The string value of the property, or null if not present or not a string. + */ static String getString(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) .filter(it -> it.getValueType() == JsonValue.ValueType.STRING) - .map(it -> stringValue((JsonString) it)) + .map(JsonUtils::stringValue) .orElse(null); } + /** + * Get the string values of a JSON property. + * + * @param jsonObject The JSON object containing the property. + * @param name The name of the property. + * @return The string values of the property, or an empty set if not present or not a string or array. + * If the property is an array, it must only contain strings, otherwise this method will throw a + * {@link ClassCastException}. + */ static Set getStringOrStrings(JsonObject jsonObject, String name) { return Optional.ofNullable(jsonObject.get(name)) diff --git a/src/main/java/io/curity/oauth/OAuthFilter.java b/src/main/java/io/curity/oauth/OAuthFilter.java index c68b21f..3e786c1 100644 --- a/src/main/java/io/curity/oauth/OAuthFilter.java +++ b/src/main/java/io/curity/oauth/OAuthFilter.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2016 Curity AB. - * + * * 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. @@ -25,8 +25,8 @@ import jakarta.servlet.UnavailableException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; + import java.io.IOException; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; @@ -35,15 +35,15 @@ public abstract class OAuthFilter implements Filter { - private static final String[] NO_SCOPES = {}; private static final Logger _logger = Logger.getLogger(OAuthFilter.class.getName()); + private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; private static final String AUTHORIZATION = "Authorization"; public static final String PRINCIPAL_ATTRIBUTE_NAME = "principal"; private Map _filterConfig; // Protected, so subclasses don't have to repeat the conversion to this private String _oauthHost = null; - private String[] _scopes = null; + private List _scopes = List.of(); private interface InitParams { @@ -58,25 +58,25 @@ public void init(FilterConfig filterConfig) throws ServletException _oauthHost = FilterHelper.getInitParamValue(InitParams.OAUTH_HOST, _filterConfig); - _scopes = FilterHelper.getOptionalInitParamValue(InitParams.SCOPE, _filterConfig, it -> it.split("\\s+")) - .orElse(NO_SCOPES); + _scopes = FilterHelper.getWhitespaceSeparatedInitParamValues(InitParams.SCOPE, _filterConfig); } /** * The doFilter is the primary filter method of a Servlet filter. It is implemented as a final method * and will call the configured filters authenticate and authorize methods. * Authorize is optional to implement as this filter implements a default scope check method. - * @param servletRequest The default servlet request + * + * @param servletRequest The default servlet request * @param servletResponse The default servlet response - * @param filterChain A filter chain to continue with after this filter is done - * @throws IOException when response fails to send an error + * @param filterChain A filter chain to continue with after this filter is done + * @throws IOException when response fails to send an error * @throws ServletException when authentication fails for some exceptional reason */ @Override public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletResponse response = (HttpServletResponse)servletResponse; + HttpServletResponse response = (HttpServletResponse) servletResponse; Optional token = extractAccessTokenFromHeader(servletRequest); String oauthHost = getOAuthServerRealm(); @@ -111,7 +111,7 @@ public final void doFilter(ServletRequest servletRequest, ServletResponse servle if (filterChain != null) { filterChain.doFilter( - new AuthenticatedUserRequestWrapper((HttpServletRequest)servletRequest, authenticatedUser), + new AuthenticatedUserRequestWrapper((HttpServletRequest) servletRequest, authenticatedUser), servletResponse); } } @@ -163,6 +163,7 @@ protected String getOAuthServerRealm() throws UnavailableException /** * This is the authenticate method of the filter, it will take the token as string input and * must perform the appropriate operation to validate the token. + * * @param token - The token extracted from the Authorization header and stripped of the Bearer * @return An AuthenticatedUser if the token was valid, or null if not. * @throws ServletException when authentication fails for some exceptional reason @@ -184,6 +185,7 @@ protected Optional authenticate(String token) throws ServletE return Optional.ofNullable(result); } + /** * Authorizes the current request by checking that all configured scopes are included in the one presented in the * request. @@ -196,10 +198,8 @@ protected Optional authenticate(String token) throws ServletE */ protected boolean isAuthorized(AuthenticatedUser authenticatedUser) { - List requiredScopes = Arrays.asList(_scopes); - // No scopes required for authorization - return requiredScopes.isEmpty() || authenticatedUser.getScopes().containsAll(requiredScopes); + return _scopes.isEmpty() || authenticatedUser.getScopes().containsAll(_scopes); } @Override @@ -222,12 +222,13 @@ public void destroy() /** * Extracts the token from the Authorization header, removing the Bearer prefix + * * @param request The incoming request * @return the token or null if not present */ private Optional extractAccessTokenFromHeader(ServletRequest request) { - HttpServletRequest httpRequest = (HttpServletRequest)request; + HttpServletRequest httpRequest = (HttpServletRequest) request; String authorizationHeader = httpRequest.getHeader(AUTHORIZATION); String result = null; @@ -235,7 +236,7 @@ private Optional extractAccessTokenFromHeader(ServletRequest request) { String[] tokenSplit = authorizationHeader.split("[Bb][Ee][Aa][Rr][Ee][Rr]\\s+"); - if(tokenSplit.length != 2) + if (tokenSplit.length != 2) { _logger.fine("Incoming token in Authorization header is not a Bearer token"); } diff --git a/src/main/java/io/curity/oauth/OAuthJwtFilter.java b/src/main/java/io/curity/oauth/OAuthJwtFilter.java index b574cf7..f7cc0f3 100644 --- a/src/main/java/io/curity/oauth/OAuthJwtFilter.java +++ b/src/main/java/io/curity/oauth/OAuthJwtFilter.java @@ -1,12 +1,12 @@ /* * Copyright (C) 2016 Curity AB. - * + * * 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. @@ -16,12 +16,14 @@ package io.curity.oauth; -import javax.json.JsonReaderFactory; -import javax.json.spi.JsonProvider; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.UnavailableException; + +import javax.json.JsonReaderFactory; +import javax.json.spi.JsonProvider; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; public class OAuthJwtFilter extends OAuthFilter @@ -53,7 +55,7 @@ public void init(FilterConfig filterConfig) throws ServletException if (_jwtValidator == null) { _jwtValidator = createTokenValidator(getFilterConfiguration()); - + _logger.info(() -> String.format("%s successfully initialized", OAuthFilter.class.getSimpleName())); } else @@ -71,10 +73,10 @@ protected TokenValidator createTokenValidator(Map filterConfig) throw // the ReaderFactory using the filter's config. JsonReaderFactory jsonReaderFactory = JsonProvider.provider().createReaderFactory(filterConfig); WebKeysClient webKeysClient = HttpClientProvider.provider().createWebKeysClient(filterConfig); - String audience = FilterHelper.getInitParamValue(InitParams.AUDIENCE, filterConfig); - String issuer = FilterHelper.getInitParamValue(InitParams.ISSUER, filterConfig); + Set audiences = Set.copyOf(FilterHelper.getWhitespaceSeparatedInitParamValues(InitParams.AUDIENCE, filterConfig)); + Set issuers = Set.copyOf(FilterHelper.getWhitespaceSeparatedInitParamValues(InitParams.ISSUER, filterConfig)); - return _jwtValidator = new JwtValidatorWithJwk(_minKidReloadTimeInSeconds, webKeysClient, audience, issuer, + return _jwtValidator = new JwtValidatorWithJwk(_minKidReloadTimeInSeconds, webKeysClient, audiences, issuers, jsonReaderFactory); } diff --git a/src/test/java/io/curity/oauth/FilterHelperTest.java b/src/test/java/io/curity/oauth/FilterHelperTest.java new file mode 100644 index 0000000..8e04626 --- /dev/null +++ b/src/test/java/io/curity/oauth/FilterHelperTest.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2016 Curity AB. + * + * 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 io.curity.oauth; + +import jakarta.servlet.FilterConfig; +import jakarta.servlet.UnavailableException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +public class FilterHelperTest +{ + @Mock + private FilterConfig mockFilterConfig; + + @Before + public void setUp() + { + MockitoAnnotations.openMocks(this); + } + + @Test + public void canProvideSimpleStrings() throws UnavailableException + { + Map params = Collections.singletonMap("tokenEndpoint", "https://example.com/token"); + + String result = FilterHelper.getInitParamValue("tokenEndpoint", params); + + assertEquals("https://example.com/token", result); + } + + @Test + public void canProvideSimpleStringsWithConverter() throws UnavailableException + { + Map params = Collections.singletonMap("maxRetries", "5"); + + Integer result = FilterHelper.getInitParamValue("maxRetries", params, Integer::parseInt); + + assertEquals(Integer.valueOf(5), result); + } + + @Test + public void canProvideWhiteSeparatedStringLists() throws UnavailableException + { + Map params = Collections.singletonMap("scope", "read write delete"); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("read", result.get(0)); + assertEquals("write", result.get(1)); + assertEquals("delete", result.get(2)); + } + + @Test + public void canProvideWhiteSeparatedStringListsWithVariousWhitespace() throws UnavailableException + { + Map params = Collections.singletonMap("scope", "read write\tdelete\nupdate"); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertEquals(4, result.size()); + assertEquals("read", result.get(0)); + assertEquals("write", result.get(1)); + assertEquals("delete", result.get(2)); + assertEquals("update", result.get(3)); + } + + @Test + public void canProvideSingleValueInWhiteSeparatedStringList() throws UnavailableException + { + Map params = Collections.singletonMap("scope", "read"); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("read", result.get(0)); + } + + @Test(expected = UnavailableException.class) + public void failsWhenParameterIsMissing() throws UnavailableException + { + Map params = Collections.emptyMap(); + + FilterHelper.getInitParamValue("tokenEndpoint", params); + } + + @Test + public void failsWhenParameterIsMissingWithCorrectMessage() throws UnavailableException + { + Map params = Collections.emptyMap(); + + try + { + FilterHelper.getInitParamValue("tokenEndpoint", params); + fail("Should have thrown UnavailableException"); + } + catch (UnavailableException e) + { + assertTrue(e.getMessage().contains("missing required initParam [tokenEndpoint]")); + assertTrue(e.getMessage().contains("OAuthFilter")); + } + } + + @Test + public void doesNotFailWhenOptionalParameterIsMissing() throws UnavailableException + { + Map params = Collections.emptyMap(); + + Optional result = FilterHelper.getOptionalInitParamValue("audience", params, + v -> v); + + assertFalse(result.isPresent()); + } + + @Test + public void doesNotFailWhenOptionalParameterIsPresentWithConverter() throws UnavailableException + { + Map params = Collections.singletonMap("connectionTimeout", "30000"); + + Optional result = FilterHelper.getOptionalInitParamValue("connectionTimeout", params, + Long::parseLong); + + assertTrue(result.isPresent()); + assertEquals(Long.valueOf(30000), result.get()); + } + + @Test + public void doesNotFailWhenOptionalParameterIsNull() throws UnavailableException + { + Map params = Collections.singletonMap("someParam", null); + + Optional result = FilterHelper.getOptionalInitParamValue("someParam", params, + v -> v); + + assertFalse(result.isPresent()); + } + + @Test + public void doesNotFailWhenNoParametersProvidedForWhiteSpaceSeparatedStringLists() + throws UnavailableException + { + Map params = Collections.emptyMap(); + + List result = FilterHelper.getWhitespaceSeparatedInitParamValues("scope", params); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void initParamsMapFromWithNoParameters() + { + when(mockFilterConfig.getInitParameterNames()) + .thenReturn(Collections.emptyEnumeration()); + + Map result = FilterHelper.initParamsMapFrom(mockFilterConfig); + + assertNotNull(result); + assertTrue(result.isEmpty()); + } + + @Test + public void initParamsMapFromWithSingleParameter() + { + when(mockFilterConfig.getInitParameterNames()) + .thenReturn(Collections.enumeration(Collections.singletonList("tokenEndpoint"))); + when(mockFilterConfig.getInitParameter("tokenEndpoint")) + .thenReturn("https://example.com/token"); + + Map result = FilterHelper.initParamsMapFrom(mockFilterConfig); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("https://example.com/token", result.get("tokenEndpoint")); + } + + @Test + public void initParamsMapFromWithMultipleParameters() + { + List paramNames = Arrays.asList("tokenEndpoint", "scope", "audience"); + when(mockFilterConfig.getInitParameterNames()) + .thenReturn(Collections.enumeration(paramNames)); + when(mockFilterConfig.getInitParameter("tokenEndpoint")) + .thenReturn("https://example.com/token"); + when(mockFilterConfig.getInitParameter("scope")) + .thenReturn("read write"); + when(mockFilterConfig.getInitParameter("audience")) + .thenReturn("api"); + + Map result = FilterHelper.initParamsMapFrom(mockFilterConfig); + + assertNotNull(result); + assertEquals(3, result.size()); + assertEquals("https://example.com/token", result.get("tokenEndpoint")); + assertEquals("read write", result.get("scope")); + assertEquals("api", result.get("audience")); + } +} From fe4e21caba16adba7fe4dc95c30191cde3cbf913 Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Thu, 30 Jul 2026 10:51:33 +0200 Subject: [PATCH 6/8] Follow documented behaviour: allow ALL audiences/issuers if none is specified for the filter. --- .../io/curity/oauth/AbstractJwtValidator.java | 4 +-- .../io/curity/oauth/JwtAudienceClaimTest.java | 28 ++++++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/curity/oauth/AbstractJwtValidator.java b/src/main/java/io/curity/oauth/AbstractJwtValidator.java index af950ab..80a8684 100644 --- a/src/main/java/io/curity/oauth/AbstractJwtValidator.java +++ b/src/main/java/io/curity/oauth/AbstractJwtValidator.java @@ -95,12 +95,12 @@ public final JsonData validate(String jwt) throws TokenValidationException assert !aud.isEmpty() : "aud claim is not present or is invalid in JWT"; assert iss != null && !iss.isEmpty() : "iss claim is not present in JWT"; - if (aud.stream().noneMatch(_audiences::contains)) + if (!_audiences.isEmpty() && aud.stream().noneMatch(_audiences::contains)) { throw new InvalidAudienceException(String.join(", ", _audiences), String.join(", ", aud)); } - if (!_issuers.contains(iss)) + if (!_issuers.isEmpty() && !_issuers.contains(iss)) { throw new InvalidIssuerException(String.join(", ", _issuers), iss); } diff --git a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java index 70d27ea..264d524 100644 --- a/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java +++ b/src/test/java/io/curity/oauth/JwtAudienceClaimTest.java @@ -93,9 +93,29 @@ public void testValidatorAcceptsManyAudiencesAndTokenHasOnlyInvalidAudiencesArra keys -> new JwtValidatorWithCert(ISSUER, Set.of("a", "b"), keys), false); } + @Test + public void testValidatorAcceptsNoAudiencesAndTokenHasAnyAudience() throws Exception + { + assertTokenValidation("any:audience", + keys -> new JwtValidatorWithCert(ISSUER, Set.of(), keys), true); + + assertTokenValidation(List.of("any:audience", "another:audience"), + keys -> new JwtValidatorWithCert(ISSUER, Set.of(), keys), true); + } + + @Test + public void testValidatorAcceptsNoIssuersAndTokenHasAnyIssuer() throws Exception + { + assertTokenValidation("some:audience", + keys -> new JwtValidatorWithCert(Set.of(), Set.of("some:audience"), keys), true); + + assertTokenValidation(List.of("some:audience", "other:audience"), + keys -> new JwtValidatorWithCert(Set.of(), Set.of("some:audience"), keys), true); + } + private static void assertTokenValidation(String tokenAudience, - Function, JwtValidator> validatorFactory, - boolean shouldBeValid) throws Exception + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception { // the token is issued with an array of audiences if there's more than one audience, // or with a string audience if there's only one. @@ -103,8 +123,8 @@ private static void assertTokenValidation(String tokenAudience, } private static void assertTokenValidation(List tokenAudiences, - Function, JwtValidator> validatorFactory, - boolean shouldBeValid) throws Exception + Function, JwtValidator> validatorFactory, + boolean shouldBeValid) throws Exception { JwtTokenFixtureHelper.TokenFixture fixture = JwtTokenFixtureHelper.createTokenFixture(ISSUER, tokenAudiences); try (JwtValidator validator = validatorFactory.apply(fixture.keys)) From a1e72c1255634b49127d7716e21019701fe9c7da Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Thu, 30 Jul 2026 10:57:13 +0200 Subject: [PATCH 7/8] Fixed error messages. --- src/main/java/io/curity/oauth/ValidationExceptions.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/curity/oauth/ValidationExceptions.java b/src/main/java/io/curity/oauth/ValidationExceptions.java index fc7ddfb..15ebbd2 100644 --- a/src/main/java/io/curity/oauth/ValidationExceptions.java +++ b/src/main/java/io/curity/oauth/ValidationExceptions.java @@ -43,7 +43,7 @@ class MissingAlgorithmException extends TokenValidationException class InvalidAudienceException extends TokenValidationException { - private static final String _formattedMessage = "Audience %s does not match expected one %s"; + private static final String _formattedMessage = "Audience(s) '%s' does not match expected '%s'"; InvalidAudienceException(String expectedAudience, String actualAudience) { @@ -103,7 +103,7 @@ class UnknownAlgorithmException extends InvalidTokenFormatException class InvalidIssuerException extends TokenValidationException { - private static final String _formattedMessage = "Issuer %s does not match expected one of %s"; + private static final String _formattedMessage = "Issuer '%s' does not match expected one of '%s'"; InvalidIssuerException(String expectedIssuer, String actualIssuer) { From f008aa4d503276d7ba93ebb759dcfc1e39dd218c Mon Sep 17 00:00:00 2001 From: renatoathaydes Date: Thu, 30 Jul 2026 10:58:05 +0200 Subject: [PATCH 8/8] Bumped version to 4.1.0. --- CHANGELOG.md | 2 ++ pom.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 911288d..b8b7387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.1.0 (2026-07-30) + - Support `aud` claim in JWT when it's specified as an array rather than a string. - Allow JWT validators to configure a Set of accepted audiences and issuers, not just one. - OAuthJwtFilter configuration now accepts whitespace-separated values for `audience` and `issuer`. diff --git a/pom.xml b/pom.xml index 70383f5..b355cbf 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.curity oauth-filter - 4.0.0 + 4.1.0 OAuth API Filter A Servlet Filter that authenticates and authorizes requests using OAuth access tokens of various kinds. https://github.com/curityio/oauth-filter-for-java