1818import java .time .Instant ;
1919import java .util .Arrays ;
2020import java .util .HashSet ;
21- import java .util .List ;
2221import java .util .Set ;
22+ import java .util .function .Function ;
2323
2424import com .nimbusds .jose .jwk .JWKSet ;
2525import com .nimbusds .jose .jwk .source .ImmutableJWKSet ;
3737import org .springframework .security .config .annotation .web .configurers .oauth2 .server .resource .OAuth2ResourceServerConfigurer ;
3838import org .springframework .security .config .test .SpringTestRule ;
3939import org .springframework .security .oauth2 .core .OAuth2AccessToken ;
40+ import org .springframework .security .oauth2 .core .authentication .OAuth2AuthenticationContext ;
4041import org .springframework .security .oauth2 .core .oidc .OidcIdToken ;
4142import org .springframework .security .oauth2 .core .oidc .OidcScopes ;
4243import org .springframework .security .oauth2 .core .oidc .OidcUserInfo ;
43- import org .springframework .security .oauth2 .core .oidc .StandardClaimNames ;
4444import org .springframework .security .oauth2 .jose .TestJwks ;
4545import org .springframework .security .oauth2 .jose .jws .SignatureAlgorithm ;
4646import org .springframework .security .oauth2 .jwt .JoseHeader ;
5757import org .springframework .security .oauth2 .server .authorization .client .RegisteredClient ;
5858import org .springframework .security .oauth2 .server .authorization .client .RegisteredClientRepository ;
5959import org .springframework .security .oauth2 .server .authorization .client .TestRegisteredClients ;
60+ import org .springframework .security .oauth2 .server .authorization .oidc .authentication .OidcUserInfoAuthenticationToken ;
61+ import org .springframework .security .oauth2 .server .resource .authentication .JwtAuthenticationToken ;
6062import org .springframework .security .web .SecurityFilterChain ;
6163import org .springframework .security .web .util .matcher .RequestMatcher ;
6264import org .springframework .test .web .servlet .MockMvc ;
65+ import org .springframework .test .web .servlet .ResultMatcher ;
6366
67+ import static org .springframework .test .web .servlet .ResultMatcher .matchAll ;
6468import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .get ;
6569import static org .springframework .test .web .servlet .request .MockMvcRequestBuilders .post ;
6670import static org .springframework .test .web .servlet .result .MockMvcResultMatchers .jsonPath ;
7377 */
7478public class OidcUserInfoTests {
7579 private static final String DEFAULT_OIDC_USER_INFO_ENDPOINT_URI = "/userinfo" ;
76- private static final List <String > OPENID_USER_INFO_SCOPES = Arrays .asList (
77- OidcScopes .OPENID , OidcScopes .ADDRESS , OidcScopes .EMAIL , OidcScopes .PHONE , OidcScopes .PROFILE
78- );
7980
8081 @ Rule
8182 public final SpringTestRule spring = new SpringTestRule ();
@@ -96,30 +97,12 @@ public void requestWhenUserInfoRequestGetThenUserInfoResponse() throws Exception
9697 OAuth2Authorization authorization = createAuthorization ();
9798 this .authorizationService .save (authorization );
9899
100+ OAuth2AccessToken accessToken = authorization .getAccessToken ().getToken ();
99101 // @formatter:off
100102 this .mvc .perform (get (DEFAULT_OIDC_USER_INFO_ENDPOINT_URI )
101- .header (HttpHeaders .AUTHORIZATION , "Bearer " + authorization . getAccessToken (). getToken () .getTokenValue ()))
103+ .header (HttpHeaders .AUTHORIZATION , "Bearer " + accessToken .getTokenValue ()))
102104 .andExpect (status ().is2xxSuccessful ())
103- .andExpect (jsonPath ("sub" ).value ("user1" ))
104- .andExpect (jsonPath ("name" ).value ("First Last" ))
105- .andExpect (jsonPath ("given_name" ).value ("First" ))
106- .andExpect (jsonPath ("family_name" ).value ("Last" ))
107- .andExpect (jsonPath ("middle_name" ).value ("Middle" ))
108- .andExpect (jsonPath ("nickname" ).value ("User" ))
109- .andExpect (jsonPath ("preferred_username" ).value ("user" ))
110- .andExpect (jsonPath ("profile" ).value ("https://example.com/user1" ))
111- .andExpect (jsonPath ("picture" ).value ("https://example.com/user1.jpg" ))
112- .andExpect (jsonPath ("website" ).value ("https://example.com" ))
113- .andExpect (jsonPath ("email" ).value ("user1@example.com" ))
114- .andExpect (jsonPath ("email_verified" ).value ("true" ))
115- .andExpect (jsonPath ("gender" ).value ("female" ))
116- .andExpect (jsonPath ("birthdate" ).value ("1970-01-01" ))
117- .andExpect (jsonPath ("zoneinfo" ).value ("Europe/Paris" ))
118- .andExpect (jsonPath ("locale" ).value ("en-US" ))
119- .andExpect (jsonPath ("phone_number" ).value ("+1 (604) 555-1234;ext=5678" ))
120- .andExpect (jsonPath ("phone_number_verified" ).value ("false" ))
121- .andExpect (jsonPath ("address" ).value ("Champ de Mars\n 5 Av. Anatole France\n 75007 Paris\n France" ))
122- .andExpect (jsonPath ("updated_at" ).value ("1970-01-01T00:00:00Z" ));
105+ .andExpect (userInfoResponse ());
123106 // @formatter:on
124107 }
125108
@@ -130,40 +113,70 @@ public void requestWhenUserInfoRequestPostThenUserInfoResponse() throws Exceptio
130113 OAuth2Authorization authorization = createAuthorization ();
131114 this .authorizationService .save (authorization );
132115
116+ OAuth2AccessToken accessToken = authorization .getAccessToken ().getToken ();
133117 // @formatter:off
134118 this .mvc .perform (post (DEFAULT_OIDC_USER_INFO_ENDPOINT_URI )
135- .header (HttpHeaders .AUTHORIZATION , "Bearer " + authorization . getAccessToken (). getToken () .getTokenValue ()))
119+ .header (HttpHeaders .AUTHORIZATION , "Bearer " + accessToken .getTokenValue ()))
136120 .andExpect (status ().is2xxSuccessful ())
137- .andExpect (jsonPath ("sub" ).value ("user1" ))
138- .andExpect (jsonPath ("name" ).value ("First Last" ))
139- .andExpect (jsonPath ("given_name" ).value ("First" ))
140- .andExpect (jsonPath ("family_name" ).value ("Last" ))
141- .andExpect (jsonPath ("middle_name" ).value ("Middle" ))
142- .andExpect (jsonPath ("nickname" ).value ("User" ))
143- .andExpect (jsonPath ("preferred_username" ).value ("user" ))
144- .andExpect (jsonPath ("profile" ).value ("https://example.com/user1" ))
145- .andExpect (jsonPath ("picture" ).value ("https://example.com/user1.jpg" ))
146- .andExpect (jsonPath ("website" ).value ("https://example.com" ))
147- .andExpect (jsonPath ("email" ).value ("user1@example.com" ))
148- .andExpect (jsonPath ("email_verified" ).value ("true" ))
149- .andExpect (jsonPath ("gender" ).value ("female" ))
150- .andExpect (jsonPath ("birthdate" ).value ("1970-01-01" ))
151- .andExpect (jsonPath ("zoneinfo" ).value ("Europe/Paris" ))
152- .andExpect (jsonPath ("locale" ).value ("en-US" ))
153- .andExpect (jsonPath ("phone_number" ).value ("+1 (604) 555-1234;ext=5678" ))
154- .andExpect (jsonPath ("phone_number_verified" ).value ("false" ))
155- .andExpect (jsonPath ("address" ).value ("Champ de Mars\n 5 Av. Anatole France\n 75007 Paris\n France" ))
156- .andExpect (jsonPath ("updated_at" ).value ("1970-01-01T00:00:00Z" ));
121+ .andExpect (userInfoResponse ());
122+ // @formatter:on
123+ }
124+
125+ @ Test
126+ public void requestWhenSignedJwtAndCustomUserInfoMapperThenUserInfoResponse () throws Exception {
127+ this .spring .register (CustomUserInfoConfiguration .class ).autowire ();
128+
129+ OAuth2Authorization authorization = createAuthorization ();
130+ this .authorizationService .save (authorization );
131+
132+ OAuth2AccessToken accessToken = authorization .getAccessToken ().getToken ();
133+ // @formatter:off
134+ this .mvc .perform (get (DEFAULT_OIDC_USER_INFO_ENDPOINT_URI )
135+ .header (HttpHeaders .AUTHORIZATION , "Bearer " + accessToken .getTokenValue ()))
136+ .andExpect (status ().is2xxSuccessful ())
137+ .andExpect (userInfoResponse ());
138+ // @formatter:on
139+ }
140+
141+ private static ResultMatcher userInfoResponse () {
142+ // @formatter:off
143+ return matchAll (
144+ jsonPath ("sub" ).value ("user1" ),
145+ jsonPath ("name" ).value ("First Last" ),
146+ jsonPath ("given_name" ).value ("First" ),
147+ jsonPath ("family_name" ).value ("Last" ),
148+ jsonPath ("middle_name" ).value ("Middle" ),
149+ jsonPath ("nickname" ).value ("User" ),
150+ jsonPath ("preferred_username" ).value ("user" ),
151+ jsonPath ("profile" ).value ("https://example.com/user1" ),
152+ jsonPath ("picture" ).value ("https://example.com/user1.jpg" ),
153+ jsonPath ("website" ).value ("https://example.com" ),
154+ jsonPath ("email" ).value ("user1@example.com" ),
155+ jsonPath ("email_verified" ).value ("true" ),
156+ jsonPath ("gender" ).value ("female" ),
157+ jsonPath ("birthdate" ).value ("1970-01-01" ),
158+ jsonPath ("zoneinfo" ).value ("Europe/Paris" ),
159+ jsonPath ("locale" ).value ("en-US" ),
160+ jsonPath ("phone_number" ).value ("+1 (604) 555-1234;ext=5678" ),
161+ jsonPath ("phone_number_verified" ).value ("false" ),
162+ jsonPath ("address" ).value ("Champ de Mars\n 5 Av. Anatole France\n 75007 Paris\n France" ),
163+ jsonPath ("updated_at" ).value ("1970-01-01T00:00:00Z" )
164+ );
157165 // @formatter:on
158166 }
159167
160168 private OAuth2Authorization createAuthorization () {
161169 JoseHeader headers = JoseHeader .withAlgorithm (SignatureAlgorithm .RS256 ).build ();
162- JwtClaimsSet claimSet = JwtClaimsSet .builder ().claim (StandardClaimNames .SUB , "user" ).build ();
170+ // @formatter:off
171+ JwtClaimsSet claimSet = JwtClaimsSet .builder ()
172+ .claims (claims -> claims .putAll (createUserInfo ().getClaims ()))
173+ .build ();
174+ // @formatter:on
163175 Jwt jwt = this .jwtEncoder .encode (headers , claimSet );
164176
165177 Instant now = Instant .now ();
166- Set <String > scopes = new HashSet <>(OPENID_USER_INFO_SCOPES );
178+ Set <String > scopes = new HashSet <>(Arrays .asList (
179+ OidcScopes .OPENID , OidcScopes .ADDRESS , OidcScopes .EMAIL , OidcScopes .PHONE , OidcScopes .PROFILE ));
167180 OAuth2AccessToken accessToken = new OAuth2AccessToken (
168181 OAuth2AccessToken .TokenType .BEARER , jwt .getTokenValue (), now , now .plusSeconds (300 ), scopes );
169182 OidcIdToken idToken = OidcIdToken .withTokenValue ("id-token" )
@@ -203,6 +216,45 @@ private static OidcUserInfo createUserInfo() {
203216 // @formatter:on
204217 }
205218
219+ @ EnableWebSecurity
220+ static class CustomUserInfoConfiguration extends AuthorizationServerConfiguration {
221+
222+ @ Bean
223+ @ Override
224+ SecurityFilterChain securityFilterChain (HttpSecurity http ) throws Exception {
225+ OAuth2AuthorizationServerConfigurer <HttpSecurity > authorizationServerConfigurer =
226+ new OAuth2AuthorizationServerConfigurer <>();
227+ RequestMatcher endpointsMatcher = authorizationServerConfigurer
228+ .getEndpointsMatcher ();
229+
230+ // Custom User Info Mapper that retrieves claims from a signed JWT
231+ Function <OAuth2AuthenticationContext , OidcUserInfo > userInfoMapper = context -> {
232+ OidcUserInfoAuthenticationToken authentication = context .getAuthentication ();
233+ JwtAuthenticationToken principal = (JwtAuthenticationToken ) authentication .getPrincipal ();
234+
235+ return new OidcUserInfo (principal .getToken ().getClaims ());
236+ };
237+
238+ // @formatter:off
239+ http
240+ .requestMatcher (endpointsMatcher )
241+ .authorizeRequests (authorizeRequests ->
242+ authorizeRequests .anyRequest ().authenticated ()
243+ )
244+ .csrf (csrf -> csrf .ignoringRequestMatchers (endpointsMatcher ))
245+ .oauth2ResourceServer (OAuth2ResourceServerConfigurer ::jwt )
246+ .apply (authorizationServerConfigurer )
247+ .oidc (oidc -> oidc
248+ .userInfoEndpoint (userInfo -> userInfo
249+ .userInfoMapper (userInfoMapper )
250+ )
251+ );
252+ // @formatter:on
253+
254+ return http .build ();
255+ }
256+ }
257+
206258 @ EnableWebSecurity
207259 static class AuthorizationServerConfiguration {
208260
0 commit comments