From 38c6ff5079e198d1311be7aa157ce4248e4d1cb6 Mon Sep 17 00:00:00 2001 From: Jinhui Du Date: Mon, 7 Sep 2026 11:03:43 -0700 Subject: [PATCH] Require explicit allowlist in IdentityServerDescriptorProvider to prevent SSRF - Require explicit allowlisted issuers in IdentityServerDescriptorProvider constructor. - Reject unlisted, empty, or null issuers in getServerDescriptor. - Add null check in DefaultPublicKeyLocator. - Update test suites and add unit tests for allowlisting behavior. --- .../discovery/DefaultPublicKeyLocator.java | 3 + .../IdentityServerDescriptorProvider.java | 34 +++++++-- .../oauth/jsontoken/JsonTokenParserTest.java | 20 +++++- .../oauth/jsontoken/JsonTokenTestBase.java | 2 +- .../IdentityServerDescriptorProviderTest.java | 71 +++++++++++++++++++ .../signatures/SignedTokenBuilderTest.java | 2 +- 6 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 src/test/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProviderTest.java diff --git a/src/main/java/net/oauth/jsontoken/discovery/DefaultPublicKeyLocator.java b/src/main/java/net/oauth/jsontoken/discovery/DefaultPublicKeyLocator.java index 4db99b8..fed0d59 100644 --- a/src/main/java/net/oauth/jsontoken/discovery/DefaultPublicKeyLocator.java +++ b/src/main/java/net/oauth/jsontoken/discovery/DefaultPublicKeyLocator.java @@ -56,6 +56,9 @@ public DefaultPublicKeyLocator( @Override public List findVerifier(String issuer, String keyId) { URI serverDescriptor = descriptorProvider.getServerDescriptor(issuer); + if (serverDescriptor == null) { + return null; + } Verifier rsaVerifier = new RsaSHA256Verifier( descriptorResolver.resolve(serverDescriptor).getVerificationKey(keyId)); diff --git a/src/main/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProvider.java b/src/main/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProvider.java index 8f6ef5a..0cc2f13 100644 --- a/src/main/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProvider.java +++ b/src/main/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProvider.java @@ -15,13 +15,15 @@ */ package net.oauth.jsontoken.discovery; +import com.google.common.collect.ImmutableSet; import java.net.URI; +import java.util.Collection; /** - * A {@link ServerDescriptorProvider} that returns the issuer id as the server descriptor. If a JSON - * Token issuer uses their own server descriptor as their issuer id, then the JSON Token verifier - * would use this implementation of {@link ServerDescriptorProvider} with the {@link - * DefaultPublicKeyLocator}. + * A {@link ServerDescriptorProvider} that returns the issuer id as the server descriptor for + * explicitly allowlisted issuers. If a JSON Token issuer uses their own server descriptor as their + * issuer id, then the JSON Token verifier would use this implementation of {@link + * ServerDescriptorProvider} with the {@link DefaultPublicKeyLocator}. * *

For example, some OAuth Servers might use their Client's server descriptors as client_ids, and * then use this implementation of {@link ServerDescriptorProvider} with the {@link @@ -29,12 +31,36 @@ */ public class IdentityServerDescriptorProvider implements ServerDescriptorProvider { + private final ImmutableSet allowedIssuers; + + /** + * Public constructor. + * + * @param allowedIssuers A collection of trusted issuer IDs whose server descriptors may be + * resolved. + */ + public IdentityServerDescriptorProvider(Collection allowedIssuers) { + this.allowedIssuers = ImmutableSet.copyOf(allowedIssuers); + } + + /** + * Public constructor. + * + * @param allowedIssuers One or more trusted issuer IDs whose server descriptors may be resolved. + */ + public IdentityServerDescriptorProvider(String... allowedIssuers) { + this.allowedIssuers = ImmutableSet.copyOf(allowedIssuers); + } + /* * (non-Javadoc) * @see net.oauth.jsontoken.discovery.ServerDescriptorProvider#getServerDescriptor(java.lang.String) */ @Override public URI getServerDescriptor(String issuer) { + if (issuer == null || !allowedIssuers.contains(issuer)) { + return null; + } return URI.create(issuer); } } diff --git a/src/test/java/net/oauth/jsontoken/JsonTokenParserTest.java b/src/test/java/net/oauth/jsontoken/JsonTokenParserTest.java index 334f55e..a45f75d 100644 --- a/src/test/java/net/oauth/jsontoken/JsonTokenParserTest.java +++ b/src/test/java/net/oauth/jsontoken/JsonTokenParserTest.java @@ -98,7 +98,7 @@ public void testVerifyAndDeserialize_tokenFromRuby() throws Exception { } public void testPublicKey() throws Exception { - RsaSHA256Signer signer = new RsaSHA256Signer("google.com", "key1", privateKey); + RsaSHA256Signer signer = new RsaSHA256Signer("example.com", "key1", privateKey); JsonToken token = new JsonToken(signer, clock); token.setParam("bar", 15); @@ -111,7 +111,7 @@ public void testPublicKey() throws Exception { JsonTokenParser parser = getJsonTokenParser(); token = parser.verifyAndDeserialize(tokenString); - assertEquals("google.com", token.getIssuer()); + assertEquals("example.com", token.getIssuer()); assertEquals(15, token.getParamAsPrimitive("bar").getAsLong()); assertEquals("some value", token.getParamAsPrimitive("foo").getAsString()); @@ -133,6 +133,22 @@ public void testPublicKey() throws Exception { assertThrows(SignatureException.class, () -> parser.verifyAndDeserialize(tamperedToken)); } + public void testPublicKey_untrustedIssuer() throws Exception { + RsaSHA256Signer signer = new RsaSHA256Signer("attacker.com", "key1", privateKey); + + JsonToken token = new JsonToken(signer, clock); + token.setParam("bar", 15); + token.setExpiration(clock.now().plus(Duration.ofMillis(60))); + + String tokenString = token.serializeAndSign(); + + JsonTokenParser parser = getJsonTokenParser(); + assertThrowsWithErrorCode( + IllegalStateException.class, + ErrorCode.NO_VERIFIER, + () -> parser.verifyAndDeserialize(tokenString)); + } + private JsonTokenParser getJsonTokenParser() { return new JsonTokenParser(clock, locators, new AlwaysPassChecker()); } diff --git a/src/test/java/net/oauth/jsontoken/JsonTokenTestBase.java b/src/test/java/net/oauth/jsontoken/JsonTokenTestBase.java index 8e3ec85..faf0464 100644 --- a/src/test/java/net/oauth/jsontoken/JsonTokenTestBase.java +++ b/src/test/java/net/oauth/jsontoken/JsonTokenTestBase.java @@ -125,7 +125,7 @@ protected void setUp() throws Exception { VerifierProvider rsaLocator = new DefaultPublicKeyLocator( - new IdentityServerDescriptorProvider(), + new IdentityServerDescriptorProvider("example.com"), uri -> JsonServerInfo.getDocument(SERVER_INFO_DOCUMENT)); locators = new VerifierProviders(); diff --git a/src/test/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProviderTest.java b/src/test/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProviderTest.java new file mode 100644 index 0000000..3d797e1 --- /dev/null +++ b/src/test/java/net/oauth/jsontoken/discovery/IdentityServerDescriptorProviderTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Google LLC + * + * 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 net.oauth.jsontoken.discovery; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import com.google.common.collect.ImmutableList; +import java.net.URI; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class IdentityServerDescriptorProviderTest { + + @Test + public void testAllowedIssuer_varargs() { + IdentityServerDescriptorProvider provider = + new IdentityServerDescriptorProvider("https://accounts.google.com", "https://example.com"); + + assertEquals( + URI.create("https://accounts.google.com"), + provider.getServerDescriptor("https://accounts.google.com")); + assertEquals( + URI.create("https://example.com"), + provider.getServerDescriptor("https://example.com")); + } + + @Test + public void testAllowedIssuer_collection() { + IdentityServerDescriptorProvider provider = + new IdentityServerDescriptorProvider( + ImmutableList.of("https://accounts.google.com", "https://example.com")); + + assertEquals( + URI.create("https://accounts.google.com"), + provider.getServerDescriptor("https://accounts.google.com")); + } + + @Test + public void testDisallowedIssuer_returnsNull() { + IdentityServerDescriptorProvider provider = + new IdentityServerDescriptorProvider("https://accounts.google.com"); + + assertNull(provider.getServerDescriptor("https://attacker.com")); + assertNull(provider.getServerDescriptor("http://169.254.169.254/latest/meta-data/")); + } + + @Test + public void testNullOrEmptyIssuer_returnsNull() { + IdentityServerDescriptorProvider provider = + new IdentityServerDescriptorProvider("https://accounts.google.com"); + + assertNull(provider.getServerDescriptor(null)); + assertNull(provider.getServerDescriptor("")); + } +} diff --git a/src/test/java/net/oauth/signatures/SignedTokenBuilderTest.java b/src/test/java/net/oauth/signatures/SignedTokenBuilderTest.java index 8b329c3..9ad633f 100644 --- a/src/test/java/net/oauth/signatures/SignedTokenBuilderTest.java +++ b/src/test/java/net/oauth/signatures/SignedTokenBuilderTest.java @@ -23,7 +23,7 @@ public class SignedTokenBuilderTest extends JsonTokenTestBase { public void testSignature() throws Exception { - Signer signer = new RsaSHA256Signer("google.com", "key1", privateKey); + Signer signer = new RsaSHA256Signer("example.com", "key1", privateKey); SignedOAuthToken token = new SignedOAuthToken(signer); token.setMethod("GET");