Skip to content

Commit c767751

Browse files
committed
Add OidcIdToken.Builder
Fixes gh-7592
1 parent 4954a22 commit c767751

File tree

2 files changed

+356
-3
lines changed

2 files changed

+356
-3
lines changed

oauth2/oauth2-core/src/main/java/org/springframework/security/oauth2/core/oidc/OidcIdToken.java

+217-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,29 @@
1515
*/
1616
package org.springframework.security.oauth2.core.oidc;
1717

18-
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
19-
import org.springframework.util.Assert;
20-
2118
import java.time.Instant;
19+
import java.util.Collection;
2220
import java.util.Collections;
2321
import java.util.LinkedHashMap;
22+
import java.util.List;
2423
import java.util.Map;
24+
import java.util.function.Consumer;
25+
26+
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
27+
import org.springframework.util.Assert;
28+
29+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.ACR;
30+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.AMR;
31+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.AT_HASH;
32+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.AUD;
33+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.AUTH_TIME;
34+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.AZP;
35+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.C_HASH;
36+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.EXP;
37+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.IAT;
38+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.ISS;
39+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.NONCE;
40+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.SUB;
2541

2642
/**
2743
* An implementation of an {@link AbstractOAuth2Token} representing an OpenID Connect Core 1.0 ID Token.
@@ -59,4 +75,202 @@ public OidcIdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map<S
5975
public Map<String, Object> getClaims() {
6076
return this.claims;
6177
}
78+
79+
/**
80+
* Create a {@link Builder} based on the given token value
81+
*
82+
* @param tokenValue the token value to use
83+
* @return the {@link Builder} for further configuration
84+
* @since 5.3
85+
*/
86+
public static Builder withTokenValue(String tokenValue) {
87+
return new Builder(tokenValue);
88+
}
89+
90+
/**
91+
* A builder for {@link OidcIdToken}s
92+
*
93+
* @author Josh Cummings
94+
* @since 5.3
95+
*/
96+
public static final class Builder {
97+
private String tokenValue;
98+
private final Map<String, Object> claims = new LinkedHashMap<>();
99+
100+
private Builder(String tokenValue) {
101+
this.tokenValue = tokenValue;
102+
}
103+
104+
/**
105+
* Use this token value in the resulting {@link OidcIdToken}
106+
*
107+
* @param tokenValue The token value to use
108+
* @return the {@link Builder} for further configurations
109+
*/
110+
public Builder tokenValue(String tokenValue) {
111+
this.tokenValue = tokenValue;
112+
return this;
113+
}
114+
115+
/**
116+
* Use this claim in the resulting {@link OidcIdToken}
117+
*
118+
* @param name The claim name
119+
* @param value The claim value
120+
* @return the {@link Builder} for further configurations
121+
*/
122+
public Builder claim(String name, Object value) {
123+
this.claims.put(name, value);
124+
return this;
125+
}
126+
127+
/**
128+
* Provides access to every {@link #claim(String, Object)}
129+
* declared so far with the possibility to add, replace, or remove.
130+
* @param claimsConsumer the consumer
131+
* @return the {@link Builder} for further configurations
132+
*/
133+
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
134+
claimsConsumer.accept(this.claims);
135+
return this;
136+
}
137+
138+
/**
139+
* Use this access token hash in the resulting {@link OidcIdToken}
140+
*
141+
* @param accessTokenHash The access token hash to use
142+
* @return the {@link Builder} for further configurations
143+
*/
144+
public Builder accessTokenHash(String accessTokenHash) {
145+
return claim(AT_HASH, accessTokenHash);
146+
}
147+
148+
/**
149+
* Use this audience in the resulting {@link OidcIdToken}
150+
*
151+
* @param audience The audience(s) to use
152+
* @return the {@link Builder} for further configurations
153+
*/
154+
public Builder audience(Collection<String> audience) {
155+
return claim(AUD, audience);
156+
}
157+
158+
/**
159+
* Use this authentication {@link Instant} in the resulting {@link OidcIdToken}
160+
*
161+
* @param authenticatedAt The authentication {@link Instant} to use
162+
* @return the {@link Builder} for further configurations
163+
*/
164+
public Builder authTime(Instant authenticatedAt) {
165+
return claim(AUTH_TIME, authenticatedAt);
166+
}
167+
168+
/**
169+
* Use this authentication context class reference in the resulting {@link OidcIdToken}
170+
*
171+
* @param authenticationContextClass The authentication context class reference to use
172+
* @return the {@link Builder} for further configurations
173+
*/
174+
public Builder authenticationContextClass(String authenticationContextClass) {
175+
return claim(ACR, authenticationContextClass);
176+
}
177+
178+
/**
179+
* Use these authentication methods in the resulting {@link OidcIdToken}
180+
*
181+
* @param authenticationMethods The authentication methods to use
182+
* @return the {@link Builder} for further configurations
183+
*/
184+
public Builder authenticationMethods(List<String> authenticationMethods) {
185+
return claim(AMR, authenticationMethods);
186+
}
187+
188+
/**
189+
* Use this authorization code hash in the resulting {@link OidcIdToken}
190+
*
191+
* @param authorizationCodeHash The authorization code hash to use
192+
* @return the {@link Builder} for further configurations
193+
*/
194+
public Builder authorizationCodeHash(String authorizationCodeHash) {
195+
return claim(C_HASH, authorizationCodeHash);
196+
}
197+
198+
/**
199+
* Use this authorized party in the resulting {@link OidcIdToken}
200+
*
201+
* @param authorizedParty The authorized party to use
202+
* @return the {@link Builder} for further configurations
203+
*/
204+
public Builder authorizedParty(String authorizedParty) {
205+
return claim(AZP, authorizedParty);
206+
}
207+
208+
/**
209+
* Use this expiration in the resulting {@link OidcIdToken}
210+
*
211+
* @param expiresAt The expiration to use
212+
* @return the {@link Builder} for further configurations
213+
*/
214+
public Builder expiresAt(Instant expiresAt) {
215+
return this.claim(EXP, expiresAt);
216+
}
217+
218+
/**
219+
* Use this issued-at timestamp in the resulting {@link OidcIdToken}
220+
*
221+
* @param issuedAt The issued-at timestamp to use
222+
* @return the {@link Builder} for further configurations
223+
*/
224+
public Builder issuedAt(Instant issuedAt) {
225+
return this.claim(IAT, issuedAt);
226+
}
227+
228+
/**
229+
* Use this issuer in the resulting {@link OidcIdToken}
230+
*
231+
* @param issuer The issuer to use
232+
* @return the {@link Builder} for further configurations
233+
*/
234+
public Builder issuer(String issuer) {
235+
return this.claim(ISS, issuer);
236+
}
237+
238+
/**
239+
* Use this nonce in the resulting {@link OidcIdToken}
240+
*
241+
* @param nonce The nonce to use
242+
* @return the {@link Builder} for further configurations
243+
*/
244+
public Builder nonce(String nonce) {
245+
return this.claim(NONCE, nonce);
246+
}
247+
248+
/**
249+
* Use this subject in the resulting {@link OidcIdToken}
250+
*
251+
* @param subject The subject to use
252+
* @return the {@link Builder} for further configurations
253+
*/
254+
public Builder subject(String subject) {
255+
return this.claim(SUB, subject);
256+
}
257+
258+
/**
259+
* Build the {@link OidcIdToken}
260+
*
261+
* @return The constructed {@link OidcIdToken}
262+
*/
263+
public OidcIdToken build() {
264+
Instant iat = toInstant(this.claims.get(IAT));
265+
Instant exp = toInstant(this.claims.get(EXP));
266+
return new OidcIdToken(this.tokenValue, iat, exp, this.claims);
267+
}
268+
269+
private Instant toInstant(Object timestamp) {
270+
if (timestamp != null) {
271+
Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant");
272+
}
273+
return (Instant) timestamp;
274+
}
275+
}
62276
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.core.oidc;
18+
19+
import java.time.Instant;
20+
21+
import org.junit.Test;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
import static org.assertj.core.api.Assertions.assertThatCode;
25+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.EXP;
26+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.IAT;
27+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.SUB;
28+
29+
/**
30+
* Tests for {@link OidcUserInfo}
31+
*/
32+
public class OidcIdTokenBuilderTests {
33+
@Test
34+
public void buildWhenCalledTwiceThenGeneratesTwoOidcIdTokens() {
35+
OidcIdToken.Builder idTokenBuilder = OidcIdToken.withTokenValue("token");
36+
37+
OidcIdToken first = idTokenBuilder
38+
.tokenValue("V1")
39+
.claim("TEST_CLAIM_1", "C1")
40+
.build();
41+
42+
OidcIdToken second = idTokenBuilder
43+
.tokenValue("V2")
44+
.claim("TEST_CLAIM_1", "C2")
45+
.claim("TEST_CLAIM_2", "C3")
46+
.build();
47+
48+
assertThat(first.getClaims()).hasSize(1);
49+
assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1");
50+
assertThat(first.getTokenValue()).isEqualTo("V1");
51+
52+
assertThat(second.getClaims()).hasSize(2);
53+
assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2");
54+
assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3");
55+
assertThat(second.getTokenValue()).isEqualTo("V2");
56+
}
57+
58+
@Test
59+
public void expiresAtWhenUsingGenericOrNamedClaimMethodRequiresInstant() {
60+
OidcIdToken.Builder idTokenBuilder = OidcIdToken.withTokenValue("token");
61+
62+
Instant now = Instant.now();
63+
64+
OidcIdToken idToken = idTokenBuilder
65+
.expiresAt(now).build();
66+
assertThat(idToken.getExpiresAt()).isSameAs(now);
67+
68+
idToken = idTokenBuilder
69+
.expiresAt(now).build();
70+
assertThat(idToken.getExpiresAt()).isSameAs(now);
71+
72+
assertThatCode(() -> idTokenBuilder
73+
.claim(EXP, "not an instant").build())
74+
.isInstanceOf(IllegalArgumentException.class);
75+
}
76+
77+
@Test
78+
public void issuedAtWhenUsingGenericOrNamedClaimMethodRequiresInstant() {
79+
OidcIdToken.Builder idTokenBuilder = OidcIdToken.withTokenValue("token");
80+
81+
Instant now = Instant.now();
82+
83+
OidcIdToken idToken = idTokenBuilder
84+
.issuedAt(now).build();
85+
assertThat(idToken.getIssuedAt()).isSameAs(now);
86+
87+
idToken = idTokenBuilder
88+
.issuedAt(now).build();
89+
assertThat(idToken.getIssuedAt()).isSameAs(now);
90+
91+
assertThatCode(() -> idTokenBuilder
92+
.claim(IAT, "not an instant").build())
93+
.isInstanceOf(IllegalArgumentException.class);
94+
}
95+
96+
@Test
97+
public void subjectWhenUsingGenericOrNamedClaimMethodThenLastOneWins() {
98+
OidcIdToken.Builder idTokenBuilder = OidcIdToken.withTokenValue("token");
99+
100+
String generic = new String("sub");
101+
String named = new String("sub");
102+
103+
OidcIdToken idToken = idTokenBuilder
104+
.subject(named)
105+
.claim(SUB, generic).build();
106+
assertThat(idToken.getSubject()).isSameAs(generic);
107+
108+
idToken = idTokenBuilder
109+
.claim(SUB, generic)
110+
.subject(named).build();
111+
assertThat(idToken.getSubject()).isSameAs(named);
112+
}
113+
114+
@Test
115+
public void claimsWhenRemovingAClaimThenIsNotPresent() {
116+
OidcIdToken.Builder idTokenBuilder = OidcIdToken.withTokenValue("token")
117+
.claim("needs", "a claim");
118+
119+
OidcIdToken idToken = idTokenBuilder
120+
.subject("sub")
121+
.claims(claims -> claims.remove(SUB))
122+
.build();
123+
assertThat(idToken.getSubject()).isNull();
124+
}
125+
126+
@Test
127+
public void claimsWhenAddingAClaimThenIsPresent() {
128+
OidcIdToken.Builder idTokenBuilder = OidcIdToken.withTokenValue("token");
129+
130+
String name = new String("name");
131+
String value = new String("value");
132+
OidcIdToken idToken = idTokenBuilder
133+
.claims(claims -> claims.put(name, value))
134+
.build();
135+
136+
assertThat(idToken.getClaims()).hasSize(1);
137+
assertThat(idToken.getClaims().get(name)).isSameAs(value);
138+
}
139+
}

0 commit comments

Comments
 (0)