Add "How-to: Customize the OpenID Connect 1.0 UserInfo response"
Closes gh-537
This commit is contained in:
committed by
Steve Riesenberg
parent
ecda4e0a15
commit
9171006952
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.userinfo;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
|
||||
@Configuration
|
||||
public class EnableUserInfoSecurityConfig {
|
||||
|
||||
@Bean // <1>
|
||||
@Order(1)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
// @formatter:off
|
||||
http
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) // <2>
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// @fold:on
|
||||
@Bean
|
||||
@Order(2)
|
||||
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
@Bean // <3>
|
||||
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
// @fold:on
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new InMemoryUserDetailsManager(userDetails);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RegisteredClientRepository registeredClientRepository() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId("messaging-client")
|
||||
.clientSecret("{noop}secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope(OidcScopes.ADDRESS)
|
||||
.scope(OidcScopes.EMAIL)
|
||||
.scope(OidcScopes.PHONE)
|
||||
.scope(OidcScopes.PROFILE)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
KeyPair keyPair = generateRsaKey();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
// @formatter:off
|
||||
RSAKey rsaKey = new RSAKey.Builder(publicKey)
|
||||
.privateKey(privateKey)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
JWKSet jwkSet = new JWKSet(rsaKey);
|
||||
return new ImmutableJWKSet<>(jwkSet);
|
||||
}
|
||||
|
||||
private static KeyPair generateRsaKey() { // <6>
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder().issuer("http://localhost:9000").build();
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.userinfo.idtoken;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
|
||||
@Configuration
|
||||
public class IdTokenCustomizerConfig {
|
||||
|
||||
// @formatter:off
|
||||
@Bean // <1>
|
||||
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer(
|
||||
OidcUserInfoService userInfoService) {
|
||||
return (context) -> {
|
||||
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
|
||||
OidcUserInfo userInfo = userInfoService.loadUser( // <2>
|
||||
context.getPrincipal().getName());
|
||||
context.getClaims().claims(claims ->
|
||||
claims.putAll(userInfo.getClaims()));
|
||||
}
|
||||
};
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.userinfo.idtoken;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Example service to perform lookup of user info for customizing an {@code id_token}.
|
||||
*/
|
||||
@Service
|
||||
public class OidcUserInfoService {
|
||||
|
||||
private final UserInfoRepository userInfoRepository = new UserInfoRepository();
|
||||
|
||||
public OidcUserInfo loadUser(String username) {
|
||||
return new OidcUserInfo(this.userInfoRepository.findByUsername(username));
|
||||
}
|
||||
|
||||
static class UserInfoRepository {
|
||||
|
||||
private final Map<String, Map<String, Object>> userInfo = new HashMap<>();
|
||||
|
||||
public UserInfoRepository() {
|
||||
this.userInfo.put("user1", createUser("user1"));
|
||||
this.userInfo.put("user2", createUser("user2"));
|
||||
}
|
||||
|
||||
public Map<String, Object> findByUsername(String username) {
|
||||
return this.userInfo.get(username);
|
||||
}
|
||||
|
||||
private static Map<String, Object> createUser(String username) {
|
||||
return OidcUserInfo.builder()
|
||||
.subject(username)
|
||||
.name("First Last")
|
||||
.givenName("First")
|
||||
.familyName("Last")
|
||||
.middleName("Middle")
|
||||
.nickname("User")
|
||||
.preferredUsername(username)
|
||||
.profile("https://example.com/" + username)
|
||||
.picture("https://example.com/" + username + ".jpg")
|
||||
.website("https://example.com")
|
||||
.email(username + "@example.com")
|
||||
.emailVerified(true)
|
||||
.gender("female")
|
||||
.birthdate("1970-01-01")
|
||||
.zoneinfo("Europe/Paris")
|
||||
.locale("en-US")
|
||||
.phoneNumber("+1 (604) 555-1234;ext=5678")
|
||||
.phoneNumberVerified("false")
|
||||
.claim("address", Collections.singletonMap("formatted", "Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"))
|
||||
.updatedAt("1970-01-01T00:00:00Z")
|
||||
.build()
|
||||
.getClaims();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.userinfo.jwt;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||
|
||||
@Configuration
|
||||
public class JwtTokenCustomizerConfig {
|
||||
|
||||
// @formatter:off
|
||||
@Bean
|
||||
public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
|
||||
return (context) -> {
|
||||
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
|
||||
context.getClaims().claims((claims) -> {
|
||||
claims.put("claim-1", "value-1");
|
||||
claims.put("claim-2", "value-2");
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.userinfo.jwt;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
|
||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
@Configuration
|
||||
public class JwtUserInfoMapperSecurityConfig {
|
||||
|
||||
@Bean // <1>
|
||||
@Order(1)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer
|
||||
.getEndpointsMatcher();
|
||||
|
||||
Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper = (context) -> { // <2>
|
||||
OidcUserInfoAuthenticationToken authentication = context.getAuthentication();
|
||||
JwtAuthenticationToken principal = (JwtAuthenticationToken) authentication.getPrincipal();
|
||||
|
||||
return new OidcUserInfo(principal.getToken().getClaims());
|
||||
};
|
||||
|
||||
// @formatter:off
|
||||
authorizationServerConfigurer
|
||||
.oidc((oidc) -> oidc
|
||||
.userInfoEndpoint((userInfo) -> userInfo
|
||||
.userInfoMapper(userInfoMapper) // <3>
|
||||
)
|
||||
);
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) // <4>
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
|
||||
)
|
||||
.apply(authorizationServerConfigurer); // <5>
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// @fold:on
|
||||
@Bean
|
||||
@Order(2)
|
||||
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
// @formatter:off
|
||||
http
|
||||
.authorizeHttpRequests((authorize) -> authorize
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults());
|
||||
// @formatter:on
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean // <3>
|
||||
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
// @formatter:off
|
||||
UserDetails userDetails = User.withDefaultPasswordEncoder()
|
||||
.username("user")
|
||||
.password("password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new InMemoryUserDetailsManager(userDetails);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RegisteredClientRepository registeredClientRepository() {
|
||||
// @formatter:off
|
||||
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId("messaging-client")
|
||||
.clientSecret("{noop}secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
// @formatter:on
|
||||
|
||||
return new InMemoryRegisteredClientRepository(registeredClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
KeyPair keyPair = generateRsaKey();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
// @formatter:off
|
||||
RSAKey rsaKey = new RSAKey.Builder(publicKey)
|
||||
.privateKey(privateKey)
|
||||
.keyID(UUID.randomUUID().toString())
|
||||
.build();
|
||||
// @formatter:on
|
||||
JWKSet jwkSet = new JWKSet(rsaKey);
|
||||
return new ImmutableJWKSet<>(jwkSet);
|
||||
}
|
||||
|
||||
private static KeyPair generateRsaKey() { // <6>
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
keyPairGenerator.initialize(2048);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ProviderSettings providerSettings() {
|
||||
return ProviderSettings.builder().issuer("http://localhost:9000").build();
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Helper class that performs steps of the {@code authorization_code} flow using
|
||||
* {@link MockMvc} for testing.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class AuthorizationCodeGrantFlow {
|
||||
private static final Pattern HIDDEN_STATE_INPUT_PATTERN = Pattern.compile(".+<input type=\"hidden\" name=\"state\" value=\"([^\"]+)\">.+");
|
||||
private static final TypeReference<Map<String, Object>> TOKEN_RESPONSE_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
|
||||
};
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
|
||||
private String username = "user";
|
||||
|
||||
private Set<String> scopes = new HashSet<>();
|
||||
|
||||
public AuthorizationCodeGrantFlow(MockMvc mockMvc) {
|
||||
this.mockMvc = mockMvc;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void addScope(String scope) {
|
||||
this.scopes.add(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the authorization request and obtain a state parameter.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @return The state parameter for submitting consent for authorization
|
||||
*/
|
||||
public String authorize(RegisteredClient registeredClient) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
parameters.set(OAuth2ParameterNames.STATE, "state");
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorize")
|
||||
.params(parameters)
|
||||
.with(user(this.username).roles("USER")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("content-type", containsString(MediaType.TEXT_HTML_VALUE)))
|
||||
.andReturn();
|
||||
String responseHtml = mvcResult.getResponse().getContentAsString();
|
||||
Matcher matcher = HIDDEN_STATE_INPUT_PATTERN.matcher(responseHtml);
|
||||
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit consent for the authorization request and obtain an authorization code.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param state The state paramter from the authorization request
|
||||
* @return An authorization code
|
||||
*/
|
||||
public String submitConsent(RegisteredClient registeredClient, String state) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.STATE, state);
|
||||
for (String scope : scopes) {
|
||||
parameters.add(OAuth2ParameterNames.SCOPE, scope);
|
||||
}
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/authorize")
|
||||
.params(parameters)
|
||||
.with(user(this.username).roles("USER")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).isNotNull();
|
||||
assertThat(redirectedUrl).matches("http://127.0.0.1:8080/authorized\\?code=.{15,}&state=state");
|
||||
|
||||
String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
|
||||
|
||||
return uriComponents.getQueryParams().getFirst("code");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange an authorization code for an access token.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param authorizationCode The authorization code obtained from the authorization request
|
||||
* @return The token response
|
||||
*/
|
||||
public Map<String, Object> getTokenResponse(RegisteredClient registeredClient, String authorizationCode) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CODE, authorizationCode);
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
|
||||
HttpHeaders basicAuth = new HttpHeaders();
|
||||
basicAuth.setBasicAuth(registeredClient.getClientId(), "secret");
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/token")
|
||||
.params(parameters)
|
||||
.headers(basicAuth))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty())
|
||||
.andExpect(jsonPath("$.id_token").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String responseJson = mvcResult.getResponse().getContentAsString();
|
||||
return objectMapper.readValue(responseJson, TOKEN_RESPONSE_TYPE_REFERENCE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2020-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.userinfo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sample.AuthorizationCodeGrantFlow;
|
||||
import sample.test.SpringTestContext;
|
||||
import sample.test.SpringTestContextExtension;
|
||||
import sample.userinfo.idtoken.IdTokenCustomizerConfig;
|
||||
import sample.userinfo.idtoken.OidcUserInfoService;
|
||||
import sample.userinfo.jwt.JwtTokenCustomizerConfig;
|
||||
import sample.userinfo.jwt.JwtUserInfoMapperSecurityConfig;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for the guide How-to: Customize the OpenID Connect 1.0 UserInfo response.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class EnableUserInfoSecurityConfigTests {
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Test
|
||||
public void userInfoWhenEnabledThenSuccess() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user1");
|
||||
authorizationCodeGrantFlow.addScope("message.read");
|
||||
authorizationCodeGrantFlow.addScope("message.write");
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
this.mockMvc.perform(get("/userinfo")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpect(jsonPath("sub").value("user1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userInfoWhenIdTokenCustomizerThenIdTokenClaimsMappedToResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigWithIdTokenCustomizer.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user1");
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.ADDRESS);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.EMAIL);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.PHONE);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.PROFILE);
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
this.mockMvc.perform(get("/userinfo")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpectAll(
|
||||
jsonPath("sub").value("user1"),
|
||||
jsonPath("name").value("First Last"),
|
||||
jsonPath("given_name").value("First"),
|
||||
jsonPath("family_name").value("Last"),
|
||||
jsonPath("middle_name").value("Middle"),
|
||||
jsonPath("nickname").value("User"),
|
||||
jsonPath("preferred_username").value("user1"),
|
||||
jsonPath("profile").value("https://example.com/user1"),
|
||||
jsonPath("picture").value("https://example.com/user1.jpg"),
|
||||
jsonPath("website").value("https://example.com"),
|
||||
jsonPath("email").value("user1@example.com"),
|
||||
jsonPath("email_verified").value("true"),
|
||||
jsonPath("gender").value("female"),
|
||||
jsonPath("birthdate").value("1970-01-01"),
|
||||
jsonPath("zoneinfo").value("Europe/Paris"),
|
||||
jsonPath("locale").value("en-US"),
|
||||
jsonPath("phone_number").value("+1 (604) 555-1234;ext=5678"),
|
||||
jsonPath("phone_number_verified").value("false"),
|
||||
jsonPath("address.formatted").value("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"),
|
||||
jsonPath("updated_at").value("1970-01-01T00:00:00Z")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userInfoWhenUserInfoMapperThenClaimsMappedToResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigWithJwtTokenCustomizer.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user1");
|
||||
authorizationCodeGrantFlow.addScope("message.read");
|
||||
authorizationCodeGrantFlow.addScope("message.write");
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
this.mockMvc.perform(get("/userinfo")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpectAll(
|
||||
jsonPath("sub").value("user1"),
|
||||
jsonPath("claim-1").value("value-1"),
|
||||
jsonPath("claim-2").value("value-2")
|
||||
);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@Import(EnableUserInfoSecurityConfig.class)
|
||||
static class AuthorizationServerConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import({EnableUserInfoSecurityConfig.class, IdTokenCustomizerConfig.class})
|
||||
static class AuthorizationServerConfigWithIdTokenCustomizer {
|
||||
|
||||
@Bean
|
||||
public OidcUserInfoService userInfoService() {
|
||||
return new OidcUserInfoService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@Import({JwtUserInfoMapperSecurityConfig.class, JwtTokenCustomizerConfig.class})
|
||||
static class AuthorizationServerConfigWithJwtTokenCustomizer {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
89
docs/src/docs/asciidoc/guides/how-to-userinfo.adoc
Normal file
89
docs/src/docs/asciidoc/guides/how-to-userinfo.adoc
Normal file
@@ -0,0 +1,89 @@
|
||||
[[how-to-userinfo]]
|
||||
= How-to: Customize the OpenID Connect 1.0 UserInfo response
|
||||
:index-link: ../how-to.html
|
||||
:docs-dir: ..
|
||||
|
||||
[[getting-started]]
|
||||
== Getting Started
|
||||
|
||||
This guide shows how to customize the xref:{docs-dir}/protocol-endpoints.adoc#oidc-user-info-endpoint[User Info endpoint] of the xref:{docs-dir}/index.adoc#top[Spring Authorization Server].
|
||||
The purpose of this guide is to demonstrate how to enable the endpoint and use the available customization options to produce a custom response.
|
||||
|
||||
[[enable-user-info]]
|
||||
== Enable the User Info Endpoint
|
||||
|
||||
Before customizing the response, you need to enable the User Info endpoint.
|
||||
In https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest[Section 5.3.1], the OpenID Connect 1.0 Core specification states:
|
||||
|
||||
> The Access Token obtained from an OpenID Connect Authentication Request MUST be sent as a Bearer Token, per Section 2 of https://openid.net/specs/openid-connect-core-1_0.html#RFC6750[OAuth 2.0 Bearer Token Usage] [RFC6750].
|
||||
|
||||
The User Info endpoint requires an authenticated request using the access token (which is a JWT when using the xref:{docs-dir}/getting-started.adoc#sample.gettingStarted[Getting Started config]).
|
||||
|
||||
The following listing shows how to configure https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html[Resource Server support] and provide a `JwtDecoder` that can validate the access token to allow authenticated requests to the User Info endpoint.
|
||||
|
||||
[[sample.userinfo]]
|
||||
include::code:EnableUserInfoSecurityConfig[]
|
||||
|
||||
TIP: Click on the "Expanded folded text" icon in the code sample above to display the full example.
|
||||
|
||||
This configuration provides the following:
|
||||
|
||||
<1> A Spring Security filter chain for the xref:{docs-dir}/protocol-endpoints.adoc[Protocol Endpoints].
|
||||
<2> Resource server support that allows User Info requests to be authenticated with access tokens.
|
||||
<3> An instance of `JwtDecoder` used to validate access tokens.
|
||||
|
||||
[[customize-user-info]]
|
||||
== Customize the User Info response
|
||||
|
||||
The following sections describe some options for customizing the user info response.
|
||||
|
||||
[[customize-id-token]]
|
||||
=== Customize the ID Token
|
||||
|
||||
By default, the user info response is generated by using claims from the `id_token` that are returned with the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-token-endpoint[token response].
|
||||
Using the default strategy, https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[standard claims] are returned only with the user info response based on the https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[requested scopes] during authorization.
|
||||
|
||||
The preferred way to customize the user info response is to add standard claims to the `id_token`.
|
||||
The following listing shows how add claims to the `id_token`.
|
||||
|
||||
[[sample.userinfo.idtoken]]
|
||||
include::code:IdTokenCustomizerConfig[]
|
||||
|
||||
This configuration provides the following:
|
||||
|
||||
<1> An instance of `OAuth2TokenCustomizer` for customizing the `id_token`.
|
||||
<2> A custom service used to obtain user info in a domain-specific way.
|
||||
|
||||
The following listing shows a custom service for looking up user info in a domain-specific way:
|
||||
|
||||
include::code:OidcUserInfoService[]
|
||||
|
||||
[[customize-user-info-mapper]]
|
||||
=== Customize the User Info Mapper
|
||||
|
||||
To fully customize the user info response, you can provide a custom user info mapper capable of generating the object used to render the response, which is an instance of the `OidcUserInfo` class from Spring Security.
|
||||
The mapper implementation receives an instance of `OidcUserInfoAuthenticationContext` with information about the current request, including the xref:{docs-dir}/core-model-components.adoc#oauth2-authorization-service[`OAuth2Authorization`].
|
||||
|
||||
The following listing shows how to use the customization option that is available while working directly with the `OAuth2AuthorizationServerConfigurer`.
|
||||
|
||||
[[sample.userinfo.jwt]]
|
||||
include::code:JwtUserInfoMapperSecurityConfig[]
|
||||
|
||||
This configuration maps claims from the access token (which is a JWT when using the xref:{docs-dir}/getting-started.adoc#sample.gettingStarted[Getting Started config]) to populate the user info response and provides the following:
|
||||
|
||||
<1> A Spring Security filter chain for the xref:{docs-dir}/protocol-endpoints.adoc[Protocol Endpoints].
|
||||
<2> A user info mapper that maps claims in a domain-specific way.
|
||||
<3> An example showing the configuration option for customizing the user info mapper.
|
||||
<4> Resource server support that allows User Info requests to be authenticated with access tokens.
|
||||
<5> An example showing how to apply the `OAuth2AuthorizationServerConfigurer` to the Spring Security configuration.
|
||||
|
||||
The user info mapper is not limited to mapping claims from a JWT, but this is a simple example that demonstrates the customization option.
|
||||
Similar to the <<customize-id-token,example shown earlier>> where we customize claims of the ID token, you can customize claims of the access token itself ahead of time, as in the following example:
|
||||
|
||||
include::code:JwtTokenCustomizerConfig[]
|
||||
|
||||
Whether you customize the user info response directly or use this example and customize the access token, you can look up information in a database, perform an LDAP query, make a request to another service, or use any other means of obtaining the information you want to be presented in the user info response.
|
||||
|
||||
== Conclusion
|
||||
|
||||
In this guide, you have learned how to <<enable-user-info,enable>> the xref:{docs-dir}/protocol-endpoints.adoc#oidc-user-info-endpoint[User Info endpoint] and explored various ways of customizing the response, including <<customize-id-token,customizing the ID token>> while continuing to use the built-in response and <<customize-user-info-mapper,customizing the response directly>> using a custom user info mapper.
|
||||
@@ -4,12 +4,12 @@
|
||||
[[how-to-overview]]
|
||||
== Overview
|
||||
|
||||
* xref:guides/how-to-userinfo.adoc[Customize the OpenID Connect 1.0 UserInfo response]
|
||||
* xref:guides/how-to-jpa.adoc[Implement core services with JPA]
|
||||
|
||||
[[how-to-coming-soon]]
|
||||
== Coming Soon
|
||||
|
||||
* Customize the OpenID Connect 1.0 UserInfo response (https://github.com/spring-projects/spring-authorization-server/issues/537[#537])
|
||||
* Authenticate using social login, e.g. Google (https://github.com/spring-projects/spring-authorization-server/issues/538[#538])
|
||||
* Authenticate a user in a Single Page Application with PKCE (https://github.com/spring-projects/spring-authorization-server/issues/539[#539])
|
||||
* Deny access for a revoked JWT access token (https://github.com/spring-projects/spring-authorization-server/issues/543[#543])
|
||||
|
||||
Reference in New Issue
Block a user