Skip to content

Commit 6ff71d8

Browse files
committed
Add OidcUserInfo.Builder
Fixes gh-7593
1 parent c767751 commit 6ff71d8

File tree

2 files changed

+372
-3
lines changed

2 files changed

+372
-3
lines changed

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

Lines changed: 280 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,258 @@ 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+
* @since 5.3
106+
*/
107+
public static Builder builder() {
108+
return new Builder();
109+
}
110+
111+
/**
112+
* A builder for {@link OidcUserInfo}s
113+
*
114+
* @author Josh Cummings
115+
* @since 5.3
116+
*/
117+
public static final class Builder {
118+
private final Map<String, Object> claims = new LinkedHashMap<>();
119+
120+
private Builder() {}
121+
122+
/**
123+
* Use this claim in the resulting {@link OidcUserInfo}
124+
*
125+
* @param name The claim name
126+
* @param value The claim value
127+
* @return the {@link Builder} for further configurations
128+
*/
129+
public Builder claim(String name, Object value) {
130+
this.claims.put(name, value);
131+
return this;
132+
}
133+
134+
/**
135+
* Provides access to every {@link #claim(String, Object)}
136+
* declared so far with the possibility to add, replace, or remove.
137+
* @param claimsConsumer the consumer
138+
* @return the {@link Builder} for further configurations
139+
*/
140+
public Builder claims(Consumer<Map<String, Object>> claimsConsumer) {
141+
claimsConsumer.accept(this.claims);
142+
return this;
143+
}
144+
145+
/**
146+
* Use this address in the resulting {@link OidcUserInfo}
147+
*
148+
* @param address The address to use
149+
* @return the {@link Builder} for further configurations
150+
*/
151+
public Builder address(String address) {
152+
return this.claim(ADDRESS, address);
153+
}
154+
155+
/**
156+
* Use this birthdate in the resulting {@link OidcUserInfo}
157+
*
158+
* @param birthdate The birthdate to use
159+
* @return the {@link Builder} for further configurations
160+
*/
161+
public Builder birthdate(String birthdate) {
162+
return this.claim(BIRTHDATE, birthdate);
163+
}
164+
165+
/**
166+
* Use this email in the resulting {@link OidcUserInfo}
167+
*
168+
* @param email The email to use
169+
* @return the {@link Builder} for further configurations
170+
*/
171+
public Builder email(String email) {
172+
return this.claim(EMAIL, email);
173+
}
174+
175+
/**
176+
* Use this verified-email indicator in the resulting {@link OidcUserInfo}
177+
*
178+
* @param emailVerified The verified-email indicator to use
179+
* @return the {@link Builder} for further configurations
180+
*/
181+
public Builder emailVerified(Boolean emailVerified) {
182+
return this.claim(EMAIL_VERIFIED, emailVerified);
183+
}
184+
185+
/**
186+
* Use this family name in the resulting {@link OidcUserInfo}
187+
*
188+
* @param familyName The family name to use
189+
* @return the {@link Builder} for further configurations
190+
*/
191+
public Builder familyName(String familyName) {
192+
return claim(FAMILY_NAME, familyName);
193+
}
194+
195+
/**
196+
* Use this gender in the resulting {@link OidcUserInfo}
197+
*
198+
* @param gender The gender to use
199+
* @return the {@link Builder} for further configurations
200+
*/
201+
public Builder gender(String gender) {
202+
return this.claim(GENDER, gender);
203+
}
204+
205+
/**
206+
* Use this given name in the resulting {@link OidcUserInfo}
207+
*
208+
* @param givenName The given name to use
209+
* @return the {@link Builder} for further configurations
210+
*/
211+
public Builder givenName(String givenName) {
212+
return claim(GIVEN_NAME, givenName);
213+
}
214+
215+
/**
216+
* Use this locale in the resulting {@link OidcUserInfo}
217+
*
218+
* @param locale The locale to use
219+
* @return the {@link Builder} for further configurations
220+
*/
221+
public Builder locale(String locale) {
222+
return this.claim(LOCALE, locale);
223+
}
224+
225+
/**
226+
* Use this middle name in the resulting {@link OidcUserInfo}
227+
*
228+
* @param middleName The middle name to use
229+
* @return the {@link Builder} for further configurations
230+
*/
231+
public Builder middleName(String middleName) {
232+
return claim(MIDDLE_NAME, middleName);
233+
}
234+
235+
/**
236+
* Use this name in the resulting {@link OidcUserInfo}
237+
*
238+
* @param name The name to use
239+
* @return the {@link Builder} for further configurations
240+
*/
241+
public Builder name(String name) {
242+
return claim(NAME, name);
243+
}
244+
245+
/**
246+
* Use this nickname in the resulting {@link OidcUserInfo}
247+
*
248+
* @param nickname The nickname to use
249+
* @return the {@link Builder} for further configurations
250+
*/
251+
public Builder nickname(String nickname) {
252+
return claim(NICKNAME, nickname);
253+
}
254+
255+
/**
256+
* Use this picture in the resulting {@link OidcUserInfo}
257+
*
258+
* @param picture The picture to use
259+
* @return the {@link Builder} for further configurations
260+
*/
261+
public Builder picture(String picture) {
262+
return this.claim(PICTURE, picture);
263+
}
264+
265+
/**
266+
* Use this phone number in the resulting {@link OidcUserInfo}
267+
*
268+
* @param phoneNumber The phone number to use
269+
* @return the {@link Builder} for further configurations
270+
*/
271+
public Builder phoneNumber(String phoneNumber) {
272+
return this.claim(PHONE_NUMBER, phoneNumber);
273+
}
274+
275+
/**
276+
* Use this verified-phone-number indicator in the resulting {@link OidcUserInfo}
277+
*
278+
* @param phoneNumberVerified The verified-phone-number indicator to use
279+
* @return the {@link Builder} for further configurations
280+
*/
281+
public Builder phoneNumberVerified(String phoneNumberVerified) {
282+
return this.claim(PHONE_NUMBER_VERIFIED, phoneNumberVerified);
283+
}
284+
285+
/**
286+
* Use this preferred username in the resulting {@link OidcUserInfo}
287+
*
288+
* @param preferredUsername The preferred username to use
289+
* @return the {@link Builder} for further configurations
290+
*/
291+
public Builder preferredUsername(String preferredUsername) {
292+
return claim(PREFERRED_USERNAME, preferredUsername);
293+
}
294+
295+
/**
296+
* Use this profile in the resulting {@link OidcUserInfo}
297+
*
298+
* @param profile The profile to use
299+
* @return the {@link Builder} for further configurations
300+
*/
301+
public Builder profile(String profile) {
302+
return claim(PROFILE, profile);
303+
}
304+
305+
/**
306+
* Use this subject in the resulting {@link OidcUserInfo}
307+
*
308+
* @param subject The subject to use
309+
* @return the {@link Builder} for further configurations
310+
*/
311+
public Builder subject(String subject) {
312+
return this.claim(SUB, subject);
313+
}
314+
315+
/**
316+
* Use this updated-at {@link Instant} in the resulting {@link OidcUserInfo}
317+
*
318+
* @param updatedAt The updated-at {@link Instant} to use
319+
* @return the {@link Builder} for further configurations
320+
*/
321+
public Builder updatedAt(String updatedAt) {
322+
return this.claim(UPDATED_AT, updatedAt);
323+
}
324+
325+
/**
326+
* Use this website in the resulting {@link OidcUserInfo}
327+
*
328+
* @param website The website to use
329+
* @return the {@link Builder} for further configurations
330+
*/
331+
public Builder website(String website) {
332+
return this.claim(WEBSITE, website);
333+
}
334+
335+
/**
336+
* Use this zoneinfo in the resulting {@link OidcUserInfo}
337+
*
338+
* @param zoneinfo The zoneinfo to use
339+
* @return the {@link Builder} for further configurations
340+
*/
341+
public Builder zoneinfo(String zoneinfo) {
342+
return this.claim(ZONEINFO, zoneinfo);
343+
}
344+
345+
/**
346+
* Build the {@link OidcUserInfo}
347+
*
348+
* @return The constructed {@link OidcUserInfo}
349+
*/
350+
public OidcUserInfo build() {
351+
return new OidcUserInfo(this.claims);
352+
}
353+
}
77354
}
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)