Skip to content

Commit 98aad5b

Browse files
committed
Add OidcIdToken.Builder
Fixes gh-7592
1 parent 21d9632 commit 98aad5b

File tree

2 files changed

+355
-3
lines changed

2 files changed

+355
-3
lines changed

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

Lines changed: 216 additions & 3 deletions
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,201 @@ 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+
*/
85+
public static Builder withTokenValue(String tokenValue) {
86+
return new Builder(tokenValue);
87+
}
88+
89+
/**
90+
* A builder for {@link OidcIdToken}s
91+
*
92+
* @since 5.3
93+
* @author Josh Cummings
94+
*/
95+
public static final class Builder {
96+
private String tokenValue;
97+
private final Map<String, Object> claims = new LinkedHashMap<>();
98+
99+
private Builder(String tokenValue) {
100+
this.tokenValue = tokenValue;
101+
}
102+
103+
/**
104+
* Use this token value in the resulting {@link OidcIdToken}
105+
*
106+
* @param tokenValue The token value to use
107+
* @return the {@link Builder} for further configurations
108+
*/
109+
public Builder tokenValue(String tokenValue) {
110+
this.tokenValue = tokenValue;
111+
return this;
112+
}
113+
114+
/**
115+
* Use this claim in the resulting {@link OidcIdToken}
116+
*
117+
* @param name The claim name
118+
* @param value The claim value
119+
* @return the {@link Builder} for further configurations
120+
*/
121+
public Builder claim(String name, Object value) {
122+
this.claims.put(name, value);
123+
return this;
124+
}
125+
126+
/**
127+
* Provides access to every {@link #claim(String, Object)}
128+
* declared so far with the possibility to add, replace, or remove.
129+
* @param claimsConsumer the consumer
130+
* @return the {@link Builder} for further configurations
131+
*/
132+
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
133+
claimsConsumer.accept(this.claims);
134+
return this;
135+
}
136+
137+
/**
138+
* Use this access token hash in the resulting {@link OidcIdToken}
139+
*
140+
* @param accessTokenHash The access token hash to use
141+
* @return the {@link Builder} for further configurations
142+
*/
143+
public Builder accessTokenHash(String accessTokenHash) {
144+
return claim(AT_HASH, accessTokenHash);
145+
}
146+
147+
/**
148+
* Use this audience in the resulting {@link OidcIdToken}
149+
*
150+
* @param audience The audience(s) to use
151+
* @return the {@link Builder} for further configurations
152+
*/
153+
public Builder audience(Collection<String> audience) {
154+
return claim(AUD, audience);
155+
}
156+
157+
/**
158+
* Use this authentication {@link Instant} in the resulting {@link OidcIdToken}
159+
*
160+
* @param authenticatedAt The authentication {@link Instant} to use
161+
* @return the {@link Builder} for further configurations
162+
*/
163+
public Builder authTime(Instant authenticatedAt) {
164+
return claim(AUTH_TIME, authenticatedAt);
165+
}
166+
167+
/**
168+
* Use this authentication context class reference in the resulting {@link OidcIdToken}
169+
*
170+
* @param authenticationContextClass The authentication context class reference to use
171+
* @return the {@link Builder} for further configurations
172+
*/
173+
public Builder authenticationContextClass(String authenticationContextClass) {
174+
return claim(ACR, authenticationContextClass);
175+
}
176+
177+
/**
178+
* Use these authentication methods in the resulting {@link OidcIdToken}
179+
*
180+
* @param authenticationMethods The authentication methods to use
181+
* @return the {@link Builder} for further configurations
182+
*/
183+
public Builder authenticationMethods(List<String> authenticationMethods) {
184+
return claim(AMR, authenticationMethods);
185+
}
186+
187+
/**
188+
* Use this authorization code hash in the resulting {@link OidcIdToken}
189+
*
190+
* @param authorizationCodeHash The authorization code hash to use
191+
* @return the {@link Builder} for further configurations
192+
*/
193+
public Builder authorizationCodeHash(String authorizationCodeHash) {
194+
return claim(C_HASH, authorizationCodeHash);
195+
}
196+
197+
/**
198+
* Use this authorized party in the resulting {@link OidcIdToken}
199+
*
200+
* @param authorizedParty The authorized party to use
201+
* @return the {@link Builder} for further configurations
202+
*/
203+
public Builder authorizedParty(String authorizedParty) {
204+
return claim(AZP, authorizedParty);
205+
}
206+
207+
/**
208+
* Use this expiration in the resulting {@link OidcIdToken}
209+
*
210+
* @param expiresAt The expiration to use
211+
* @return the {@link Builder} for further configurations
212+
*/
213+
public Builder expiresAt(Instant expiresAt) {
214+
return this.claim(EXP, expiresAt);
215+
}
216+
217+
/**
218+
* Use this issued-at timestamp in the resulting {@link OidcIdToken}
219+
*
220+
* @param issuedAt The issued-at timestamp to use
221+
* @return the {@link Builder} for further configurations
222+
*/
223+
public Builder issuedAt(Instant issuedAt) {
224+
return this.claim(IAT, issuedAt);
225+
}
226+
227+
/**
228+
* Use this issuer in the resulting {@link OidcIdToken}
229+
*
230+
* @param issuer The issuer to use
231+
* @return the {@link Builder} for further configurations
232+
*/
233+
public Builder issuer(String issuer) {
234+
return this.claim(ISS, issuer);
235+
}
236+
237+
/**
238+
* Use this nonce in the resulting {@link OidcIdToken}
239+
*
240+
* @param nonce The nonce to use
241+
* @return the {@link Builder} for further configurations
242+
*/
243+
public Builder nonce(String nonce) {
244+
return this.claim(NONCE, nonce);
245+
}
246+
247+
/**
248+
* Use this subject in the resulting {@link OidcIdToken}
249+
*
250+
* @param subject The subject to use
251+
* @return the {@link Builder} for further configurations
252+
*/
253+
public Builder subject(String subject) {
254+
return this.claim(SUB, subject);
255+
}
256+
257+
/**
258+
* Build the {@link OidcIdToken}
259+
*
260+
* @return The constructed {@link OidcIdToken}
261+
*/
262+
public OidcIdToken build() {
263+
Instant iat = toInstant(this.claims.get(IAT));
264+
Instant exp = toInstant(this.claims.get(EXP));
265+
return new OidcIdToken(this.tokenValue, iat, exp, this.claims);
266+
}
267+
268+
private Instant toInstant(Object timestamp) {
269+
if (timestamp != null) {
270+
Assert.isInstanceOf(Instant.class, timestamp, "timestamps must be of type Instant");
271+
}
272+
return (Instant) timestamp;
273+
}
274+
}
62275
}
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)