Skip to content

Commit a33a994

Browse files
authored
JCL-444: Improve support for RFC 9207 (#1246)
1 parent 1886a86 commit a33a994

File tree

7 files changed

+131
-70
lines changed

7 files changed

+131
-70
lines changed

openid/src/main/java/com/inrupt/client/openid/Metadata.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,9 @@ public class Metadata {
112112
* A list of DPoP signing algorithm values supported by the given OpenID Connect provider.
113113
*/
114114
public List<String> dpopSigningAlgValuesSupported;
115+
116+
/**
117+
* Indication of whether the OpenID Connect provider supports RFC-9207.
118+
*/
119+
public boolean authorizationResponseIssParameterSupported;
115120
}

openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ ErrorResponse tryParseError(final InputStream input) {
211211
}
212212

213213
private Request tokenRequest(final Metadata metadata, final TokenRequest request) {
214+
// RFC 9207 describes this behavior as a SHOULD but recognizes use cases that vary;
215+
// this would be good to consider when adding broader configuration support to the libraries.
216+
if (metadata.authorizationResponseIssParameterSupported) {
217+
if (!Objects.equals(request.getIssuer(), metadata.issuer)) {
218+
throw new OpenIdException("Issuer mismatch. " +
219+
"Please verify that the designated OpenID issuer is correct");
220+
}
221+
}
222+
214223
if (!metadata.grantTypesSupported.contains(request.getGrantType())) {
215224
throw new OpenIdException("Grant type [" + request.getGrantType() + "] is not supported by this provider.");
216225
}

openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public static Session ofClientCredentials(final URI issuer, final String clientI
142142
return new OpenIdSession(id, dpop, () -> provider.token(TokenRequest.newBuilder()
143143
.clientSecret(clientSecret)
144144
.authMethod(authMethod)
145+
.issuer(issuer)
145146
.scopes(config.getScopes().toArray(new String[0]))
146147
.build("client_credentials", clientId))
147148
.thenApply(response -> {
@@ -166,11 +167,13 @@ public static Session ofClientCredentials(final OpenIdProvider provider,
166167
final OpenIdConfig config) {
167168
final String id = UUID.randomUUID().toString();
168169
final DPoP dpop = DPoP.of(config.getProofKeyPairs());
169-
return new OpenIdSession(id, dpop, () -> provider.token(TokenRequest.newBuilder()
170+
return new OpenIdSession(id, dpop, () -> provider.metadata()
171+
.thenCompose(metadata -> provider.token(TokenRequest.newBuilder()
170172
.clientSecret(clientSecret)
171173
.authMethod(authMethod)
172174
.scopes(config.getScopes().toArray(new String[0]))
173-
.build("client_credentials", clientId))
175+
.issuer(metadata.issuer)
176+
.build("client_credentials", clientId)))
174177
.thenApply(response -> {
175178
final JwtClaims claims = parseIdToken(response.idToken, config);
176179
return new Credential(response.tokenType, getIssuer(claims), response.idToken,

openid/src/main/java/com/inrupt/client/openid/TokenRequest.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public final class TokenRequest {
3838
private final String clientSecret;
3939
private final String authMethod;
4040
private final URI redirectUri;
41+
private final URI issuer;
4142
private final List<String> scopes;
4243

4344
/**
@@ -58,6 +59,15 @@ public List<String> getScopes() {
5859
return scopes;
5960
}
6061

62+
/**
63+
* Get the issuer.
64+
*
65+
* @return the issuer, may be {@code null}
66+
*/
67+
public URI getIssuer() {
68+
return issuer;
69+
}
70+
6171
/**
6272
* Get the authentication method.
6373
*
@@ -123,7 +133,8 @@ public static Builder newBuilder() {
123133

124134
/* package-private */
125135
TokenRequest(final String clientId, final String clientSecret, final URI redirectUri, final String grantType,
126-
final String authMethod, final String code, final String codeVerifier, final List<String> scopes) {
136+
final String authMethod, final String code, final String codeVerifier, final URI issuer,
137+
final List<String> scopes) {
127138
this.clientId = clientId;
128139
this.clientSecret = clientSecret;
129140
this.redirectUri = redirectUri;
@@ -132,6 +143,7 @@ public static Builder newBuilder() {
132143
this.code = code;
133144
this.codeVerifier = codeVerifier;
134145
this.scopes = scopes;
146+
this.issuer = issuer;
135147
}
136148

137149
/**
@@ -146,6 +158,7 @@ public static class Builder {
146158
private String builderCode;
147159
private String builderCodeVerifier;
148160
private URI builderRedirectUri;
161+
private URI builderIssuer;
149162
private List<String> builderScopes = new ArrayList<>();
150163

151164
/**
@@ -181,6 +194,17 @@ public Builder scopes(final String... scopes) {
181194
return this;
182195
}
183196

197+
/**
198+
* Set the issuer URI.
199+
*
200+
* @param issuer the issuer value
201+
* @return this builder
202+
*/
203+
public Builder issuer(final URI issuer) {
204+
builderIssuer = issuer;
205+
return this;
206+
}
207+
184208
/**
185209
* Set the authentication method for the token endpoint.
186210
*
@@ -240,7 +264,7 @@ public TokenRequest build(final String grantType, final String clientId) {
240264
}
241265

242266
return new TokenRequest(clientId, builderClientSecret, builderRedirectUri, grant, builderAuthMethod,
243-
builderCode, builderCodeVerifier, builderScopes);
267+
builderCode, builderCodeVerifier, builderIssuer, builderScopes);
244268
}
245269
}
246270
}

openid/src/test/java/com/inrupt/client/openid/OpenIdMockHttpService.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,16 @@ private void setupMocks() {
7575
.willReturn(aResponse()
7676
.withStatus(200)
7777
.withHeader("Content-Type", "application/json")
78-
.withBody(getMetadataJSON()
79-
.replace(
80-
"/oauth/oauth20/token",
81-
wireMockServer.baseUrl() + "/oauth/oauth20/token"))));
78+
.withBody(getMetadataJSON())));
8279

83-
wireMockServer.stubFor(post(urlPathMatching("/oauth/oauth20/token"))
80+
wireMockServer.stubFor(post(urlPathMatching("/token"))
8481
.withHeader("Content-Type", containing("application/x-www-form-urlencoded"))
8582
.withRequestBody(containing("myCodeverifier"))
8683
.willReturn(aResponse()
8784
.withStatus(200)
8885
.withHeader("Content-Type", "application/json")
8986
.withBody(getTokenResponseJSON())));
90-
wireMockServer.stubFor(post(urlPathMatching("/oauth/oauth20/token"))
87+
wireMockServer.stubFor(post(urlPathMatching("/token"))
9188
.withHeader("Content-Type", containing("application/x-www-form-urlencoded"))
9289
.atPriority(1)
9390
.withRequestBody(containing("code=none"))
@@ -96,7 +93,7 @@ private void setupMocks() {
9693
.withHeader("Content-Type", "application/json")
9794
.withBodyFile("token-error.json")));
9895

99-
wireMockServer.stubFor(post(urlPathMatching("/oauth/oauth20/token"))
96+
wireMockServer.stubFor(post(urlPathMatching("/token"))
10097
.withHeader("Content-Type", containing("application/x-www-form-urlencoded"))
10198
.atPriority(2)
10299
.withRequestBody(containing("grant_type=client_credentials"))
@@ -108,7 +105,8 @@ private void setupMocks() {
108105

109106
private String getMetadataJSON() {
110107
try (final InputStream res = OpenIdMockHttpService.class.getResourceAsStream("/metadata.json")) {
111-
return new String(IOUtils.toByteArray(res), UTF_8);
108+
return new String(IOUtils.toByteArray(res), UTF_8)
109+
.replace("http://example.test", wireMockServer.baseUrl());
112110
} catch (final IOException ex) {
113111
throw new UncheckedIOException("Could not read class resource", ex);
114112
}
@@ -130,9 +128,6 @@ private String getTokenResponseJSON() {
130128
"}";
131129
}
132130

133-
134-
135-
136131
public String start() {
137132
wireMockServer.start();
138133

openid/src/test/java/com/inrupt/client/openid/OpenIdProviderTest.java

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import com.inrupt.client.auth.DPoP;
2727
import com.inrupt.client.openid.TokenRequest.Builder;
28+
import com.inrupt.client.util.URIBuilder;
2829

2930
import java.io.ByteArrayInputStream;
3031
import java.io.IOException;
@@ -33,8 +34,6 @@
3334
import java.security.NoSuchAlgorithmException;
3435
import java.util.Arrays;
3536
import java.util.Collections;
36-
import java.util.HashMap;
37-
import java.util.Map;
3837
import java.util.OptionalInt;
3938
import java.util.UUID;
4039
import java.util.concurrent.CompletableFuture;
@@ -49,14 +48,14 @@ class OpenIdProviderTest {
4948

5049
private static OpenIdProvider openIdProvider;
5150
private static final OpenIdMockHttpService mockHttpService = new OpenIdMockHttpService();
52-
private static final Map<String, String> config = new HashMap<>();
5351
private static final DPoP dpop = DPoP.of();
5452

53+
private static URI issuer;
5554

5655
@BeforeAll
5756
static void setup() throws NoSuchAlgorithmException {
58-
config.put("openid_uri", mockHttpService.start());
59-
openIdProvider = new OpenIdProvider(URI.create(config.get("openid_uri")), dpop);
57+
issuer = URI.create(mockHttpService.start());
58+
openIdProvider = new OpenIdProvider(issuer, dpop);
6059
}
6160

6261
@AfterAll
@@ -66,15 +65,16 @@ static void teardown() {
6665

6766
@Test
6867
void metadataAsyncTest() {
69-
assertEquals("http://example.test",
70-
openIdProvider.metadata().toCompletableFuture().join().issuer.toString());
71-
assertEquals("http://example.test/oauth/jwks",
72-
openIdProvider.metadata().toCompletableFuture().join().jwksUri.toString());
68+
assertEquals(issuer,
69+
openIdProvider.metadata().toCompletableFuture().join().issuer);
70+
assertEquals(URIBuilder.newBuilder(issuer).path("jwks").build(),
71+
openIdProvider.metadata().toCompletableFuture().join().jwksUri);
7372
}
7473

7574
@Test
7675
void unknownMetadata() {
77-
final OpenIdProvider provider = new OpenIdProvider(URI.create(config.get("openid_uri") + "/not-found"), dpop);
76+
final OpenIdProvider provider = new OpenIdProvider(URIBuilder.newBuilder(issuer).path("not-found").build(),
77+
dpop);
7878
final CompletionException err = assertThrows(CompletionException.class,
7979
provider.metadata().toCompletableFuture()::join);
8080
assertTrue(err.getCause() instanceof OpenIdException);
@@ -90,7 +90,7 @@ void authorizeAsyncTest() {
9090
URI.create("myRedirectUri")
9191
);
9292
assertEquals(
93-
"http://example.test/auth?client_id=myClientId&redirect_uri=myRedirectUri&" +
93+
issuer + "/auth?client_id=myClientId&redirect_uri=myRedirectUri&" +
9494
"response_type=code&code_challenge=myCodeChallenge&code_challenge_method=method",
9595
openIdProvider.authorize(authReq).toCompletableFuture().join().toString()
9696
);
@@ -114,11 +114,67 @@ void tokenRequestIllegalArgumentsTest() {
114114
() -> builder.build("myGrantType", null));
115115
}
116116

117+
@Test
118+
void tokenIssuerMismatch() {
119+
final TokenRequest tokenReq = TokenRequest.newBuilder()
120+
.code("someCode")
121+
.codeVerifier("myCodeverifier")
122+
.issuer(URI.create("https://not.an.issuer.test"))
123+
.redirectUri(URI.create("https://example.test/redirectUri"))
124+
.build(
125+
"authorization_code",
126+
"myClientId"
127+
);
128+
129+
final CompletionException ex = assertThrows(CompletionException.class, openIdProvider.token(tokenReq)
130+
.toCompletableFuture()::join);
131+
assertTrue(ex.getCause() instanceof OpenIdException);
132+
final OpenIdException cause = (OpenIdException) ex.getCause();
133+
assertTrue(cause.getMessage().contains("Issuer mismatch"));
134+
}
135+
136+
@Test
137+
void tokenIssuerMissing() {
138+
final TokenRequest tokenReq = TokenRequest.newBuilder()
139+
.code("someCode")
140+
.codeVerifier("myCodeverifier")
141+
.redirectUri(URI.create("https://example.test/redirectUri"))
142+
.build(
143+
"authorization_code",
144+
"myClientId"
145+
);
146+
147+
final CompletionException ex = assertThrows(CompletionException.class, openIdProvider.token(tokenReq)
148+
.toCompletableFuture()::join);
149+
assertTrue(ex.getCause() instanceof OpenIdException);
150+
final OpenIdException cause = (OpenIdException) ex.getCause();
151+
assertTrue(cause.getMessage().contains("Issuer mismatch"));
152+
}
153+
154+
@Test
155+
void tokenIssuerMatch() {
156+
final TokenRequest tokenReq = TokenRequest.newBuilder()
157+
.code("someCode")
158+
.codeVerifier("myCodeverifier")
159+
.issuer(issuer)
160+
.redirectUri(URI.create("https://example.test/redirectUri"))
161+
.build(
162+
"authorization_code",
163+
"myClientId"
164+
);
165+
final TokenResponse token = openIdProvider.token(tokenReq)
166+
.toCompletableFuture().join();
167+
assertEquals("123456", token.accessToken);
168+
assertNotNull(token.idToken);
169+
assertEquals("Bearer", token.tokenType);
170+
}
171+
117172
@Test
118173
void tokenNoClientSecretTest() {
119174
final TokenRequest tokenReq = TokenRequest.newBuilder()
120175
.code("someCode")
121176
.codeVerifier("myCodeverifier")
177+
.issuer(issuer)
122178
.redirectUri(URI.create("https://example.test/redirectUri"))
123179
.build(
124180
"authorization_code",
@@ -137,6 +193,7 @@ void tokenWithClientSecretBasicTest() {
137193
.code("someCode")
138194
.codeVerifier("myCodeverifier")
139195
.clientSecret("myClientSecret")
196+
.issuer(issuer)
140197
.authMethod("client_secret_basic")
141198
.redirectUri(URI.create("https://example.test/redirectUri"))
142199
.build(
@@ -156,6 +213,7 @@ void tokenWithClientSecretePostTest() {
156213
.code("someCode")
157214
.codeVerifier("myCodeverifier")
158215
.clientSecret("myClientSecret")
216+
.issuer(issuer)
159217
.authMethod("client_secret_post")
160218
.redirectUri(URI.create("https://example.test/redirectUri"))
161219
.build(
@@ -174,6 +232,7 @@ void tokenAsyncTest() {
174232
final TokenRequest tokenReq = TokenRequest.newBuilder()
175233
.code("someCode")
176234
.codeVerifier("myCodeverifier")
235+
.issuer(issuer)
177236
.redirectUri(URI.create("https://example.test/redirectUri"))
178237
.build("authorization_code", "myClientId");
179238
final TokenResponse token = openIdProvider.token(tokenReq).toCompletableFuture().join();
@@ -187,6 +246,7 @@ void tokenAsyncStatusCodesTest() {
187246
final TokenRequest tokenReq = TokenRequest.newBuilder()
188247
.code("none")
189248
.codeVerifier("none")
249+
.issuer(issuer)
190250
.redirectUri(URI.create("none"))
191251
.build("authorization_code", "none");
192252

@@ -217,7 +277,7 @@ void endSessionAsyncTest() {
217277
.build();
218278
final URI uri = openIdProvider.endSession(endReq).toCompletableFuture().join();
219279
assertEquals(
220-
"http://example.test/endSession?" +
280+
issuer + "/endSession?" +
221281
"client_id=myClientId&post_logout_redirect_uri=https://example.test/redirectUri&id_token_hint=&state=solid",
222282
uri.toString()
223283
);

0 commit comments

Comments
 (0)