Skip to content

Commit 3f39a4b

Browse files
committed
Add OidcUserInfo.Builder
Fixes gh-7593
1 parent 98aad5b commit 3f39a4b

File tree

2 files changed

+371
-3
lines changed

2 files changed

+371
-3
lines changed

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

Lines changed: 279 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,36 @@
1515
*/
1616
package org.springframework.security.oauth2.core.oidc;
1717

18-
import org.springframework.security.core.SpringSecurityCoreVersion;
19-
import org.springframework.util.Assert;
20-
2118
import java.io.Serializable;
19+
import java.time.Instant;
2220
import java.util.Collections;
2321
import java.util.LinkedHashMap;
2422
import java.util.Map;
23+
import java.util.function.Consumer;
24+
25+
import org.springframework.security.core.SpringSecurityCoreVersion;
26+
import org.springframework.util.Assert;
27+
28+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.ADDRESS;
29+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.BIRTHDATE;
30+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.EMAIL;
31+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.EMAIL_VERIFIED;
32+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.FAMILY_NAME;
33+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.GENDER;
34+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.GIVEN_NAME;
35+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.LOCALE;
36+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.MIDDLE_NAME;
37+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.NAME;
38+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.NICKNAME;
39+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.PHONE_NUMBER;
40+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.PHONE_NUMBER_VERIFIED;
41+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.PICTURE;
42+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.PREFERRED_USERNAME;
43+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.PROFILE;
44+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.SUB;
45+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.UPDATED_AT;
46+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.WEBSITE;
47+
import static org.springframework.security.oauth2.core.oidc.StandardClaimNames.ZONEINFO;
2548

2649
/**
2750
* A representation of a UserInfo Response that is returned
@@ -74,4 +97,257 @@ public boolean equals(Object obj) {
7497
public int hashCode() {
7598
return this.getClaims().hashCode();
7699
}
100+
101+
/**
102+
* Create a {@link Builder}
103+
*
104+
* @return the {@link Builder} for further configuration
105+
*/
106+
public static Builder builder() {
107+
return new Builder();
108+
}
109+
110+
/**
111+
* A builder for {@link OidcUserInfo}s
112+
*
113+
* @since 5.3
114+
* @author Josh Cummings
115+
*/
116+
public static final class Builder {
117+
private final Map<String, Object> claims = new LinkedHashMap<>();
118+
119+
private Builder() {}
120+
121+
/**
122+
* Use this claim in the resulting {@link OidcUserInfo}
123+
*
124+
* @param name The claim name
125+
* @param value The claim value
126+
* @return the {@link Builder} for further configurations
127+
*/
128+
public Builder claim(String name, Object value) {
129+
this.claims.put(name, value);
130+
return this;
131+
}
132+
133+
/**
134+
* Provides access to every {@link #claim(String, Object)}
135+
* declared so far with the possibility to add, replace, or remove.
136+
* @param claimsConsumer the consumer
137+
* @return the {@link Builder} for further configurations
138+
*/
139+
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
140+
claimsConsumer.accept(this.claims);
141+
return this;
142+
}
143+
144+
/**
145+
* Use this address in the resulting {@link OidcUserInfo}
146+
*
147+
* @param address The address to use
148+
* @return the {@link Builder} for further configurations
149+
*/
150+
public Builder address(String address) {
151+
return this.claim(ADDRESS, address);
152+
}
153+
154+
/**
155+
* Use this birthdate in the resulting {@link OidcUserInfo}
156+
*
157+
* @param birthdate The birthdate to use
158+
* @return the {@link Builder} for further configurations
159+
*/
160+
public Builder birthdate(String birthdate) {
161+
return this.claim(BIRTHDATE, birthdate);
162+
}
163+
164+
/**
165+
* Use this email in the resulting {@link OidcUserInfo}
166+
*
167+
* @param email The email to use
168+
* @return the {@link Builder} for further configurations
169+
*/
170+
public Builder email(String email) {
171+
return this.claim(EMAIL, email);
172+
}
173+
174+
/**
175+
* Use this verified-email indicator in the resulting {@link OidcUserInfo}
176+
*
177+
* @param emailVerified The verified-email indicator to use
178+
* @return the {@link Builder} for further configurations
179+
*/
180+
public Builder emailVerified(Boolean emailVerified) {
181+
return this.claim(EMAIL_VERIFIED, emailVerified);
182+
}
183+
184+
/**
185+
* Use this family name in the resulting {@link OidcUserInfo}
186+
*
187+
* @param familyName The family name to use
188+
* @return the {@link Builder} for further configurations
189+
*/
190+
public Builder familyName(String familyName) {
191+
return claim(FAMILY_NAME, familyName);
192+
}
193+
194+
/**
195+
* Use this gender in the resulting {@link OidcUserInfo}
196+
*
197+
* @param gender The gender to use
198+
* @return the {@link Builder} for further configurations
199+
*/
200+
public Builder gender(String gender) {
201+
return this.claim(GENDER, gender);
202+
}
203+
204+
/**
205+
* Use this given name in the resulting {@link OidcUserInfo}
206+
*
207+
* @param givenName The given name to use
208+
* @return the {@link Builder} for further configurations
209+
*/
210+
public Builder givenName(String givenName) {
211+
return claim(GIVEN_NAME, givenName);
212+
}
213+
214+
/**
215+
* Use this locale in the resulting {@link OidcUserInfo}
216+
*
217+
* @param locale The locale to use
218+
* @return the {@link Builder} for further configurations
219+
*/
220+
public Builder locale(String locale) {
221+
return this.claim(LOCALE, locale);
222+
}
223+
224+
/**
225+
* Use this middle name in the resulting {@link OidcUserInfo}
226+
*
227+
* @param middleName The middle name to use
228+
* @return the {@link Builder} for further configurations
229+
*/
230+
public Builder middleName(String middleName) {
231+
return claim(MIDDLE_NAME, middleName);
232+
}
233+
234+
/**
235+
* Use this name in the resulting {@link OidcUserInfo}
236+
*
237+
* @param name The name to use
238+
* @return the {@link Builder} for further configurations
239+
*/
240+
public Builder name(String name) {
241+
return claim(NAME, name);
242+
}
243+
244+
/**
245+
* Use this nickname in the resulting {@link OidcUserInfo}
246+
*
247+
* @param nickname The nickname to use
248+
* @return the {@link Builder} for further configurations
249+
*/
250+
public Builder nickname(String nickname) {
251+
return claim(NICKNAME, nickname);
252+
}
253+
254+
/**
255+
* Use this picture in the resulting {@link OidcUserInfo}
256+
*
257+
* @param picture The picture to use
258+
* @return the {@link Builder} for further configurations
259+
*/
260+
public Builder picture(String picture) {
261+
return this.claim(PICTURE, picture);
262+
}
263+
264+
/**
265+
* Use this phone number in the resulting {@link OidcUserInfo}
266+
*
267+
* @param phoneNumber The phone number to use
268+
* @return the {@link Builder} for further configurations
269+
*/
270+
public Builder phoneNumber(String phoneNumber) {
271+
return this.claim(PHONE_NUMBER, phoneNumber);
272+
}
273+
274+
/**
275+
* Use this verified-phone-number indicator in the resulting {@link OidcUserInfo}
276+
*
277+
* @param phoneNumberVerified The verified-phone-number indicator to use
278+
* @return the {@link Builder} for further configurations
279+
*/
280+
public Builder phoneNumberVerified(String phoneNumberVerified) {
281+
return this.claim(PHONE_NUMBER_VERIFIED, phoneNumberVerified);
282+
}
283+
284+
/**
285+
* Use this preferred username in the resulting {@link OidcUserInfo}
286+
*
287+
* @param preferredUsername The preferred username to use
288+
* @return the {@link Builder} for further configurations
289+
*/
290+
public Builder preferredUsername(String preferredUsername) {
291+
return claim(PREFERRED_USERNAME, preferredUsername);
292+
}
293+
294+
/**
295+
* Use this profile in the resulting {@link OidcUserInfo}
296+
*
297+
* @param profile The profile to use
298+
* @return the {@link Builder} for further configurations
299+
*/
300+
public Builder profile(String profile) {
301+
return claim(PROFILE, profile);
302+
}
303+
304+
/**
305+
* Use this subject in the resulting {@link OidcUserInfo}
306+
*
307+
* @param subject The subject to use
308+
* @return the {@link Builder} for further configurations
309+
*/
310+
public Builder subject(String subject) {
311+
return this.claim(SUB, subject);
312+
}
313+
314+
/**
315+
* Use this updated-at {@link Instant} in the resulting {@link OidcUserInfo}
316+
*
317+
* @param updatedAt The updated-at {@link Instant} to use
318+
* @return the {@link Builder} for further configurations
319+
*/
320+
public Builder updatedAt(String updatedAt) {
321+
return this.claim(UPDATED_AT, updatedAt);
322+
}
323+
324+
/**
325+
* Use this website in the resulting {@link OidcUserInfo}
326+
*
327+
* @param website The website to use
328+
* @return the {@link Builder} for further configurations
329+
*/
330+
public Builder website(String website) {
331+
return this.claim(WEBSITE, website);
332+
}
333+
334+
/**
335+
* Use this zoneinfo in the resulting {@link OidcUserInfo}
336+
*
337+
* @param zoneinfo The zoneinfo to use
338+
* @return the {@link Builder} for further configurations
339+
*/
340+
public Builder zoneinfo(String zoneinfo) {
341+
return this.claim(ZONEINFO, zoneinfo);
342+
}
343+
344+
/**
345+
* Build the {@link OidcUserInfo}
346+
*
347+
* @return The constructed {@link OidcUserInfo}
348+
*/
349+
public OidcUserInfo build() {
350+
return new OidcUserInfo(this.claims);
351+
}
352+
}
77353
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 org.junit.Test;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.springframework.security.oauth2.core.oidc.IdTokenClaimNames.SUB;
23+
24+
/**
25+
* Tests for {@link OidcUserInfo}
26+
*/
27+
public class OidcUserInfoBuilderTests {
28+
@Test
29+
public void buildWhenCalledTwiceThenGeneratesTwoOidcUserInfos() {
30+
OidcUserInfo.Builder userInfoBuilder = OidcUserInfo.builder();
31+
32+
OidcUserInfo first = userInfoBuilder
33+
.claim("TEST_CLAIM_1", "C1")
34+
.build();
35+
36+
OidcUserInfo second = userInfoBuilder
37+
.claim("TEST_CLAIM_1", "C2")
38+
.claim("TEST_CLAIM_2", "C3")
39+
.build();
40+
41+
assertThat(first.getClaims()).hasSize(1);
42+
assertThat(first.getClaims().get("TEST_CLAIM_1")).isEqualTo("C1");
43+
44+
assertThat(second.getClaims()).hasSize(2);
45+
assertThat(second.getClaims().get("TEST_CLAIM_1")).isEqualTo("C2");
46+
assertThat(second.getClaims().get("TEST_CLAIM_2")).isEqualTo("C3");
47+
}
48+
49+
@Test
50+
public void subjectWhenUsingGenericOrNamedClaimMethodThenLastOneWins() {
51+
OidcUserInfo.Builder userInfoBuilder = OidcUserInfo.builder();
52+
53+
String generic = new String("sub");
54+
String named = new String("sub");
55+
56+
OidcUserInfo userInfo = userInfoBuilder
57+
.subject(named)
58+
.claim(SUB, generic).build();
59+
assertThat(userInfo.getSubject()).isSameAs(generic);
60+
61+
userInfo = userInfoBuilder
62+
.claim(SUB, generic)
63+
.subject(named).build();
64+
assertThat(userInfo.getSubject()).isSameAs(named);
65+
}
66+
67+
@Test
68+
public void claimsWhenRemovingAClaimThenIsNotPresent() {
69+
OidcUserInfo.Builder userInfoBuilder = OidcUserInfo.builder()
70+
.claim("needs", "a claim");
71+
72+
OidcUserInfo userInfo = userInfoBuilder
73+
.subject("sub")
74+
.claims(claims -> claims.remove(SUB))
75+
.build();
76+
assertThat(userInfo.getSubject()).isNull();
77+
}
78+
79+
@Test
80+
public void claimsWhenAddingAClaimThenIsPresent() {
81+
OidcUserInfo.Builder userInfoBuilder = OidcUserInfo.builder();
82+
83+
String name = new String("name");
84+
String value = new String("value");
85+
OidcUserInfo userInfo = userInfoBuilder
86+
.claims(claims -> claims.put(name, value))
87+
.build();
88+
89+
assertThat(userInfo.getClaims()).hasSize(1);
90+
assertThat(userInfo.getClaims().get(name)).isSameAs(value);
91+
}
92+
}

0 commit comments

Comments
 (0)