Use id_token for user authentication

Fixes gh-4410
This commit is contained in:
Joe Grandja 2017-06-27 11:45:19 -04:00
parent c986b6f4b5
commit 9cfb890207
43 changed files with 1328 additions and 583 deletions

View File

@ -13,6 +13,7 @@ dependencies {
optional project(':spring-security-ldap')
optional project(':spring-security-messaging')
optional project(':spring-security-oauth2-client')
optional project(':spring-security-jwt-jose')
optional project(':spring-security-openid')
optional project(':spring-security-web')
optional project(':spring-security-webflux')

View File

@ -15,25 +15,34 @@
*/
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
import org.springframework.security.jwt.JwtDecoder;
import org.springframework.security.jwt.nimbus.NimbusJwtDecoderJwkSupport;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
import org.springframework.security.oauth2.client.authentication.nimbus.NimbusAuthorizationCodeTokenExchanger;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.client.user.nimbus.NimbusOAuth2UserService;
import org.springframework.security.oauth2.client.authentication.jwt.DefaultProviderJwtDecoderRegistry;
import org.springframework.security.oauth2.core.provider.DefaultProviderMetadata;
import org.springframework.security.oauth2.client.authentication.jwt.ProviderJwtDecoderRegistry;
import org.springframework.security.oauth2.core.provider.ProviderMetadata;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* @author Joe Grandja
@ -43,7 +52,8 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
private OAuth2UserService userInfoService;
private Map<URI, Function<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters = new HashMap<>();
private Map<URI, Class<? extends OAuth2User>> customUserTypes = new HashMap<>();
private Map<URI, String> userNameAttributeNames = new HashMap<>();
AuthorizationCodeAuthenticationFilterConfigurer() {
@ -71,10 +81,17 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
return this;
}
AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoTypeConverter(Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
AuthorizationCodeAuthenticationFilterConfigurer<H> customUserType(Class<? extends OAuth2User> customUserType, URI userInfoUri) {
Assert.notNull(customUserType, "customUserType cannot be null");
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
this.userInfoTypeConverters.put(userInfoUri, userInfoConverter);
this.customUserTypes.put(userInfoUri, customUserType);
return this;
}
AuthorizationCodeAuthenticationFilterConfigurer<H> userNameAttributeName(String userNameAttributeName, URI userInfoUri) {
Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty");
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
this.userNameAttributeNames.put(userInfoUri, userNameAttributeName);
return this;
}
@ -89,7 +106,7 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
@Override
public void init(H http) throws Exception {
AuthorizationCodeAuthenticationProvider authenticationProvider = new AuthorizationCodeAuthenticationProvider(
this.getAuthorizationCodeTokenExchanger(), this.getUserInfoService());
this.getAuthorizationCodeTokenExchanger(), this.getProviderJwtDecoderRegistry(), this.getUserInfoService());
authenticationProvider = this.postProcess(authenticationProvider);
http.authenticationProvider(authenticationProvider);
super.init(http);
@ -114,10 +131,51 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
return this.authorizationCodeTokenExchanger;
}
private ProviderJwtDecoderRegistry getProviderJwtDecoderRegistry() {
Map<ProviderMetadata, JwtDecoder> jwtDecoders = new HashMap<>();
ClientRegistrationRepository clientRegistrationRepository = OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder());
clientRegistrationRepository.getRegistrations().stream().forEach(registration -> {
ClientRegistration.ProviderDetails providerDetails = registration.getProviderDetails();
if (StringUtils.hasText(providerDetails.getJwkSetUri())) {
DefaultProviderMetadata providerMetadata = new DefaultProviderMetadata();
// Default the Issuer to the host of the Authorization Endpoint
providerMetadata.setIssuer(this.toURL(
UriComponentsBuilder
.fromHttpUrl(providerDetails.getAuthorizationUri())
.replacePath(null)
.toUriString()
));
providerMetadata.setAuthorizationEndpoint(this.toURL(providerDetails.getAuthorizationUri()));
providerMetadata.setTokenEndpoint(this.toURL(providerDetails.getTokenUri()));
providerMetadata.setUserInfoEndpoint(this.toURL(providerDetails.getUserInfoUri()));
providerMetadata.setJwkSetUri(this.toURL(providerDetails.getJwkSetUri()));
jwtDecoders.put(providerMetadata, new NimbusJwtDecoderJwkSupport(providerDetails.getJwkSetUri()));
}
});
return new DefaultProviderJwtDecoderRegistry(jwtDecoders);
}
private OAuth2UserService getUserInfoService() {
if (this.userInfoService == null) {
this.userInfoService = new NimbusOAuth2UserService(this.userInfoTypeConverters);
this.userInfoService = new NimbusOAuth2UserService();
if (!this.customUserTypes.isEmpty()) {
((NimbusOAuth2UserService)this.userInfoService).setCustomUserTypes(this.customUserTypes);
}
if (!this.userNameAttributeNames.isEmpty()) {
((NimbusOAuth2UserService)this.userInfoService).setUserNameAttributeNames(this.userNameAttributeNames);
}
}
return this.userInfoService;
}
private URL toURL(String urlStr) {
if (!StringUtils.hasText(urlStr)) {
return null;
}
try {
return new URL(urlStr);
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("Failed to convert '" + urlStr + "' to a URL: " + ex.getMessage(), ex);
}
}
}

View File

@ -16,7 +16,6 @@
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
import org.springframework.context.ApplicationContext;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
@ -35,7 +34,6 @@ import org.springframework.util.CollectionUtils;
import java.net.URI;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
@ -95,10 +93,17 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
return this.and();
}
public OAuth2LoginConfigurer<B> userInfoTypeConverter(Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
public OAuth2LoginConfigurer<B> customUserType(Class<? extends OAuth2User> customUserType, URI userInfoUri) {
Assert.notNull(customUserType, "customUserType cannot be null");
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoTypeConverter(userInfoConverter, userInfoUri);
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.customUserType(customUserType, userInfoUri);
return this.and();
}
public OAuth2LoginConfigurer<B> userNameAttributeName(String userNameAttributeName, URI userInfoUri) {
Assert.hasText(userNameAttributeName, "userNameAttributeName cannot be empty");
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userNameAttributeName(userNameAttributeName, userInfoUri);
return this.and();
}

View File

@ -17,7 +17,7 @@ package org.springframework.security.jwt;
import org.springframework.security.oauth2.core.ClaimAccessor;
import java.net.URI;
import java.net.URL;
import java.time.Instant;
/**
@ -33,17 +33,17 @@ import java.time.Instant;
*/
public interface JwtClaimAccessor extends ClaimAccessor {
default URI getIssuer() {
return this.getClaimAsURI(JwtClaim.ISS);
default URL getIssuer() {
return this.getClaimAsURL(JwtClaim.ISS);
}
default String getSubject() {
return this.getClaimAsString(JwtClaim.SUB);
}
default String getAudience() {
// FIXME Should return String[]
return this.getClaimAsString(JwtClaim.AUD);
default String[] getAudience() {
// TODO Impl JwtClaim.AUD
return null;
}
default Instant getExpiresAt() {

View File

@ -3,10 +3,11 @@ apply plugin: 'io.spring.convention.spring-module'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-oauth2-core')
compile project(':spring-security-jwt-jose')
compile project(':spring-security-web')
compile springCoreDependency
compile 'com.nimbusds:oauth2-oidc-sdk'
compile 'org.springframework:spring-web'
compile 'com.nimbusds:oauth2-oidc-sdk'
provided 'javax.servlet:javax.servlet-api'
}

View File

@ -218,6 +218,7 @@ public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAut
this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri());
this.tokenUri(clientRegistration.getProviderDetails().getTokenUri());
this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri());
this.jwkSetUri(clientRegistration.getProviderDetails().getJwkSetUri());
this.clientName(clientRegistration.getClientName());
this.clientAlias(clientRegistration.getClientAlias());
}

View File

@ -22,10 +22,16 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtDecoder;
import org.springframework.security.oauth2.client.authentication.jwt.ProviderJwtDecoderRegistry;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.core.AccessToken;
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.endpoint.OidcParameter;
import org.springframework.util.Assert;
import java.util.Collection;
@ -68,16 +74,20 @@ import java.util.Collection;
*/
public class AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private final AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
private final ProviderJwtDecoderRegistry providerJwtDecoderRegistry;
private final OAuth2UserService userInfoService;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
public AuthorizationCodeAuthenticationProvider(
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger,
ProviderJwtDecoderRegistry providerJwtDecoderRegistry,
OAuth2UserService userInfoService) {
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
Assert.notNull(providerJwtDecoderRegistry, "providerJwtDecoderRegistry cannot be null");
Assert.notNull(userInfoService, "userInfoService cannot be null");
this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
this.providerJwtDecoderRegistry = providerJwtDecoderRegistry;
this.userInfoService = userInfoService;
}
@ -85,6 +95,7 @@ public class AuthorizationCodeAuthenticationProvider implements AuthenticationPr
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
(AuthorizationCodeAuthenticationToken) authentication;
ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
TokenResponseAttributes tokenResponse =
this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication);
@ -92,8 +103,20 @@ public class AuthorizationCodeAuthenticationProvider implements AuthenticationPr
AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(),
tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(),
tokenResponse.getExpiresAt(), tokenResponse.getScopes());
OAuth2AuthenticationToken accessTokenAuthentication = new OAuth2AuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(), accessToken);
IdToken idToken = null;
if (tokenResponse.getAdditionalParameters().containsKey(OidcParameter.ID_TOKEN)) {
JwtDecoder jwtDecoder = this.providerJwtDecoderRegistry.getJwtDecoder(clientRegistration.getProviderDetails().getJwkSetUri());
if (jwtDecoder == null) {
throw new IllegalArgumentException("Unable to find a registered JwtDecoder for the provider '" + clientRegistration.getProviderDetails().getTokenUri() +
"'. Check to ensure you have configured the JwkSet URI property.");
}
Jwt jwt = jwtDecoder.decode((String)tokenResponse.getAdditionalParameters().get(OidcParameter.ID_TOKEN));
idToken = new IdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
}
OAuth2AuthenticationToken accessTokenAuthentication =
new OAuth2AuthenticationToken(clientRegistration, accessToken, idToken);
accessTokenAuthentication.setDetails(authorizationCodeAuthentication.getDetails());
OAuth2User user = this.userInfoService.loadUser(accessTokenAuthentication);
@ -101,20 +124,21 @@ public class AuthorizationCodeAuthenticationProvider implements AuthenticationPr
Collection<? extends GrantedAuthority> authorities =
this.authoritiesMapper.mapAuthorities(user.getAuthorities());
OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(user, authorities,
accessTokenAuthentication.getClientRegistration(), accessTokenAuthentication.getAccessToken());
OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(
user, authorities, accessTokenAuthentication.getClientRegistration(),
accessTokenAuthentication.getAccessToken(), accessTokenAuthentication.getIdToken());
authenticationResult.setDetails(accessTokenAuthentication.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
this.authoritiesMapper = authoritiesMapper;
}
@Override
public boolean supports(Class<?> authentication) {
return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@ -24,6 +24,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.core.AccessToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.util.Assert;
import java.util.Collection;
@ -33,7 +34,7 @@ import java.util.Collection;
* that represents an <i>OAuth 2.0</i> {@link Authentication}.
*
* <p>
* It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}.
* It associates an {@link OAuth2User}, {@link ClientRegistration}, {@link AccessToken} and optionally an {@link IdToken}.
* This <code>Authentication</code> is considered <i>&quot;authenticated&quot;</i> if the {@link OAuth2User}
* is provided in the respective constructor. This typically happens after the {@link OAuth2UserService}
* retrieves the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>.
@ -43,19 +44,21 @@ import java.util.Collection;
* @see OAuth2User
* @see ClientRegistration
* @see AccessToken
* @see IdToken
*/
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final OAuth2User principal;
private final ClientRegistration clientRegistration;
private final AccessToken accessToken;
private final IdToken idToken;
public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken) {
this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken);
public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken, IdToken idToken) {
this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken, idToken);
}
public OAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
ClientRegistration clientRegistration, AccessToken accessToken) {
ClientRegistration clientRegistration, AccessToken accessToken, IdToken idToken) {
super(authorities);
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
@ -63,6 +66,7 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
this.principal = principal;
this.clientRegistration = clientRegistration;
this.accessToken = accessToken;
this.idToken = idToken;
this.setAuthenticated(principal != null);
}
@ -84,4 +88,8 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
public AccessToken getAccessToken() {
return this.accessToken;
}
public IdToken getIdToken() {
return this.idToken;
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.client.authentication.jwt;
import org.springframework.security.jwt.JwtDecoder;
import org.springframework.security.oauth2.core.provider.ProviderMetadata;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* The default implementation of a {@link ProviderJwtDecoderRegistry} that associates
* a {@link JwtDecoder} to a {@link ProviderMetadata}. The <code>ProviderMetadata</code>
* is matched against the <code>providerIdentifier</code> parameter passed to {@link #getJwtDecoder(String)}.
*
* @author Joe Grandja
* @since 5.0
*/
public class DefaultProviderJwtDecoderRegistry implements ProviderJwtDecoderRegistry {
private final Map<ProviderMetadata, JwtDecoder> jwtDecoders;
public DefaultProviderJwtDecoderRegistry(Map<ProviderMetadata, JwtDecoder> jwtDecoders) {
Assert.notNull(jwtDecoders, "jwtDecoders cannot be null");
this.jwtDecoders = Collections.unmodifiableMap(new HashMap<>(jwtDecoders));
}
@Override
public JwtDecoder getJwtDecoder(String providerIdentifier) {
Assert.hasText(providerIdentifier, "providerIdentifier cannot be empty");
Optional<ProviderMetadata> providerMetadataKey = this.jwtDecoders.keySet().stream().filter(providerMetadata ->
providerIdentifier.equals(providerMetadata.getIssuer().toString()) ||
providerIdentifier.equals(providerMetadata.getAuthorizationEndpoint().toString()) ||
providerIdentifier.equals(providerMetadata.getTokenEndpoint().toString()) ||
providerIdentifier.equals(providerMetadata.getUserInfoEndpoint().toString()) ||
providerIdentifier.equals(providerMetadata.getJwkSetUri().toString())
).findFirst();
return (providerMetadataKey.isPresent() ? this.jwtDecoders.get(providerMetadataKey.get()) : null);
}
}

View File

@ -13,7 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication.jwt;
import org.springframework.security.jwt.JwtDecoder;
/**
* Support classes for converting to {@link org.springframework.security.oauth2.core.user.OAuth2User}.
* A registry for {@link JwtDecoder}'s that are associated to an <i>OAuth 2.0 Provider</i>.
*
* @author Joe Grandja
* @since 5.0
*/
package org.springframework.security.oauth2.client.user.converter;
public interface ProviderJwtDecoderRegistry {
JwtDecoder getJwtDecoder(String providerIdentifier);
}

View File

@ -122,6 +122,7 @@ public class ClientRegistration {
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String jwkSetUri;
protected ProviderDetails() {
}
@ -149,6 +150,14 @@ public class ClientRegistration {
protected void setUserInfoUri(String userInfoUri) {
this.userInfoUri = userInfoUri;
}
public String getJwkSetUri() {
return this.jwkSetUri;
}
protected void setJwkSetUri(String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}
}
public static class Builder {
@ -161,6 +170,7 @@ public class ClientRegistration {
protected String authorizationUri;
protected String tokenUri;
protected String userInfoUri;
protected String jwkSetUri;
protected String clientName;
protected String clientAlias;
@ -180,6 +190,7 @@ public class ClientRegistration {
this.authorizationUri(clientRegistrationProperties.getAuthorizationUri());
this.tokenUri(clientRegistrationProperties.getTokenUri());
this.userInfoUri(clientRegistrationProperties.getUserInfoUri());
this.jwkSetUri(clientRegistrationProperties.getJwkSetUri());
this.clientName(clientRegistrationProperties.getClientName());
this.clientAlias(clientRegistrationProperties.getClientAlias());
}
@ -227,6 +238,11 @@ public class ClientRegistration {
return this;
}
public Builder jwkSetUri(String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
return this;
}
public Builder clientName(String clientName) {
this.clientName = clientName;
return this;
@ -256,6 +272,7 @@ public class ClientRegistration {
providerDetails.setAuthorizationUri(this.authorizationUri);
providerDetails.setTokenUri(this.tokenUri);
providerDetails.setUserInfoUri(this.userInfoUri);
providerDetails.setJwkSetUri(this.jwkSetUri);
clientRegistration.setProviderDetails(providerDetails);
clientRegistration.setClientName(this.clientName);

View File

@ -42,6 +42,7 @@ public class ClientRegistrationProperties {
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String jwkSetUri;
private String clientName;
private String clientAlias;
@ -118,6 +119,14 @@ public class ClientRegistrationProperties {
this.userInfoUri = userInfoUri;
}
public String getJwkSetUri() {
return this.jwkSetUri;
}
public void setJwkSetUri(String jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}
public String getClientName() {
return this.clientName;
}

View File

@ -19,20 +19,21 @@ import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.user.UserInfo;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.security.oauth2.oidc.core.user.OidcUser;
/**
* Implementations of this interface are responsible for obtaining
* the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
* using the provided {@link OAuth2AuthenticationToken#getAccessToken()}
* and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}
* (for a standard <i>OAuth 2.0 Provider</i>) or {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
* and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2AuthenticationToken
* @see AuthenticatedPrincipal
* @see OAuth2User
* @see OidcUser
* @see UserInfo
*/
public interface OAuth2UserService {

View File

@ -1,57 +0,0 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.client.user.converter;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.io.IOException;
import java.util.Map;
import java.util.function.Function;
/**
* Base implementation of a <code>Function</code> that converts a {@link ClientHttpResponse}
* to a specific type of {@link OAuth2User}.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2User
* @see ClientHttpResponse
*/
public abstract class AbstractOAuth2UserConverter<R extends OAuth2User> implements Function<ClientHttpResponse, R> {
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
protected AbstractOAuth2UserConverter() {
}
@Override
public final R apply(ClientHttpResponse clientHttpResponse) {
Map<String, Object> userAttributes;
try {
userAttributes = (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, clientHttpResponse);
} catch (IOException ex) {
throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
}
return this.apply(userAttributes);
}
protected abstract R apply(Map<String, Object> userAttributes);
}

View File

@ -1,57 +0,0 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.client.user.converter;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import java.io.IOException;
import java.util.function.Function;
/**
* A <code>Function</code> that converts a {@link ClientHttpResponse}
* to a custom type of {@link OAuth2User}, as supplied via the constructor.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2User
* @see ClientHttpResponse
*/
public final class CustomOAuth2UserConverter<R extends OAuth2User> implements Function<ClientHttpResponse, R> {
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
private final Class<R> customType;
public CustomOAuth2UserConverter(Class<R> customType) {
Assert.notNull(customType, "customType cannot be null");
this.customType = customType;
}
@Override
public R apply(ClientHttpResponse clientHttpResponse) {
R user;
try {
user = (R) this.jackson2HttpMessageConverter.read(this.customType, clientHttpResponse);
} catch (IOException ex) {
throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
}
return user;
}
}

View File

@ -1,46 +0,0 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.client.user.converter;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import java.util.Map;
/**
* An implementation of a {@link AbstractOAuth2UserConverter} that converts
* a {@link ClientHttpResponse} to a {@link OAuth2User}.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2User
* @see ClientHttpResponse
*/
public final class OAuth2UserConverter extends AbstractOAuth2UserConverter<OAuth2User> {
private final String nameAttributeKey;
public OAuth2UserConverter(String nameAttributeKey) {
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
this.nameAttributeKey = nameAttributeKey;
}
@Override
protected OAuth2User apply(Map<String, Object> userAttributes) {
return new DefaultOAuth2User(userAttributes, this.nameAttributeKey);
}
}

View File

@ -22,112 +22,206 @@ import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.user.UserInfo;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.security.oauth2.oidc.core.user.DefaultOidcUser;
import org.springframework.security.oauth2.oidc.core.user.OidcUser;
import org.springframework.security.oauth2.oidc.core.user.OidcUserAuthority;
import org.springframework.util.Assert;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.*;
/**
* An implementation of an {@link OAuth2UserService} that uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
*
* <p>
* This implementation uses a <code>Map</code> of converter's <i>keyed</i> by <code>URI</code>.
* The <code>URI</code> represents the <i>UserInfo Endpoint</i> address and the mapped <code>Function</code>
* is capable of converting the <i>UserInfo Response</i> to either an
* {@link OAuth2User} (for a standard <i>OAuth 2.0 Provider</i>) or
* {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
* This implementation may be configured with a <code>Map</code> of custom {@link OAuth2User} types
* <i>keyed</i> by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
*
* <p>
* For {@link OAuth2User}'s registered at a standard <i>OAuth 2.0 Provider</i>, the attribute name
* for the &quot;user's name&quot; is required. This can be supplied via {@link #setUserNameAttributeNames(Map)},
* <i>keyed</i> by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2AuthenticationToken
* @see AuthenticatedPrincipal
* @see OAuth2User
* @see OidcUser
* @see UserInfo
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
*/
public class NimbusOAuth2UserService implements OAuth2UserService {
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private final Map<URI, Function<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters;
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
private Map<URI, String> userNameAttributeNames = Collections.unmodifiableMap(Collections.emptyMap());
private Map<URI, Class<? extends OAuth2User>> customUserTypes = Collections.unmodifiableMap(Collections.emptyMap());
public NimbusOAuth2UserService(Map<URI, Function<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters) {
Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty");
this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters);
public NimbusOAuth2UserService() {
}
@Override
public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
OAuth2User user;
public final OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
try {
ClientRegistration clientRegistration = token.getClientRegistration();
URI userInfoUri;
try {
userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri());
} catch (Exception ex) {
throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " +
clientRegistration.getProviderDetails().getUserInfoUri(), ex);
}
Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = this.userInfoTypeConverters.get(userInfoUri);
if (userInfoConverter == null) {
throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString());
}
BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
// Request the User Info
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
HTTPResponse httpResponse = httpRequest.send();
if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
UserInfoErrorResponse userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
StringBuilder errorDescription = new StringBuilder();
errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> ");
errorDescription.append("Error details: [");
errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString());
errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode());
if (errorObject.getCode() != null) {
errorDescription.append(", Error Code: ").append(errorObject.getCode());
}
if (errorObject.getDescription() != null) {
errorDescription.append(", Error Description: ").append(errorObject.getDescription());
}
errorDescription.append("]");
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
user = userInfoConverter.apply(new NimbusClientHttpResponse(httpResponse));
} catch (ParseException ex) {
// This error occurs if the User Info Response is not well-formed or invalid
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE), ex);
} catch (IOException ex) {
// This error occurs when there is a network-related issue
throw new AuthenticationServiceException("An error occurred while sending the User Info Request: " +
ex.getMessage(), ex);
if (this.getCustomUserTypes().containsKey(userInfoUri)) {
return this.loadCustomUser(token);
}
if (token.getIdToken() != null) {
return this.loadOidcUser(token);
}
return this.loadOAuth2User(token);
}
protected OAuth2User loadOidcUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
// TODO Retrieving the UserInfo should be optional. Need to add the capability for opting in/out
Map<String, Object> userAttributes = this.getUserInfo(token);
UserInfo userInfo = new UserInfo(userAttributes);
GrantedAuthority authority = new OidcUserAuthority(token.getIdToken(), userInfo);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
return new DefaultOidcUser(authorities, token.getIdToken(), userInfo);
}
protected OAuth2User loadOAuth2User(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
if (!this.getUserNameAttributeNames().containsKey(userInfoUri)) {
throw new IllegalArgumentException("The attribute name for the \"user's name\" is required for the OAuth2User " +
" retrieved from the UserInfo Endpoint -> " + userInfoUri.toString());
}
String userNameAttributeName = this.getUserNameAttributeNames().get(userInfoUri);
Map<String, Object> userAttributes = this.getUserInfo(token);
GrantedAuthority authority = new OAuth2UserAuthority(userAttributes);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
protected OAuth2User loadCustomUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
Class<? extends OAuth2User> customUserType = this.getCustomUserTypes().get(userInfoUri);
OAuth2User user;
try {
user = customUserType.newInstance();
} catch (ReflectiveOperationException ex) {
throw new IllegalArgumentException("An error occurred while attempting to instantiate the custom OAuth2User \"" +
customUserType.getName() + "\" -> " + ex.getMessage(), ex);
}
Map<String, Object> userAttributes = this.getUserInfo(token);
if (token.getIdToken() != null) {
userAttributes.putAll(token.getIdToken().getClaims());
}
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(user);
wrapper.setAutoGrowNestedPaths(true);
wrapper.setPropertyValues(userAttributes);
return user;
}
protected Map<String, Object> getUserInfo(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
HTTPResponse httpResponse;
try {
httpResponse = httpRequest.send();
} catch (IOException ex) {
throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " +
ex.getMessage(), ex);
}
if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
UserInfoErrorResponse userInfoErrorResponse;
try {
userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
} catch (ParseException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred parsing the UserInfo Error response: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
StringBuilder errorDescription = new StringBuilder();
errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> ");
errorDescription.append("Error details: [");
errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString());
errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode());
if (errorObject.getCode() != null) {
errorDescription.append(", Error Code: ").append(errorObject.getCode());
}
if (errorObject.getDescription() != null) {
errorDescription.append(", Error Description: ").append(errorObject.getDescription());
}
errorDescription.append("]");
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
try {
return (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, new NimbusClientHttpResponse(httpResponse));
} catch (IOException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred reading the UserInfo Success response: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
}
protected Map<URI, String> getUserNameAttributeNames() {
return this.userNameAttributeNames;
}
public final void setUserNameAttributeNames(Map<URI, String> userNameAttributeNames) {
Assert.notEmpty(userNameAttributeNames, "userNameAttributeNames cannot be empty");
this.userNameAttributeNames = Collections.unmodifiableMap(new HashMap<>(userNameAttributeNames));
}
protected Map<URI, Class<? extends OAuth2User>> getCustomUserTypes() {
return this.customUserTypes;
}
public final void setCustomUserTypes(Map<URI, Class<? extends OAuth2User>> customUserTypes) {
Assert.notEmpty(customUserTypes, "customUserTypes cannot be empty");
this.customUserTypes = Collections.unmodifiableMap(new HashMap<>(customUserTypes));
}
private URI getUserInfoUri(OAuth2AuthenticationToken token) {
ClientRegistration clientRegistration = token.getClientRegistration();
try {
return new URI(clientRegistration.getProviderDetails().getUserInfoUri());
} catch (Exception ex) {
throw new IllegalArgumentException("An error occurred parsing the UserInfo URI: " +
clientRegistration.getProviderDetails().getUserInfoUri(), ex);
}
}
}

View File

@ -56,4 +56,32 @@ public abstract class AbstractToken implements Serializable {
public Instant getExpiresAt() {
return this.expiresAt;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
AbstractToken that = (AbstractToken) obj;
if (!this.getTokenValue().equals(that.getTokenValue())) {
return false;
}
if (!this.getIssuedAt().equals(that.getIssuedAt())) {
return false;
}
return this.getExpiresAt().equals(that.getExpiresAt());
}
@Override
public int hashCode() {
int result = this.getTokenValue().hashCode();
result = 31 * result + this.getIssuedAt().hashCode();
result = 31 * result + this.getExpiresAt().hashCode();
return result;
}
}

View File

@ -17,8 +17,8 @@ package org.springframework.security.oauth2.core;
import org.springframework.util.Assert;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.Map;
@ -56,14 +56,14 @@ public interface ClaimAccessor {
}
}
default URI getClaimAsURI(String claim) {
default URL getClaimAsURL(String claim) {
if (!this.containsClaim(claim)) {
return null;
}
try {
return new URI(this.getClaimAsString(claim));
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URI: " + ex.getMessage(), ex);
return new URL(this.getClaimAsString(claim));
} catch (MalformedURLException ex) {
throw new IllegalArgumentException("Unable to convert claim '" + claim + "' to URL: " + ex.getMessage(), ex);
}
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.core.provider;
import java.net.URL;
/**
* Default implementation of {@link ProviderMetadata}.
*
* @author Joe Grandja
* @since 5.0
*/
public class DefaultProviderMetadata implements ProviderMetadata {
private URL issuer;
private URL authorizationEndpoint;
private URL tokenEndpoint;
private URL userInfoEndpoint;
private URL jwkSetUri;
public DefaultProviderMetadata() {
}
@Override
public URL getIssuer() {
return issuer;
}
public void setIssuer(URL issuer) {
this.issuer = issuer;
}
@Override
public URL getAuthorizationEndpoint() {
return authorizationEndpoint;
}
public void setAuthorizationEndpoint(URL authorizationEndpoint) {
this.authorizationEndpoint = authorizationEndpoint;
}
@Override
public URL getTokenEndpoint() {
return tokenEndpoint;
}
public void setTokenEndpoint(URL tokenEndpoint) {
this.tokenEndpoint = tokenEndpoint;
}
@Override
public URL getUserInfoEndpoint() {
return userInfoEndpoint;
}
public void setUserInfoEndpoint(URL userInfoEndpoint) {
this.userInfoEndpoint = userInfoEndpoint;
}
@Override
public URL getJwkSetUri() {
return jwkSetUri;
}
public void setJwkSetUri(URL jwkSetUri) {
this.jwkSetUri = jwkSetUri;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
DefaultProviderMetadata that = (DefaultProviderMetadata) obj;
if (!this.getIssuer().equals(that.getIssuer())) {
return false;
}
if (!this.getAuthorizationEndpoint().equals(that.getAuthorizationEndpoint())) {
return false;
}
if (!this.getTokenEndpoint().equals(that.getTokenEndpoint())) {
return false;
}
return this.getUserInfoEndpoint().equals(that.getUserInfoEndpoint());
}
@Override
public int hashCode() {
int result = this.getIssuer().hashCode();
result = 31 * result + this.getAuthorizationEndpoint().hashCode();
result = 31 * result + this.getTokenEndpoint().hashCode();
result = 31 * result + this.getUserInfoEndpoint().hashCode();
return result;
}
}

View File

@ -13,27 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.user.converter;
package org.springframework.security.oauth2.core.provider;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.oauth2.oidc.user.DefaultUserInfo;
import org.springframework.security.oauth2.oidc.user.UserInfo;
import java.util.Map;
import java.net.URL;
/**
* An implementation of a {@link AbstractOAuth2UserConverter} that converts
* a {@link ClientHttpResponse} to a {@link UserInfo}.
* Metadata describing the configuration information for an <i>OAuth 2.0 Provider</i>.
*
* @author Joe Grandja
* @since 5.0
* @see UserInfo
* @see ClientHttpResponse
*/
public final class UserInfoConverter extends AbstractOAuth2UserConverter<UserInfo> {
public interface ProviderMetadata {
URL getIssuer();
URL getAuthorizationEndpoint();
URL getTokenEndpoint();
URL getUserInfoEndpoint();
URL getJwkSetUri();
@Override
protected UserInfo apply(Map<String, Object> userAttributes) {
return new DefaultUserInfo(userAttributes);
}
}

View File

@ -18,9 +18,7 @@ package org.springframework.security.oauth2.core.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
@ -42,22 +40,18 @@ import java.util.stream.Collectors;
public class DefaultOAuth2User implements OAuth2User {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Set<GrantedAuthority> authorities;
private final Map<String, Object> attributes;
private Map<String, Object> attributes;
private final String nameAttributeKey;
public DefaultOAuth2User(Map<String, Object> attributes, String nameAttributeKey) {
this(Collections.emptySet(), attributes, nameAttributeKey);
}
public DefaultOAuth2User(Set<GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) {
Assert.notNull(authorities, "authorities cannot be null");
Assert.notEmpty(authorities, "authorities cannot be empty");
Assert.notEmpty(attributes, "attributes cannot be empty");
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
if (!attributes.containsKey(nameAttributeKey)) {
throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey);
}
this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities));
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
this.setAttributes(attributes);
this.nameAttributeKey = nameAttributeKey;
}
@ -76,37 +70,15 @@ public class DefaultOAuth2User implements OAuth2User {
return this.attributes;
}
protected String getAttributeAsString(String key) {
Object value = this.getAttributes().get(key);
return (value != null ? value.toString() : null);
}
protected Boolean getAttributeAsBoolean(String key) {
String value = this.getAttributeAsString(key);
return (value != null ? Boolean.valueOf(value) : null);
}
protected Instant getAttributeAsInstant(String key) {
String value = this.getAttributeAsString(key);
if (value == null) {
return null;
}
try {
return Instant.ofEpochSecond(Long.valueOf(value));
} catch (NumberFormatException ex) {
throw new IllegalArgumentException("Invalid long value: " + ex.getMessage(), ex);
}
protected final void setAttributes(Map<String, Object> attributes) {
Assert.notEmpty(attributes, "attributes cannot be empty");
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
}
private Set<GrantedAuthority> sortAuthorities(Set<GrantedAuthority> authorities) {
if (CollectionUtils.isEmpty(authorities)) {
return Collections.emptySet();
}
SortedSet<GrantedAuthority> sortedAuthorities =
new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority()));
authorities.stream().forEach(sortedAuthorities::add);
return sortedAuthorities;
}

View File

@ -52,4 +52,5 @@ public interface OAuth2User extends AuthenticatedPrincipal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Map<String, Object> getAttributes();
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.core.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A {@link GrantedAuthority} that is associated with an {@link OAuth2User}.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2User
*/
public class OAuth2UserAuthority implements GrantedAuthority {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String authority;
private Map<String, Object> attributes;
public OAuth2UserAuthority(Map<String, Object> attributes) {
this("ROLE_USER", attributes);
}
public OAuth2UserAuthority(String authority, Map<String, Object> attributes) {
Assert.hasText(authority, "authority cannot be empty");
Assert.notEmpty(attributes, "attributes cannot be empty");
this.authority = authority;
this.setAttributes(attributes);
}
@Override
public String getAuthority() {
return this.authority;
}
public Map<String, Object> getAttributes() {
return this.attributes;
}
protected final void setAttributes(Map<String, Object> attributes) {
Assert.notEmpty(attributes, "attributes cannot be empty");
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
OAuth2UserAuthority that = (OAuth2UserAuthority) obj;
if (!this.getAuthority().equals(that.getAuthority())) {
return false;
}
return this.getAttributes().equals(that.getAttributes());
}
@Override
public int hashCode() {
int result = this.getAuthority().hashCode();
result = 31 * result + this.getAttributes().hashCode();
return result;
}
@Override
public String toString() {
return this.getAuthority();
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core;
/**
* The Address Claim represents a physical mailing address defined by the <i>OpenID Connect Core 1.0</i> specification
* that can be returned either in the <i>UserInfo Response</i> or the <i>ID Token</i>.
*
* @author Joe Grandja
* @since 5.0
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#AddressClaim">Address Claim</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
*/
public interface Address {
String getFormatted();
String getStreetAddress();
String getLocality();
String getRegion();
String getPostalCode();
String getCountry();
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core;
import org.springframework.security.oauth2.core.AbstractToken;
import org.springframework.util.Assert;
import java.time.Instant;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* An implementation of an {@link AbstractToken} representing an <i>OpenID Connect Core 1.0 ID Token</i>.
*
* <p>
* The <code>IdToken</code> is a security token that contains &quot;Claims&quot;
* about the authentication of an End-User by an Authorization Server.
*
* @author Joe Grandja
* @since 5.0
* @see AbstractToken
* @see IdTokenClaimAccessor
* @see StandardClaimAccessor
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
*/
public class IdToken extends AbstractToken implements IdTokenClaimAccessor {
private final Map<String, Object> claims;
public IdToken(String tokenValue, Instant issuedAt, Instant expiresAt, Map<String, Object> claims) {
super(tokenValue, issuedAt, expiresAt);
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core;
/**
* The &quot;Claims&quot; defined by the <i>OpenID Connect Core 1.0</i> specification
* that can be returned in the <i>ID Token</i>.
*
* @author Joe Grandja
* @since 5.0
* @see IdToken
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
*/
public interface IdTokenClaim {
String ISS = "iss";
String SUB = "sub";
String AUD = "aud";
String EXP = "exp";
String IAT = "iat";
String AUTH_TIME = "auth_time";
String NONCE = "nonce";
String ACR = "acr";
String AMR = "amr";
String AZP = "azp";
String AT_HASH = "at_hash";
String C_HASH = "c_hash";
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core;
import org.springframework.security.oauth2.core.ClaimAccessor;
import java.net.URL;
import java.time.Instant;
/**
* A {@link ClaimAccessor} for the &quot;Claims&quot; that can be returned in the <i>ID Token</i>
* which provides information about the authentication of an End-User by an Authorization Server.
*
* @see ClaimAccessor
* @see StandardClaimAccessor
* @see StandardClaim
* @see IdTokenClaim
* @see IdToken
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
* @author Joe Grandja
* @since 5.0
*/
public interface IdTokenClaimAccessor extends StandardClaimAccessor {
default URL getIssuer() {
return this.getClaimAsURL(IdTokenClaim.ISS);
}
default String getSubject() {
return this.getClaimAsString(IdTokenClaim.SUB);
}
default String[] getAudience() {
// TODO Impl IdTokenClaim.AUD
return null;
}
default Instant getExpiresAt() {
return this.getClaimAsInstant(IdTokenClaim.EXP);
}
default Instant getIssuedAt() {
return this.getClaimAsInstant(IdTokenClaim.IAT);
}
default Instant getAuthenticatedAt() {
return this.getClaimAsInstant(IdTokenClaim.AUTH_TIME);
}
default String getNonce() {
return this.getClaimAsString(IdTokenClaim.NONCE);
}
default String getAuthenticationContextClass() {
return this.getClaimAsString(IdTokenClaim.ACR);
}
default String[] getAuthenticationMethods() {
// TODO Impl IdTokenClaim.AMR
return null;
}
default String getAuthorizedParty() {
return this.getClaimAsString(IdTokenClaim.AZP);
}
default String getAccessTokenHash() {
return this.getClaimAsString(IdTokenClaim.AT_HASH);
}
default String getAuthorizationCodeHash() {
return this.getClaimAsString(IdTokenClaim.C_HASH);
}
}

View File

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.oidc;
package org.springframework.security.oauth2.oidc.core;
/**
* The Standard Claims defined by the <i>OpenID Connect Core 1.0</i> specification
* and returned in either the <i>UserInfo Response</i> or in the <i>ID Token</i>.
* The &quot;Standard Claims&quot; defined by the <i>OpenID Connect Core 1.0</i> specification
* that can be returned either in the <i>UserInfo Response</i> or the <i>ID Token</i>.
*
* @author Joe Grandja
* @since 5.0
@ -25,7 +25,7 @@ package org.springframework.security.oauth2.oidc;
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
*/
public interface StandardClaimName {
public interface StandardClaim {
String SUB = "sub";
@ -66,4 +66,5 @@ public interface StandardClaimName {
String ADDRESS = "address";
String UPDATED_AT = "updated_at";
}

View File

@ -0,0 +1,118 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core;
import org.springframework.security.oauth2.core.ClaimAccessor;
import java.time.Instant;
/**
* A {@link ClaimAccessor} for the &quot;Standard Claims&quot; that can be returned
* either in the <i>UserInfo Response</i> or the <i>ID Token</i>.
*
* @see ClaimAccessor
* @see StandardClaim
* @see UserInfo
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
* @author Joe Grandja
* @since 5.0
*/
public interface StandardClaimAccessor extends ClaimAccessor {
default String getSubject() {
return this.getClaimAsString(StandardClaim.SUB);
}
default String getFullName() {
return this.getClaimAsString(StandardClaim.NAME);
}
default String getGivenName() {
return this.getClaimAsString(StandardClaim.GIVEN_NAME);
}
default String getFamilyName() {
return this.getClaimAsString(StandardClaim.FAMILY_NAME);
}
default String getMiddleName() {
return this.getClaimAsString(StandardClaim.MIDDLE_NAME);
}
default String getNickName() {
return this.getClaimAsString(StandardClaim.NICKNAME);
}
default String getPreferredUsername() {
return this.getClaimAsString(StandardClaim.PREFERRED_USERNAME);
}
default String getProfile() {
return this.getClaimAsString(StandardClaim.PROFILE);
}
default String getPicture() {
return this.getClaimAsString(StandardClaim.PICTURE);
}
default String getWebsite() {
return this.getClaimAsString(StandardClaim.WEBSITE);
}
default String getEmail() {
return this.getClaimAsString(StandardClaim.EMAIL);
}
default Boolean getEmailVerified() {
return this.getClaimAsBoolean(StandardClaim.EMAIL_VERIFIED);
}
default String getGender() {
return this.getClaimAsString(StandardClaim.GENDER);
}
default String getBirthdate() {
return this.getClaimAsString(StandardClaim.BIRTHDATE);
}
default String getZoneInfo() {
return this.getClaimAsString(StandardClaim.ZONEINFO);
}
default String getLocale() {
return this.getClaimAsString(StandardClaim.LOCALE);
}
default String getPhoneNumber() {
return this.getClaimAsString(StandardClaim.PHONE_NUMBER);
}
default Boolean getPhoneNumberVerified() {
return this.getClaimAsBoolean(StandardClaim.PHONE_NUMBER_VERIFIED);
}
default Address getAddress() {
// TODO Impl StandardClaim.ADDRESS
return null;
}
default Instant getUpdatedAt() {
return this.getClaimAsInstant(StandardClaim.UPDATED_AT);
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A representation of a <i>UserInfo Response</i> that is returned
* from the OAuth 2.0 Protected Resource <i>UserInfo Endpoint</i>.
*
* <p>
* The <code>UserInfo</code> contains a set of &quot;Standard Claims&quot; about the authentication of an End-User.
*
* @author Joe Grandja
* @since 5.0
* @see StandardClaimAccessor
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
*/
public class UserInfo implements StandardClaimAccessor {
private final Map<String, Object> claims;
public UserInfo(Map<String, Object> claims) {
Assert.notEmpty(claims, "claims cannot be empty");
this.claims = Collections.unmodifiableMap(new LinkedHashMap<>(claims));
}
@Override
public Map<String, Object> getClaims() {
return this.claims;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
UserInfo that = (UserInfo) obj;
return this.getClaims().equals(that.getClaims());
}
@Override
public int hashCode() {
return this.getClaims().hashCode();
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core.endpoint;
/**
* Standard parameters defined in the OAuth Parameters Registry
* and used by the authorization endpoint and token endpoint.
*
* @author Joe Grandja
* @since 5.0
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#OAuthParametersRegistry">18.2 OAuth Parameters Registration</a>
*/
public interface OidcParameter {
String ID_TOKEN = "id_token";
}

View File

@ -16,4 +16,4 @@
/**
* Core classes and interfaces providing support for <i>OpenID Connect Core 1.0</i>.
*/
package org.springframework.security.oauth2.oidc;
package org.springframework.security.oauth2.oidc.core;

View File

@ -0,0 +1,79 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.IdTokenClaim;
import org.springframework.security.oauth2.oidc.core.StandardClaim;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.springframework.security.oauth2.oidc.core.StandardClaim.NAME;
/**
* The default implementation of an {@link OidcUser}.
*
* <p>
* The claim used for accessing the &quot;name&quot; of the
* user <code>Principal</code> via {@link #getClaims()}
* is {@link StandardClaim#NAME} or if not available
* will default to {@link IdTokenClaim#SUB}.
*
* @author Joe Grandja
* @since 5.0
* @see OidcUser
* @see DefaultOAuth2User
* @see IdToken
* @see UserInfo
*/
public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
private final IdToken idToken;
private final UserInfo userInfo;
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken) {
this(authorities, idToken, null);
}
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken, UserInfo userInfo) {
super(authorities, idToken.getClaims(), IdTokenClaim.SUB);
this.idToken = idToken;
this.userInfo = userInfo;
if (userInfo != null) {
this.setAttributes(
Stream.of(this.getAttributes(), userInfo.getClaims())
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (k1, k2) -> k1))
);
}
}
@Override
public Map<String, Object> getClaims() {
return this.getAttributes();
}
@Override
public String getName() {
String name = this.getClaimAsString(NAME);
return (name != null ? name : super.getName());
}
}

View File

@ -13,24 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.oidc.user;
package org.springframework.security.oauth2.oidc.core.user;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.IdTokenClaimAccessor;
import org.springframework.security.oauth2.oidc.core.StandardClaimAccessor;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import java.time.Instant;
import java.util.Map;
/**
* A representation of a user <code>Principal</code>
* that is registered with an <i>OpenID Connect 1.0 Provider</i>.
*
* <p>
* The structure of the user <code>Principal</code> is defined by the
* <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>,
* which is an <i>OAuth 2.0 Protected Resource</i> that returns a set of
* <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a>
* about the authenticated End-User.
* An <code>OidcUser</code> contains &quot;Claims&quot; about the Authentication of the End-User.
* The claims are aggregated from the <code>IdToken</code> and optionally the <code>UserInfo</code>.
*
* <p>
* Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
@ -39,65 +40,18 @@ import java.time.Instant;
*
* @author Joe Grandja
* @since 5.0
* @see DefaultUserInfo
* @see AuthenticatedPrincipal
* @see DefaultOidcUser
* @see OAuth2User
* @see IdToken
* @see UserInfo
* @see IdTokenClaimAccessor
* @see StandardClaimAccessor
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
*/
public interface UserInfo extends OAuth2User {
public interface OidcUser extends OAuth2User, IdTokenClaimAccessor {
String getSubject();
Map<String, Object> getClaims();
String getGivenName();
String getFamilyName();
String getMiddleName();
String getNickName();
String getPreferredUsername();
String getProfile();
String getPicture();
String getWebsite();
String getEmail();
Boolean getEmailVerified();
String getGender();
String getBirthdate();
String getZoneInfo();
String getLocale();
String getPhoneNumber();
Boolean getPhoneNumberVerified();
Address getAddress();
Instant getUpdatedAt();
interface Address {
String getFormatted();
String getStreetAddress();
String getLocality();
String getRegion();
String getPostalCode();
String getCountry();
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.core.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A {@link GrantedAuthority} that is associated with an {@link OidcUser}.
*
* @author Joe Grandja
* @since 5.0
* @see OidcUser
*/
public class OidcUserAuthority extends OAuth2UserAuthority {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final IdToken idToken;
private final UserInfo userInfo;
public OidcUserAuthority(IdToken idToken) {
this(idToken, null);
}
public OidcUserAuthority(IdToken idToken, UserInfo userInfo) {
this("ROLE_USER", idToken, userInfo);
}
public OidcUserAuthority(String authority, IdToken idToken, UserInfo userInfo) {
super(authority, idToken.getClaims());
this.idToken = idToken;
this.userInfo = userInfo;
if (userInfo != null) {
this.setAttributes(
Stream.of(this.getAttributes(), userInfo.getClaims())
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (k1, k2) -> k1))
);
}
}
public IdToken getIdToken() {
return this.idToken;
}
public UserInfo getUserInfo() {
return this.userInfo;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || this.getClass() != obj.getClass()) {
return false;
}
if (!super.equals(obj)) {
return false;
}
OidcUserAuthority that = (OidcUserAuthority) obj;
if (!this.getIdToken().equals(that.getIdToken())) {
return false;
}
return this.getUserInfo() != null ? this.getUserInfo().equals(that.getUserInfo()) : that.getUserInfo() == null;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + this.getIdToken().hashCode();
result = 31 * result + (this.getUserInfo() != null ? this.getUserInfo().hashCode() : 0);
return result;
}
}

View File

@ -16,4 +16,4 @@
/**
* Provides a model for an <i>OpenID Connect Core 1.0</i> representation of a user <code>Principal</code>.
*/
package org.springframework.security.oauth2.oidc.user;
package org.springframework.security.oauth2.oidc.core.user;

View File

@ -1,154 +0,0 @@
/*
* Copyright 2012-2017 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
*
* http://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 org.springframework.security.oauth2.oidc.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.oidc.StandardClaimName;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import static org.springframework.security.oauth2.oidc.StandardClaimName.*;
/**
* The default implementation of a {@link UserInfo}.
*
* <p>
* The <i>key</i> used for accessing the &quot;name&quot; of the
* <code>Principal</code> (user) via {@link #getAttributes()}
* is {@link StandardClaimName#NAME} or if not available
* will default to {@link StandardClaimName#SUB}.
*
* @author Joe Grandja
* @since 5.0
* @see UserInfo
* @see DefaultOAuth2User
*/
public class DefaultUserInfo extends DefaultOAuth2User implements UserInfo {
public DefaultUserInfo(Map<String, Object> attributes) {
this(Collections.emptySet(), attributes);
}
public DefaultUserInfo(Set<GrantedAuthority> authorities, Map<String, Object> attributes) {
super(authorities, attributes, SUB);
}
@Override
public String getSubject() {
return this.getAttributeAsString(SUB);
}
@Override
public String getName() {
String name = this.getAttributeAsString(NAME);
return (name != null ? name : super.getName());
}
@Override
public String getGivenName() {
return this.getAttributeAsString(GIVEN_NAME);
}
@Override
public String getFamilyName() {
return this.getAttributeAsString(FAMILY_NAME);
}
@Override
public String getMiddleName() {
return this.getAttributeAsString(MIDDLE_NAME);
}
@Override
public String getNickName() {
return this.getAttributeAsString(NICKNAME);
}
@Override
public String getPreferredUsername() {
return this.getAttributeAsString(PREFERRED_USERNAME);
}
@Override
public String getProfile() {
return this.getAttributeAsString(PROFILE);
}
@Override
public String getPicture() {
return this.getAttributeAsString(PICTURE);
}
@Override
public String getWebsite() {
return this.getAttributeAsString(WEBSITE);
}
@Override
public String getEmail() {
return this.getAttributeAsString(EMAIL);
}
@Override
public Boolean getEmailVerified() {
return this.getAttributeAsBoolean(EMAIL_VERIFIED);
}
@Override
public String getGender() {
return this.getAttributeAsString(GENDER);
}
@Override
public String getBirthdate() {
return this.getAttributeAsString(BIRTHDATE);
}
@Override
public String getZoneInfo() {
return this.getAttributeAsString(ZONEINFO);
}
@Override
public String getLocale() {
return this.getAttributeAsString(LOCALE);
}
@Override
public String getPhoneNumber() {
return this.getAttributeAsString(PHONE_NUMBER);
}
@Override
public Boolean getPhoneNumberVerified() {
return this.getAttributeAsBoolean(PHONE_NUMBER_VERIFIED);
}
@Override
public Address getAddress() {
// TODO Impl
return null;
}
@Override
public Instant getUpdatedAt() {
return this.getAttributeAsInstant(UPDATED_AT);
}
}

View File

@ -35,6 +35,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
@ -48,6 +49,7 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
import org.springframework.security.oauth2.core.endpoint.ResponseType;
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
@ -389,7 +391,11 @@ public class OAuth2LoginApplicationTests {
attributes.put("last-name", "Grandja");
attributes.put("email", "joeg@springsecurity.io");
DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email");
GrantedAuthority authority = new OAuth2UserAuthority(attributes);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
DefaultOAuth2User user = new DefaultOAuth2User(authorities, attributes, "email");
OAuth2UserService mock = mock(OAuth2UserService.class);
when(mock.loadUser(any())).thenReturn(user);

View File

@ -24,21 +24,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.util.Set;
import java.util.function.Function;
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.*;
@ -54,8 +48,7 @@ import static org.springframework.boot.autoconfigure.security.oauth2.client.Clie
@AutoConfigureAfter(ClientRegistrationAutoConfiguration.class)
public class OAuth2LoginAutoConfiguration {
private static final String USER_INFO_URI_PROPERTY = "user-info-uri";
private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter";
private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key";
private static final String USER_NAME_ATTR_NAME_PROPERTY = "user-name-attribute-name";
@EnableWebSecurity
protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter {
@ -75,11 +68,11 @@ public class OAuth2LoginAutoConfiguration {
.and()
.oauth2Login();
this.registerUserInfoTypeConverters(http.oauth2Login());
this.registerUserNameAttributeNames(http.oauth2Login());
}
// @formatter:on
private void registerUserInfoTypeConverters(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
private void registerUserNameAttributeNames(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
for (String clientPropertyKey : clientPropertyKeys) {
String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + "." + clientPropertyKey;
@ -87,22 +80,9 @@ public class OAuth2LoginAutoConfiguration {
continue;
}
String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_URI_PROPERTY);
String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_CONVERTER_PROPERTY);
if (userInfoUriValue != null && userInfoConverterTypeValue != null) {
Class<? extends Function> userInfoConverterType = ClassUtils.resolveClassName(
userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Function.class);
Function<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = null;
if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) {
Constructor<? extends Function> oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class);
if (oauth2UserConverterConstructor != null) {
String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + "." + USER_INFO_NAME_ATTR_KEY_PROPERTY);
userInfoConverter = (Function<ClientHttpResponse, ? extends OAuth2User>)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
}
}
if (userInfoConverter == null) {
userInfoConverter = (Function<ClientHttpResponse, ? extends OAuth2User>)userInfoConverterType.newInstance();
}
oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
String userNameAttributeNameValue = this.environment.getProperty(fullClientPropertyKey + "." + USER_NAME_ATTR_NAME_PROPERTY);
if (userInfoUriValue != null && userNameAttributeNameValue != null) {
oauth2LoginConfigurer.userInfoEndpoint().userNameAttributeName(userNameAttributeNameValue, URI.create(userInfoUriValue));
}
}
}

View File

@ -16,17 +16,19 @@
package sample.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author Joe Grandja
*/
public class GitHubOAuth2User implements OAuth2User {
private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");
private String id;
private String name;
private String login;
@ -37,7 +39,7 @@ public class GitHubOAuth2User implements OAuth2User {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.emptyList();
return this.authorities;
}
@Override

View File

@ -9,7 +9,7 @@ security:
authorization-uri: "https://accounts.google.com/o/oauth2/auth"
token-uri: "https://accounts.google.com/o/oauth2/token"
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
client-name: Google
client-alias: google
github:
@ -20,7 +20,6 @@ security:
authorization-uri: "https://github.com/login/oauth/authorize"
token-uri: "https://github.com/login/oauth/access_token"
user-info-uri: "https://api.github.com/user"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
client-name: GitHub
client-alias: github
facebook:
@ -31,7 +30,6 @@ security:
authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
user-info-uri: "https://graph.facebook.com/me"
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
client-name: Facebook
client-alias: facebook
okta:
@ -39,6 +37,5 @@ security:
authorized-grant-type: authorization_code
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
scopes: openid, email, profile
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
client-name: Okta
client-alias: okta

View File

@ -21,14 +21,15 @@ security:
github:
client-id: your-app-client-id
client-secret: your-app-client-secret
user-info-name-attribute-key: "name"
user-name-attribute-name: "name"
facebook:
client-id: your-app-client-id
client-secret: your-app-client-secret
user-info-name-attribute-key: "name"
user-name-attribute-name: "name"
okta:
client-id: your-app-client-id
client-secret: your-app-client-secret
authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys