Merge Formatting Changes

Issue gh-8945
This commit is contained in:
Rob Winch
2020-08-24 16:38:12 -05:00
2779 changed files with 82438 additions and 91751 deletions

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.lang.Nullable;
@@ -21,8 +22,8 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* An implementation of an {@link OAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
* An implementation of an {@link OAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
*
* @author Joe Grandja
* @since 5.2
@@ -31,24 +32,27 @@ import org.springframework.util.Assert;
public final class AuthorizationCodeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
/**
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns {@code null} if authorization is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} OR the client is already authorized.
*
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration()
* client} in the provided {@code context}. Returns {@code null} if authorization is
* not supported, e.g. the client's
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is
* not {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} OR the
* client is already authorized.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not
* supported
*/
@Override
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getClientRegistration().getAuthorizationGrantType()) &&
context.getAuthorizedClient() == null) {
// ClientAuthorizationRequiredException is caught by OAuth2AuthorizationRequestRedirectFilter which initiates authorization
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(
context.getClientRegistration().getAuthorizationGrantType()) && context.getAuthorizedClient() == null) {
// ClientAuthorizationRequiredException is caught by
// OAuth2AuthorizationRequestRedirectFilter which initiates authorization
throw new ClientAuthorizationRequiredException(context.getClientRegistration().getRegistrationId());
}
return null;
}
}

View File

@@ -13,41 +13,48 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
*/
public final class AuthorizationCodeReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
public final class AuthorizationCodeReactiveOAuth2AuthorizedClientProvider
implements ReactiveOAuth2AuthorizedClientProvider {
/**
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns an empty {@code Mono} if authorization is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} OR the client is already authorized.
*
* Attempt to authorize the {@link OAuth2AuthorizationContext#getClientRegistration()
* client} in the provided {@code context}. Returns an empty {@code Mono} if
* authorization is not supported, e.g. the client's
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is
* not {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} OR the
* client is already authorized.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization is not supported
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if
* authorization is not supported
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(context.getClientRegistration().getAuthorizationGrantType()) &&
context.getAuthorizedClient() == null) {
// ClientAuthorizationRequiredException is caught by OAuth2AuthorizationRequestRedirectWebFilter which initiates authorization
return Mono.error(() -> new ClientAuthorizationRequiredException(context.getClientRegistration().getRegistrationId()));
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(
context.getClientRegistration().getAuthorizationGrantType()) && context.getAuthorizedClient() == null) {
// ClientAuthorizationRequiredException is caught by
// OAuth2AuthorizationRequestRedirectWebFilter which initiates authorization
return Mono.error(() -> new ClientAuthorizationRequiredException(
context.getClientRegistration().getRegistrationId()));
}
return Mono.empty();
}
}

View File

@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -27,41 +33,36 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* An implementation of an {@link OAuth2AuthorizedClientManager}
* that is capable of operating outside of the context of a {@code HttpServletRequest},
* e.g. in a scheduled/background thread and/or in the service-tier.
* An implementation of an {@link OAuth2AuthorizedClientManager} that is capable of
* operating outside of the context of a {@code HttpServletRequest}, e.g. in a
* scheduled/background thread and/or in the service-tier.
*
* <p>
* (When operating <em>within</em> the context of a {@code HttpServletRequest},
* use {@link DefaultOAuth2AuthorizedClientManager} instead.)
* (When operating <em>within</em> the context of a {@code HttpServletRequest}, use
* {@link DefaultOAuth2AuthorizedClientManager} instead.)
*
* <h2>Authorized Client Persistence</h2>
*
* <p>
* This manager utilizes an {@link OAuth2AuthorizedClientService}
* to persist {@link OAuth2AuthorizedClient}s.
* This manager utilizes an {@link OAuth2AuthorizedClientService} to persist
* {@link OAuth2AuthorizedClient}s.
*
* <p>
* By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
* will be saved in the {@link OAuth2AuthorizedClientService}.
* This functionality can be changed by configuring a custom {@link OAuth2AuthorizationSuccessHandler}
* via {@link #setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)}.
* will be saved in the {@link OAuth2AuthorizedClientService}. This functionality can be
* changed by configuring a custom {@link OAuth2AuthorizationSuccessHandler} via
* {@link #setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)}.
*
* <p>
* By default, when an authorization attempt fails due to an
* {@value OAuth2ErrorCodes#INVALID_GRANT} error,
* the previously saved {@link OAuth2AuthorizedClient}
* will be removed from the {@link OAuth2AuthorizedClientService}.
* (The {@value OAuth2ErrorCodes#INVALID_GRANT} error can occur
* when a refresh token that is no longer valid is used to retrieve a new access token.)
* This functionality can be changed by configuring a custom {@link OAuth2AuthorizationFailureHandler}
* via {@link #setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)}.
* {@value OAuth2ErrorCodes#INVALID_GRANT} error, the previously saved
* {@link OAuth2AuthorizedClient} will be removed from the
* {@link OAuth2AuthorizedClientService}. (The {@value OAuth2ErrorCodes#INVALID_GRANT}
* error can occur when a refresh token that is no longer valid is used to retrieve a new
* access token.) This functionality can be changed by configuring a custom
* {@link OAuth2AuthorizationFailureHandler} via
* {@link #setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)}.
*
* @author Joe Grandja
* @since 5.2
@@ -72,95 +73,113 @@ import java.util.function.Function;
* @see OAuth2AuthorizationFailureHandler
*/
public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager {
private static final OAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
private static final OAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = OAuth2AuthorizedClientProviderBuilder
.builder().clientCredentials().build();
private final ClientRegistrationRepository clientRegistrationRepository;
private final OAuth2AuthorizedClientService authorizedClientService;
private OAuth2AuthorizedClientProvider authorizedClientProvider;
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper;
private OAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
private OAuth2AuthorizationFailureHandler authorizationFailureHandler;
/**
* Constructs an {@code AuthorizedClientServiceOAuth2AuthorizedClientManager} using the provided parameters.
*
* Constructs an {@code AuthorizedClientServiceOAuth2AuthorizedClientManager} using
* the provided parameters.
* @param clientRegistrationRepository the repository of client registrations
* @param authorizedClientService the authorized client service
*/
public AuthorizedClientServiceOAuth2AuthorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
public AuthorizedClientServiceOAuth2AuthorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientService authorizedClientService) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
this.authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER;
this.contextAttributesMapper = new DefaultContextAttributesMapper();
this.authorizationSuccessHandler = (authorizedClient, principal, attributes) ->
authorizedClientService.saveAuthorizedClient(authorizedClient, principal);
this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientService
.saveAuthorizedClient(authorizedClient, principal);
this.authorizationFailureHandler = new RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) ->
authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()));
(clientRegistrationId, principal, attributes) -> authorizedClientService
.removeAuthorizedClient(clientRegistrationId, principal.getName()));
}
@Nullable
@Override
public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
String clientRegistrationId = authorizeRequest.getClientRegistrationId();
OAuth2AuthorizedClient authorizedClient = authorizeRequest.getAuthorizedClient();
Authentication principal = authorizeRequest.getPrincipal();
OAuth2AuthorizationContext.Builder contextBuilder;
if (authorizedClient != null) {
contextBuilder = OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient);
} else {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
Assert.notNull(clientRegistration, "Could not find ClientRegistration with id '" + clientRegistrationId + "'");
authorizedClient = this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName());
}
else {
ClientRegistration clientRegistration = this.clientRegistrationRepository
.findByRegistrationId(clientRegistrationId);
Assert.notNull(clientRegistration,
"Could not find ClientRegistration with id '" + clientRegistrationId + "'");
authorizedClient = this.authorizedClientService.loadAuthorizedClient(clientRegistrationId,
principal.getName());
if (authorizedClient != null) {
contextBuilder = OAuth2AuthorizationContext.withAuthorizedClient(authorizedClient);
} else {
}
else {
contextBuilder = OAuth2AuthorizationContext.withClientRegistration(clientRegistration);
}
}
OAuth2AuthorizationContext authorizationContext = contextBuilder
.principal(principal)
.attributes(attributes -> {
OAuth2AuthorizationContext authorizationContext = buildAuthorizationContext(authorizeRequest, principal,
contextBuilder);
try {
authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
}
catch (OAuth2AuthorizationException ex) {
this.authorizationFailureHandler.onAuthorizationFailure(ex, principal, Collections.emptyMap());
throw ex;
}
if (authorizedClient != null) {
this.authorizationSuccessHandler.onAuthorizationSuccess(authorizedClient, principal,
Collections.emptyMap());
}
else {
// In the case of re-authorization, the returned `authorizedClient` may be
// null if re-authorization is not supported.
// For these cases, return the provided
// `authorizationContext.authorizedClient`.
if (authorizationContext.getAuthorizedClient() != null) {
return authorizationContext.getAuthorizedClient();
}
}
return authorizedClient;
}
private OAuth2AuthorizationContext buildAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest,
Authentication principal, OAuth2AuthorizationContext.Builder contextBuilder) {
// @formatter:off
return contextBuilder.principal(principal)
.attributes((attributes) -> {
Map<String, Object> contextAttributes = this.contextAttributesMapper.apply(authorizeRequest);
if (!CollectionUtils.isEmpty(contextAttributes)) {
attributes.putAll(contextAttributes);
}
})
.build();
try {
authorizedClient = this.authorizedClientProvider.authorize(authorizationContext);
} catch (OAuth2AuthorizationException ex) {
this.authorizationFailureHandler.onAuthorizationFailure(ex, principal, Collections.emptyMap());
throw ex;
}
if (authorizedClient != null) {
this.authorizationSuccessHandler.onAuthorizationSuccess(
authorizedClient, principal, Collections.emptyMap());
} else {
// In the case of re-authorization, the returned `authorizedClient` may be null if re-authorization is not supported.
// For these cases, return the provided `authorizationContext.authorizedClient`.
if (authorizationContext.getAuthorizedClient() != null) {
return authorizationContext.getAuthorizedClient();
}
}
return authorizedClient;
// @formatter:on
}
/**
* Sets the {@link OAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
*
* @param authorizedClientProvider the {@link OAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client
* Sets the {@link OAuth2AuthorizedClientProvider} used for authorizing (or
* re-authorizing) an OAuth 2.0 Client.
* @param authorizedClientProvider the {@link OAuth2AuthorizedClientProvider} used for
* authorizing (or re-authorizing) an OAuth 2.0 Client
*/
public void setAuthorizedClientProvider(OAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
@@ -168,24 +187,28 @@ public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implemen
}
/**
* Sets the {@code Function} used for mapping attribute(s) from the {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes
* to be associated to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
*
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
* Sets the {@code Function} used for mapping attribute(s) from the
* {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes to be associated to
* the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
* @param contextAttributesMapper the {@code Function} used for supplying the
* {@code Map} of attributes to the {@link OAuth2AuthorizationContext#getAttributes()
* authorization context}
*/
public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper) {
public void setContextAttributesMapper(
Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper) {
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
this.contextAttributesMapper = contextAttributesMapper;
}
/**
* Sets the {@link OAuth2AuthorizationSuccessHandler} that handles successful authorizations.
* Sets the {@link OAuth2AuthorizationSuccessHandler} that handles successful
* authorizations.
*
* <p>
* The default saves {@link OAuth2AuthorizedClient}s in the {@link OAuth2AuthorizedClientService}.
*
* @param authorizationSuccessHandler the {@link OAuth2AuthorizationSuccessHandler} that handles successful authorizations
* The default saves {@link OAuth2AuthorizedClient}s in the
* {@link OAuth2AuthorizedClientService}.
* @param authorizationSuccessHandler the {@link OAuth2AuthorizationSuccessHandler}
* that handles successful authorizations
* @since 5.3
*/
public void setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
@@ -194,14 +217,16 @@ public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implemen
}
/**
* Sets the {@link OAuth2AuthorizationFailureHandler} that handles authorization failures.
* Sets the {@link OAuth2AuthorizationFailureHandler} that handles authorization
* failures.
*
* <p>
* A {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} is used by default.
*
* @param authorizationFailureHandler the {@link OAuth2AuthorizationFailureHandler} that handles authorization failures
* @see RemoveAuthorizedClientOAuth2AuthorizationFailureHandler
* A {@link RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} is used by
* default.
* @param authorizationFailureHandler the {@link OAuth2AuthorizationFailureHandler}
* that handles authorization failures
* @since 5.3
* @see RemoveAuthorizedClientOAuth2AuthorizationFailureHandler
*/
public void setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler authorizationFailureHandler) {
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
@@ -209,9 +234,11 @@ public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implemen
}
/**
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
* The default implementation of the {@link #setContextAttributesMapper(Function)
* contextAttributesMapper}.
*/
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Map<String, Object>> {
public static class DefaultContextAttributesMapper
implements Function<OAuth2AuthorizeRequest, Map<String, Object>> {
@Override
public Map<String, Object> apply(OAuth2AuthorizeRequest authorizeRequest) {
@@ -224,5 +251,7 @@ public final class AuthorizedClientServiceOAuth2AuthorizedClientManager implemen
}
return contextAttributes;
}
}
}

View File

@@ -13,78 +13,95 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientManager}
* that is capable of operating outside of the context of a {@link ServerWebExchange},
* e.g. in a scheduled/background thread and/or in the service-tier.
* An implementation of a {@link ReactiveOAuth2AuthorizedClientManager} that is capable of
* operating outside of the context of a {@link ServerWebExchange}, e.g. in a
* scheduled/background thread and/or in the service-tier.
*
* <p>(When operating <em>within</em> the context of a {@link ServerWebExchange},
* use {@link DefaultReactiveOAuth2AuthorizedClientManager} instead.)</p>
* <p>
* (When operating <em>within</em> the context of a {@link ServerWebExchange}, use
* {@link DefaultReactiveOAuth2AuthorizedClientManager} instead.)
* </p>
*
* <p>This is a reactive equivalent of {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}.</p>
* <p>
* This is a reactive equivalent of
* {@link org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager}.
* </p>
*
* <h2>Authorized Client Persistence</h2>
*
* <p>This client manager utilizes a {@link ReactiveOAuth2AuthorizedClientService}
* to persist {@link OAuth2AuthorizedClient}s.</p>
* <p>
* This client manager utilizes a {@link ReactiveOAuth2AuthorizedClientService} to persist
* {@link OAuth2AuthorizedClient}s.
* </p>
*
* <p>By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
* will be saved in the authorized client service.
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler}
* via {@link #setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)}.</p>
* <p>
* By default, when an authorization attempt succeeds, the {@link OAuth2AuthorizedClient}
* will be saved in the authorized client service. This functionality can be changed by
* configuring a custom {@link ReactiveOAuth2AuthorizationSuccessHandler} via
* {@link #setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)}.
* </p>
*
* <p>By default, when an authorization attempt fails due to an
* <p>
* By default, when an authorization attempt fails due to an
* {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error,
* the previously saved {@link OAuth2AuthorizedClient}
* will be removed from the authorized client service.
* (The {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT}
* error generally occurs when a refresh token that is no longer valid
* is used to retrieve a new access token.)
* This functionality can be changed by configuring a custom {@link ReactiveOAuth2AuthorizationFailureHandler}
* via {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.</p>
* the previously saved {@link OAuth2AuthorizedClient} will be removed from the authorized
* client service. (The
* {@value org.springframework.security.oauth2.core.OAuth2ErrorCodes#INVALID_GRANT} error
* generally occurs when a refresh token that is no longer valid is used to retrieve a new
* access token.) This functionality can be changed by configuring a custom
* {@link ReactiveOAuth2AuthorizationFailureHandler} via
* {@link #setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)}.
* </p>
*
* @author Ankur Pathak
* @author Phil Clay
* @since 5.2.2
* @see ReactiveOAuth2AuthorizedClientManager
* @see ReactiveOAuth2AuthorizedClientProvider
* @see ReactiveOAuth2AuthorizedClientService
* @see ReactiveOAuth2AuthorizationSuccessHandler
* @see ReactiveOAuth2AuthorizationFailureHandler
* @since 5.2.2
*/
public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
implements ReactiveOAuth2AuthorizedClientManager {
private static final ReactiveOAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER =
ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
private static final ReactiveOAuth2AuthorizedClientProvider DEFAULT_AUTHORIZED_CLIENT_PROVIDER = ReactiveOAuth2AuthorizedClientProviderBuilder
.builder().clientCredentials().build();
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
private ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = DEFAULT_AUTHORIZED_CLIENT_PROVIDER;
private Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper = new DefaultContextAttributesMapper();
private ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler;
private ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler;
/**
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager} using the provided parameters.
*
* Constructs an {@code AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager}
* using the provided parameters.
* @param clientRegistrationRepository the repository of client registrations
* @param authorizedClientService the authorized client service
* @param authorizedClientService the authorized client service
*/
public AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
ReactiveClientRegistrationRepository clientRegistrationRepository,
@@ -93,19 +110,18 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientService = authorizedClientService;
this.authorizationSuccessHandler = (authorizedClient, principal, attributes) ->
authorizedClientService.saveAuthorizedClient(authorizedClient, principal);
this.authorizationSuccessHandler = (authorizedClient, principal, attributes) -> authorizedClientService
.saveAuthorizedClient(authorizedClient, principal);
this.authorizationFailureHandler = new RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
(clientRegistrationId, principal, attributes) ->
this.authorizedClientService.removeAuthorizedClient(clientRegistrationId, principal.getName()));
(clientRegistrationId, principal, attributes) -> this.authorizedClientService
.removeAuthorizedClient(clientRegistrationId, principal.getName()));
}
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest, "authorizeRequest cannot be null");
return createAuthorizationContext(authorizeRequest)
.flatMap(authorizationContext -> authorize(authorizationContext, authorizeRequest.getPrincipal()));
.flatMap((authorizationContext) -> authorize(authorizationContext, authorizeRequest.getPrincipal()));
}
private Mono<OAuth2AuthorizationContext> createAuthorizationContext(OAuth2AuthorizeRequest authorizeRequest) {
@@ -113,56 +129,57 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
Authentication principal = authorizeRequest.getPrincipal();
return Mono.justOrEmpty(authorizeRequest.getAuthorizedClient())
.map(OAuth2AuthorizationContext::withAuthorizedClient)
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.flatMap(clientRegistration -> this.authorizedClientService.loadAuthorizedClient(clientRegistrationId, principal.getName())
.switchIfEmpty(Mono.defer(() -> this.clientRegistrationRepository
.findByRegistrationId(clientRegistrationId)
.flatMap((clientRegistration) -> this.authorizedClientService
.loadAuthorizedClient(clientRegistrationId, principal.getName())
.map(OAuth2AuthorizationContext::withAuthorizedClient)
.switchIfEmpty(Mono.fromSupplier(() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
.flatMap(contextBuilder -> this.contextAttributesMapper.apply(authorizeRequest)
.defaultIfEmpty(Collections.emptyMap())
.map(contextAttributes -> {
.switchIfEmpty(Mono.fromSupplier(
() -> OAuth2AuthorizationContext.withClientRegistration(clientRegistration))))
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException(
"Could not find ClientRegistration with id '" + clientRegistrationId + "'")))))
.flatMap((contextBuilder) -> this.contextAttributesMapper.apply(authorizeRequest)
.defaultIfEmpty(Collections.emptyMap()).map((contextAttributes) -> {
OAuth2AuthorizationContext.Builder builder = contextBuilder.principal(principal);
if (!contextAttributes.isEmpty()) {
builder = builder.attributes(attributes -> attributes.putAll(contextAttributes));
builder = builder.attributes((attributes) -> attributes.putAll(contextAttributes));
}
return builder.build();
}));
}
/**
* Performs authorization and then delegates to either the {@link #authorizationSuccessHandler}
* or {@link #authorizationFailureHandler}, depending on the authorization result.
*
* Performs authorization and then delegates to either the
* {@link #authorizationSuccessHandler} or {@link #authorizationFailureHandler},
* depending on the authorization result.
* @param authorizationContext the context to authorize
* @param principal the principle to authorize
* @return a {@link Mono} that emits the authorized client after the authorization attempt succeeds
* and the {@link #authorizationSuccessHandler} has completed,
* or completes with an exception after the authorization attempt fails
* and the {@link #authorizationFailureHandler} has completed
* @return a {@link Mono} that emits the authorized client after the authorization
* attempt succeeds and the {@link #authorizationSuccessHandler} has completed, or
* completes with an exception after the authorization attempt fails and the
* {@link #authorizationFailureHandler} has completed
*/
private Mono<OAuth2AuthorizedClient> authorize(
OAuth2AuthorizationContext authorizationContext,
private Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext authorizationContext,
Authentication principal) {
return this.authorizedClientProvider.authorize(authorizationContext)
// Delegate to the authorizationSuccessHandler of the successful authorization
.flatMap(authorizedClient -> this.authorizationSuccessHandler.onAuthorizationSuccess(
authorizedClient,
principal,
Collections.emptyMap())
// Delegate to the authorizationSuccessHandler of the successful
// authorization
.flatMap((authorizedClient) -> this.authorizationSuccessHandler
.onAuthorizationSuccess(authorizedClient, principal, Collections.emptyMap())
.thenReturn(authorizedClient))
// Delegate to the authorizationFailureHandler of the failed authorization
.onErrorResume(OAuth2AuthorizationException.class, authorizationException -> this.authorizationFailureHandler.onAuthorizationFailure(
authorizationException,
principal,
Collections.emptyMap())
.then(Mono.error(authorizationException)))
.onErrorResume(OAuth2AuthorizationException.class,
(authorizationException) -> this.authorizationFailureHandler
.onAuthorizationFailure(authorizationException, principal, Collections.emptyMap())
.then(Mono.error(authorizationException)))
.switchIfEmpty(Mono.defer(() -> Mono.justOrEmpty(authorizationContext.getAuthorizedClient())));
}
/**
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client.
*
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or re-authorizing) an OAuth 2.0 Client
* Sets the {@link ReactiveOAuth2AuthorizedClientProvider} used for authorizing (or
* re-authorizing) an OAuth 2.0 Client.
* @param authorizedClientProvider the {@link ReactiveOAuth2AuthorizedClientProvider}
* used for authorizing (or re-authorizing) an OAuth 2.0 Client
*/
public void setAuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider) {
Assert.notNull(authorizedClientProvider, "authorizedClientProvider cannot be null");
@@ -170,13 +187,15 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
}
/**
* Sets the {@code Function} used for mapping attribute(s) from the {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes
* to be associated to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
*
* @param contextAttributesMapper the {@code Function} used for supplying the {@code Map} of attributes
* to the {@link OAuth2AuthorizationContext#getAttributes() authorization context}
* Sets the {@code Function} used for mapping attribute(s) from the
* {@link OAuth2AuthorizeRequest} to a {@code Map} of attributes to be associated to
* the {@link OAuth2AuthorizationContext#getAttributes() authorization context}.
* @param contextAttributesMapper the {@code Function} used for supplying the
* {@code Map} of attributes to the {@link OAuth2AuthorizationContext#getAttributes()
* authorization context}
*/
public void setContextAttributesMapper(Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
public void setContextAttributesMapper(
Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> contextAttributesMapper) {
Assert.notNull(contextAttributesMapper, "contextAttributesMapper cannot be null");
this.contextAttributesMapper = contextAttributesMapper;
}
@@ -184,9 +203,10 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
/**
* Sets the handler that handles successful authorizations.
*
* The default saves {@link OAuth2AuthorizedClient}s in the {@link ReactiveOAuth2AuthorizedClientService}.
*
* @param authorizationSuccessHandler the handler that handles successful authorizations.
* The default saves {@link OAuth2AuthorizedClient}s in the
* {@link ReactiveOAuth2AuthorizedClientService}.
* @param authorizationSuccessHandler the handler that handles successful
* authorizations.
* @since 5.3
*/
public void setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler authorizationSuccessHandler) {
@@ -197,12 +217,13 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
/**
* Sets the handler that handles authorization failures.
*
* <p>A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler}
* is used by default.</p>
*
* <p>
* A {@link RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} is used
* by default.
* </p>
* @param authorizationFailureHandler the handler that handles authorization failures.
* @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
* @since 5.3
* @see RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
*/
public void setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler authorizationFailureHandler) {
Assert.notNull(authorizationFailureHandler, "authorizationFailureHandler cannot be null");
@@ -210,16 +231,19 @@ public final class AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager
}
/**
* The default implementation of the {@link #setContextAttributesMapper(Function) contextAttributesMapper}.
* The default implementation of the {@link #setContextAttributesMapper(Function)
* contextAttributesMapper}.
*/
public static class DefaultContextAttributesMapper implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
public static class DefaultContextAttributesMapper
implements Function<OAuth2AuthorizeRequest, Mono<Map<String, Object>>> {
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper =
new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
private final AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper mapper = new AuthorizedClientServiceOAuth2AuthorizedClientManager.DefaultContextAttributesMapper();
@Override
public Mono<Map<String, Object>> apply(OAuth2AuthorizeRequest authorizeRequest) {
return Mono.fromCallable(() -> mapper.apply(authorizeRequest));
return Mono.fromCallable(() -> this.mapper.apply(authorizeRequest));
}
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
@@ -20,8 +21,8 @@ import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.util.Assert;
/**
* This exception is thrown on the client side when an attempt to authenticate
* or authorize an OAuth 2.0 client fails.
* This exception is thrown on the client side when an attempt to authenticate or
* authorize an OAuth 2.0 client fails.
*
* @author Phil Clay
* @since 5.3
@@ -33,16 +34,15 @@ public class ClientAuthorizationException extends OAuth2AuthorizationException {
/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
*/
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId) {
this(error, clientRegistrationId, error.toString());
}
/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
* @param message the exception message
@@ -55,7 +55,6 @@ public class ClientAuthorizationException extends OAuth2AuthorizationException {
/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
* @param cause the root cause
@@ -66,13 +65,13 @@ public class ClientAuthorizationException extends OAuth2AuthorizationException {
/**
* Constructs a {@code ClientAuthorizationException} using the provided parameters.
*
* @param error the {@link OAuth2Error OAuth 2.0 Error}
* @param clientRegistrationId the identifier for the client's registration
* @param message the exception message
* @param cause the root cause
*/
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, String message, Throwable cause) {
public ClientAuthorizationException(OAuth2Error error, String clientRegistrationId, String message,
Throwable cause) {
super(error, message, cause);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
this.clientRegistrationId = clientRegistrationId;
@@ -80,10 +79,10 @@ public class ClientAuthorizationException extends OAuth2AuthorizationException {
/**
* Returns the identifier for the client's registration.
*
* @return the identifier for the client's registration
*/
public String getClientRegistrationId() {
return this.clientRegistrationId;
}
}

View File

@@ -13,24 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.oauth2.core.OAuth2Error;
/**
* This exception is thrown when an OAuth 2.0 Client is required
* to obtain authorization from the Resource Owner.
* This exception is thrown when an OAuth 2.0 Client is required to obtain authorization
* from the Resource Owner.
*
* @author Joe Grandja
* @since 5.1
* @see OAuth2AuthorizedClient
*/
public class ClientAuthorizationRequiredException extends ClientAuthorizationException {
private static final String CLIENT_AUTHORIZATION_REQUIRED_ERROR_CODE = "client_authorization_required";
/**
* Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters.
*
* Constructs a {@code ClientAuthorizationRequiredException} using the provided
* parameters.
* @param clientRegistrationId the identifier for the client's registration
*/
public ClientAuthorizationRequiredException(String clientRegistrationId) {
@@ -38,4 +40,5 @@ public class ClientAuthorizationRequiredException extends ClientAuthorizationExc
"Authorization required for Client Registration Id: " + clientRegistrationId, null),
clientRegistrationId);
}
}

View File

@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
@@ -26,13 +31,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.util.Assert;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
/**
* An implementation of an {@link OAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
* An implementation of an {@link OAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
*
* @author Joe Grandja
* @since 5.2
@@ -40,55 +41,60 @@ import java.time.Instant;
* @see DefaultClientCredentialsTokenResponseClient
*/
public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = new DefaultClientCredentialsTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns {@code null} if authorization (or re-authorization) is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* Attempt to authorize (or re-authorize) the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* {@code context}. Returns {@code null} if authorization (or re-authorization) is not
* supported, e.g. the client's {@link ClientRegistration#getAuthorizationGrantType()
* authorization grant type} is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS
* client_credentials} OR the {@link OAuth2AuthorizedClient#getAccessToken() access
* token} is not expired.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or re-authorization) is not supported
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or
* re-authorization) is not supported
*/
@Override
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
ClientRegistration clientRegistration = context.getClientRegistration();
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized but access token is NOT expired than no need for re-authorization
// If client is already authorized but access token is NOT expired than no
// need for re-authorization
return null;
}
// As per spec, in section 4.4.3 Access Token Response
// https://tools.ietf.org/html/rfc6749#section-4.4.3
// A refresh token SHOULD NOT be included.
//
// Therefore, renewing an expired access token (re-authorization)
// is the same as acquiring a new access token (authorization).
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
clientRegistration);
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, clientCredentialsGrantRequest);
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken());
}
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest =
new OAuth2ClientCredentialsGrantRequest(clientRegistration);
OAuth2AccessTokenResponse tokenResponse;
private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
try {
tokenResponse = this.accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest);
} catch (OAuth2AuthorizationException ex) {
return this.accessTokenResponseClient.getTokenResponse(clientCredentialsGrantRequest);
}
catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken());
}
private boolean hasTokenExpired(AbstractOAuth2Token token) {
@@ -96,23 +102,26 @@ public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OA
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code client_credentials} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code client_credentials} grant
*/
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds.
*
* <p>
* An access token is considered expired if {@code OAuth2AccessToken#getExpiresAt() - clockSkew}
* is before the current time {@code clock#instant()}.
*
* An access token is considered expired if
* {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
@@ -122,12 +131,13 @@ public final class ClientCredentialsOAuth2AuthorizedClientProvider implements OA
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveClientCredentialsTokenResponseClient;
@@ -23,65 +30,63 @@ import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
* @see WebClientReactiveClientCredentialsTokenResponseClient
*/
public final class ClientCredentialsReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient =
new WebClientReactiveClientCredentialsTokenResponseClient();
public final class ClientCredentialsReactiveOAuth2AuthorizedClientProvider
implements ReactiveOAuth2AuthorizedClientProvider {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = new WebClientReactiveClientCredentialsTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns an empty {@code Mono} if authorization (or re-authorization) is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* Attempt to authorize (or re-authorize) the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* {@code context}. Returns an empty {@code Mono} if authorization (or
* re-authorization) is not supported, e.g. the client's
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is
* not {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} OR the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization (or re-authorization) is not supported
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if
* authorization (or re-authorization) is not supported
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
ClientRegistration clientRegistration = context.getClientRegistration();
if (!AuthorizationGrantType.CLIENT_CREDENTIALS.equals(clientRegistration.getAuthorizationGrantType())) {
return Mono.empty();
}
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized but access token is NOT expired than no need for re-authorization
// If client is already authorized but access token is NOT expired than no
// need for re-authorization
return Mono.empty();
}
// As per spec, in section 4.4.3 Access Token Response
// https://tools.ietf.org/html/rfc6749#section-4.4.3
// A refresh token SHOULD NOT be included.
//
// Therefore, renewing an expired access token (re-authorization)
// is the same as acquiring a new access token (authorization).
return Mono.just(new OAuth2ClientCredentialsGrantRequest(clientRegistration))
.flatMap(this.accessTokenResponseClient::getTokenResponse)
.onErrorMap(OAuth2AuthorizationException.class,
e -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(), e))
.map(tokenResponse -> new OAuth2AuthorizedClient(
clientRegistration, context.getPrincipal().getName(), tokenResponse.getAccessToken()));
(ex) -> new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(),
ex))
.map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken()));
}
private boolean hasTokenExpired(AbstractOAuth2Token token) {
@@ -89,23 +94,26 @@ public final class ClientCredentialsReactiveOAuth2AuthorizedClientProvider imple
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code client_credentials} grant
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code client_credentials} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code client_credentials} grant
*/
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
public void setAccessTokenResponseClient(
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds.
*
* <p>
* An access token is considered expired if {@code OAuth2AccessToken#getExpiresAt() - clockSkew}
* is before the current time {@code clock#instant()}.
*
* An access token is considered expired if
* {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
@@ -115,12 +123,13 @@ public final class ClientCredentialsReactiveOAuth2AuthorizedClientProvider imple
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@@ -13,36 +13,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
package org.springframework.security.oauth2.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An implementation of an {@link OAuth2AuthorizedClientProvider} that simply delegates
* to it's internal {@code List} of {@link OAuth2AuthorizedClientProvider}(s).
* An implementation of an {@link OAuth2AuthorizedClientProvider} that simply delegates to
* it's internal {@code List} of {@link OAuth2AuthorizedClientProvider}(s).
* <p>
* Each provider is given a chance to
* {@link OAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext) authorize}
* the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context
* with the first {@code non-null} {@link OAuth2AuthorizedClient} being returned.
* the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* context with the first {@code non-null} {@link OAuth2AuthorizedClient} being returned.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AuthorizedClientProvider
*/
public final class DelegatingOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
private final List<OAuth2AuthorizedClientProvider> authorizedClientProviders;
/**
* Constructs a {@code DelegatingOAuth2AuthorizedClientProvider} using the provided parameters.
*
* @param authorizedClientProviders a list of {@link OAuth2AuthorizedClientProvider}(s)
* Constructs a {@code DelegatingOAuth2AuthorizedClientProvider} using the provided
* parameters.
* @param authorizedClientProviders a list of
* {@link OAuth2AuthorizedClientProvider}(s)
*/
public DelegatingOAuth2AuthorizedClientProvider(OAuth2AuthorizedClientProvider... authorizedClientProviders) {
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
@@ -50,9 +53,10 @@ public final class DelegatingOAuth2AuthorizedClientProvider implements OAuth2Aut
}
/**
* Constructs a {@code DelegatingOAuth2AuthorizedClientProvider} using the provided parameters.
*
* @param authorizedClientProviders a {@code List} of {@link OAuth2AuthorizedClientProvider}(s)
* Constructs a {@code DelegatingOAuth2AuthorizedClientProvider} using the provided
* parameters.
* @param authorizedClientProviders a {@code List} of
* {@link OAuth2AuthorizedClientProvider}(s)
*/
public DelegatingOAuth2AuthorizedClientProvider(List<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
@@ -63,7 +67,7 @@ public final class DelegatingOAuth2AuthorizedClientProvider implements OAuth2Aut
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
for (OAuth2AuthorizedClientProvider authorizedClientProvider : authorizedClientProviders) {
for (OAuth2AuthorizedClientProvider authorizedClientProvider : this.authorizedClientProviders) {
OAuth2AuthorizedClient oauth2AuthorizedClient = authorizedClientProvider.authorize(context);
if (oauth2AuthorizedClient != null) {
return oauth2AuthorizedClient;
@@ -71,4 +75,5 @@ public final class DelegatingOAuth2AuthorizedClientProvider implements OAuth2Aut
}
return null;
}
}

View File

@@ -13,49 +13,58 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
package org.springframework.security.oauth2.client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.util.Assert;
/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} that simply delegates
* to it's internal {@code List} of {@link ReactiveOAuth2AuthorizedClientProvider}(s).
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} that simply
* delegates to it's internal {@code List} of
* {@link ReactiveOAuth2AuthorizedClientProvider}(s).
* <p>
* Each provider is given a chance to
* {@link ReactiveOAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext) authorize}
* the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context
* with the first available {@link OAuth2AuthorizedClient} being returned.
* {@link ReactiveOAuth2AuthorizedClientProvider#authorize(OAuth2AuthorizationContext)
* authorize} the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the
* provided context with the first available {@link OAuth2AuthorizedClient} being
* returned.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
*/
public final class DelegatingReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
private final List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders;
/**
* Constructs a {@code DelegatingReactiveOAuth2AuthorizedClientProvider} using the provided parameters.
*
* @param authorizedClientProviders a list of {@link ReactiveOAuth2AuthorizedClientProvider}(s)
* Constructs a {@code DelegatingReactiveOAuth2AuthorizedClientProvider} using the
* provided parameters.
* @param authorizedClientProviders a list of
* {@link ReactiveOAuth2AuthorizedClientProvider}(s)
*/
public DelegatingReactiveOAuth2AuthorizedClientProvider(ReactiveOAuth2AuthorizedClientProvider... authorizedClientProviders) {
public DelegatingReactiveOAuth2AuthorizedClientProvider(
ReactiveOAuth2AuthorizedClientProvider... authorizedClientProviders) {
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
this.authorizedClientProviders = Collections.unmodifiableList(Arrays.asList(authorizedClientProviders));
}
/**
* Constructs a {@code DelegatingReactiveOAuth2AuthorizedClientProvider} using the provided parameters.
*
* @param authorizedClientProviders a {@code List} of {@link OAuth2AuthorizedClientProvider}(s)
* Constructs a {@code DelegatingReactiveOAuth2AuthorizedClientProvider} using the
* provided parameters.
* @param authorizedClientProviders a {@code List} of
* {@link OAuth2AuthorizedClientProvider}(s)
*/
public DelegatingReactiveOAuth2AuthorizedClientProvider(List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
public DelegatingReactiveOAuth2AuthorizedClientProvider(
List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders) {
Assert.notEmpty(authorizedClientProviders, "authorizedClientProviders cannot be empty");
this.authorizedClientProviders = Collections.unmodifiableList(new ArrayList<>(authorizedClientProviders));
}
@@ -64,7 +73,7 @@ public final class DelegatingReactiveOAuth2AuthorizedClientProvider implements R
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
return Flux.fromIterable(this.authorizedClientProviders)
.concatMap(authorizedClientProvider -> authorizedClientProvider.authorize(context))
.next();
.concatMap((authorizedClientProvider) -> authorizedClientProvider.authorize(context)).next();
}
}

View File

@@ -13,19 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.util.Assert;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* An {@link OAuth2AuthorizedClientService} that stores
* {@link OAuth2AuthorizedClient Authorized Client(s)} in-memory.
* An {@link OAuth2AuthorizedClientService} that stores {@link OAuth2AuthorizedClient
* Authorized Client(s)} in-memory.
*
* @author Joe Grandja
* @author Vedran Pavic
@@ -37,12 +38,14 @@ import java.util.concurrent.ConcurrentHashMap;
* @see Authentication
*/
public final class InMemoryOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private final Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients;
private final ClientRegistrationRepository clientRegistrationRepository;
/**
* Constructs an {@code InMemoryOAuth2AuthorizedClientService} using the provided parameters.
*
* Constructs an {@code InMemoryOAuth2AuthorizedClientService} using the provided
* parameters.
* @param clientRegistrationRepository the repository of client registrations
*/
public InMemoryOAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository) {
@@ -52,14 +55,15 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
}
/**
* Constructs an {@code InMemoryOAuth2AuthorizedClientService} using the provided parameters.
*
* @since 5.2
* Constructs an {@code InMemoryOAuth2AuthorizedClientService} using the provided
* parameters.
* @param clientRegistrationRepository the repository of client registrations
* @param authorizedClients the initial {@code Map} of authorized client(s) keyed by {@link OAuth2AuthorizedClientId}
* @param authorizedClients the initial {@code Map} of authorized client(s) keyed by
* {@link OAuth2AuthorizedClientId}
* @since 5.2
*/
public InMemoryOAuth2AuthorizedClientService(ClientRegistrationRepository clientRegistrationRepository,
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients) {
Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.notEmpty(authorizedClients, "authorizedClients cannot be empty");
this.clientRegistrationRepository = clientRegistrationRepository;
@@ -68,7 +72,8 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
@Override
@SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName) {
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId,
String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId);
@@ -82,8 +87,8 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
this.authorizedClients.put(new OAuth2AuthorizedClientId(authorizedClient.getClientRegistration().getRegistrationId(),
principal.getName()), authorizedClient);
this.authorizedClients.put(new OAuth2AuthorizedClientId(
authorizedClient.getClientRegistration().getRegistrationId(), principal.getName()), authorizedClient);
}
@Override
@@ -95,4 +100,5 @@ public final class InMemoryOAuth2AuthorizedClientService implements OAuth2Author
this.authorizedClients.remove(new OAuth2AuthorizedClientId(clientRegistrationId, principalName));
}
}
}

View File

@@ -13,20 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* An {@link OAuth2AuthorizedClientService} that stores
* {@link OAuth2AuthorizedClient Authorized Client(s)} in-memory.
* An {@link OAuth2AuthorizedClientService} that stores {@link OAuth2AuthorizedClient
* Authorized Client(s)} in-memory.
*
* @author Rob Winch
* @author Vedran Pavic
@@ -37,27 +39,31 @@ import java.util.concurrent.ConcurrentHashMap;
* @see Authentication
*/
public final class InMemoryReactiveOAuth2AuthorizedClientService implements ReactiveOAuth2AuthorizedClientService {
private final Map<OAuth2AuthorizedClientId, OAuth2AuthorizedClient> authorizedClients = new ConcurrentHashMap<>();
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
/**
* Constructs an {@code InMemoryReactiveOAuth2AuthorizedClientService} using the provided parameters.
*
* Constructs an {@code InMemoryReactiveOAuth2AuthorizedClientService} using the
* provided parameters.
* @param clientRegistrationRepository the repository of client registrations
*/
public InMemoryReactiveOAuth2AuthorizedClientService(ReactiveClientRegistrationRepository clientRegistrationRepository) {
public InMemoryReactiveOAuth2AuthorizedClientService(
ReactiveClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
@SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName) {
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId,
String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return (Mono<T>) this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.map(clientRegistration -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.flatMap(identifier -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.flatMap((identifier) -> Mono.justOrEmpty(this.authorizedClients.get(identifier)));
}
@Override
@@ -75,9 +81,12 @@ public final class InMemoryReactiveOAuth2AuthorizedClientService implements Reac
public Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
// @formatter:off
return this.clientRegistrationRepository.findByRegistrationId(clientRegistrationId)
.map(clientRegistration -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.map((clientRegistration) -> new OAuth2AuthorizedClientId(clientRegistrationId, principalName))
.doOnNext(this.authorizedClients::remove)
.then(Mono.empty());
// @formatter:on
}
}

View File

@@ -13,8 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.nio.charset.StandardCharsets;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.ArgumentPreparedStatementSetter;
@@ -31,26 +44,15 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
/**
* A JDBC implementation of an {@link OAuth2AuthorizedClientService}
* that uses a {@link JdbcOperations} for {@link OAuth2AuthorizedClient} persistence.
* A JDBC implementation of an {@link OAuth2AuthorizedClientService} that uses a
* {@link JdbcOperations} for {@link OAuth2AuthorizedClient} persistence.
*
* <p>
* <b>NOTE:</b> This {@code OAuth2AuthorizedClientService} depends on the table definition
* described in "classpath:org/springframework/security/oauth2/client/oauth2-client-schema.sql"
* and therefore MUST be defined in the database schema.
* described in
* "classpath:org/springframework/security/oauth2/client/oauth2-client-schema.sql" and
* therefore MUST be defined in the database schema.
*
* @author Joe Grandja
* @author Stav Shamir
@@ -61,42 +63,58 @@ import java.util.function.Function;
* @see RowMapper
*/
public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClientService {
private static final String COLUMN_NAMES =
"client_registration_id, " +
"principal_name, " +
"access_token_type, " +
"access_token_value, " +
"access_token_issued_at, " +
"access_token_expires_at, " +
"access_token_scopes, " +
"refresh_token_value, " +
"refresh_token_issued_at";
// @formatter:off
private static final String COLUMN_NAMES = "client_registration_id, "
+ "principal_name, "
+ "access_token_type, "
+ "access_token_value, "
+ "access_token_issued_at, "
+ "access_token_expires_at, "
+ "access_token_scopes, "
+ "refresh_token_value, "
+ "refresh_token_issued_at";
// @formatter:on
private static final String TABLE_NAME = "oauth2_authorized_client";
private static final String PK_FILTER = "client_registration_id = ? AND principal_name = ?";
private static final String LOAD_AUTHORIZED_CLIENT_SQL = "SELECT " + COLUMN_NAMES +
" FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
private static final String SAVE_AUTHORIZED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME +
" (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String REMOVE_AUTHORIZED_CLIENT_SQL = "DELETE FROM " + TABLE_NAME +
" WHERE " + PK_FILTER;
private static final String UPDATE_AUTHORIZED_CLIENT_SQL = "UPDATE " + TABLE_NAME +
" SET access_token_type = ?, access_token_value = ?, access_token_issued_at = ?," +
" access_token_expires_at = ?, access_token_scopes = ?," +
" refresh_token_value = ?, refresh_token_issued_at = ?" +
" WHERE " + PK_FILTER;
// @formatter:off
private static final String LOAD_AUTHORIZED_CLIENT_SQL = "SELECT " + COLUMN_NAMES
+ " FROM " + TABLE_NAME
+ " WHERE " + PK_FILTER;
// @formatter:on
// @formatter:off
private static final String SAVE_AUTHORIZED_CLIENT_SQL = "INSERT INTO " + TABLE_NAME
+ " (" + COLUMN_NAMES + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
// @formatter:on
private static final String REMOVE_AUTHORIZED_CLIENT_SQL = "DELETE FROM " + TABLE_NAME + " WHERE " + PK_FILTER;
// @formatter:off
private static final String UPDATE_AUTHORIZED_CLIENT_SQL = "UPDATE " + TABLE_NAME
+ " SET access_token_type = ?, access_token_value = ?, access_token_issued_at = ?,"
+ " access_token_expires_at = ?, access_token_scopes = ?,"
+ " refresh_token_value = ?, refresh_token_issued_at = ?"
+ " WHERE " + PK_FILTER;
// @formatter:on
protected final JdbcOperations jdbcOperations;
protected RowMapper<OAuth2AuthorizedClient> authorizedClientRowMapper;
protected Function<OAuth2AuthorizedClientHolder, List<SqlParameterValue>> authorizedClientParametersMapper;
/**
* Constructs a {@code JdbcOAuth2AuthorizedClientService} using the provided parameters.
*
* Constructs a {@code JdbcOAuth2AuthorizedClientService} using the provided
* parameters.
* @param jdbcOperations the JDBC operations
* @param clientRegistrationRepository the repository of client registrations
*/
public JdbcOAuth2AuthorizedClientService(
JdbcOperations jdbcOperations, ClientRegistrationRepository clientRegistrationRepository) {
public JdbcOAuth2AuthorizedClientService(JdbcOperations jdbcOperations,
ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(jdbcOperations, "jdbcOperations cannot be null");
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.jdbcOperations = jdbcOperations;
@@ -106,19 +124,16 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
@Override
@SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName) {
public <T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId,
String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
SqlParameterValue[] parameters = new SqlParameterValue[] {
new SqlParameterValue(Types.VARCHAR, clientRegistrationId),
new SqlParameterValue(Types.VARCHAR, principalName)
};
new SqlParameterValue(Types.VARCHAR, principalName) };
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
List<OAuth2AuthorizedClient> result = this.jdbcOperations.query(
LOAD_AUTHORIZED_CLIENT_SQL, pss, this.authorizedClientRowMapper);
List<OAuth2AuthorizedClient> result = this.jdbcOperations.query(LOAD_AUTHORIZED_CLIENT_SQL, pss,
this.authorizedClientRowMapper);
return !result.isEmpty() ? (T) result.get(0) : null;
}
@@ -126,40 +141,36 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
public void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
boolean existsAuthorizedClient = null != this.loadAuthorizedClient(
authorizedClient.getClientRegistration().getRegistrationId(), principal.getName());
if (existsAuthorizedClient) {
updateAuthorizedClient(authorizedClient, principal);
} else {
}
else {
try {
insertAuthorizedClient(authorizedClient, principal);
} catch (DuplicateKeyException e) {
}
catch (DuplicateKeyException ex) {
updateAuthorizedClient(authorizedClient, principal);
}
}
}
private void updateAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
List<SqlParameterValue> parameters = this.authorizedClientParametersMapper.apply(
new OAuth2AuthorizedClientHolder(authorizedClient, principal));
List<SqlParameterValue> parameters = this.authorizedClientParametersMapper
.apply(new OAuth2AuthorizedClientHolder(authorizedClient, principal));
SqlParameterValue clientRegistrationIdParameter = parameters.remove(0);
SqlParameterValue principalNameParameter = parameters.remove(0);
parameters.add(clientRegistrationIdParameter);
parameters.add(principalNameParameter);
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(UPDATE_AUTHORIZED_CLIENT_SQL, pss);
}
private void insertAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
List<SqlParameterValue> parameters = this.authorizedClientParametersMapper.apply(
new OAuth2AuthorizedClientHolder(authorizedClient, principal));
List<SqlParameterValue> parameters = this.authorizedClientParametersMapper
.apply(new OAuth2AuthorizedClientHolder(authorizedClient, principal));
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray());
this.jdbcOperations.update(SAVE_AUTHORIZED_CLIENT_SQL, pss);
}
@@ -167,21 +178,19 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
public void removeAuthorizedClient(String clientRegistrationId, String principalName) {
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
SqlParameterValue[] parameters = new SqlParameterValue[] {
new SqlParameterValue(Types.VARCHAR, clientRegistrationId),
new SqlParameterValue(Types.VARCHAR, principalName)
};
new SqlParameterValue(Types.VARCHAR, principalName) };
PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters);
this.jdbcOperations.update(REMOVE_AUTHORIZED_CLIENT_SQL, pss);
}
/**
* Sets the {@link RowMapper} used for mapping the current row in {@code java.sql.ResultSet} to {@link OAuth2AuthorizedClient}.
* The default is {@link OAuth2AuthorizedClientRowMapper}.
*
* @param authorizedClientRowMapper the {@link RowMapper} used for mapping the current row in {@code java.sql.ResultSet} to {@link OAuth2AuthorizedClient}
* Sets the {@link RowMapper} used for mapping the current row in
* {@code java.sql.ResultSet} to {@link OAuth2AuthorizedClient}. The default is
* {@link OAuth2AuthorizedClientRowMapper}.
* @param authorizedClientRowMapper the {@link RowMapper} used for mapping the current
* row in {@code java.sql.ResultSet} to {@link OAuth2AuthorizedClient}
*/
public final void setAuthorizedClientRowMapper(RowMapper<OAuth2AuthorizedClient> authorizedClientRowMapper) {
Assert.notNull(authorizedClientRowMapper, "authorizedClientRowMapper cannot be null");
@@ -189,21 +198,24 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
}
/**
* Sets the {@code Function} used for mapping {@link OAuth2AuthorizedClientHolder} to a {@code List} of {@link SqlParameterValue}.
* The default is {@link OAuth2AuthorizedClientParametersMapper}.
*
* @param authorizedClientParametersMapper the {@code Function} used for mapping {@link OAuth2AuthorizedClientHolder} to a {@code List} of {@link SqlParameterValue}
* Sets the {@code Function} used for mapping {@link OAuth2AuthorizedClientHolder} to
* a {@code List} of {@link SqlParameterValue}. The default is
* {@link OAuth2AuthorizedClientParametersMapper}.
* @param authorizedClientParametersMapper the {@code Function} used for mapping
* {@link OAuth2AuthorizedClientHolder} to a {@code List} of {@link SqlParameterValue}
*/
public final void setAuthorizedClientParametersMapper(Function<OAuth2AuthorizedClientHolder, List<SqlParameterValue>> authorizedClientParametersMapper) {
public final void setAuthorizedClientParametersMapper(
Function<OAuth2AuthorizedClientHolder, List<SqlParameterValue>> authorizedClientParametersMapper) {
Assert.notNull(authorizedClientParametersMapper, "authorizedClientParametersMapper cannot be null");
this.authorizedClientParametersMapper = authorizedClientParametersMapper;
}
/**
* The default {@link RowMapper} that maps the current row
* in {@code java.sql.ResultSet} to {@link OAuth2AuthorizedClient}.
* The default {@link RowMapper} that maps the current row in
* {@code java.sql.ResultSet} to {@link OAuth2AuthorizedClient}.
*/
public static class OAuth2AuthorizedClientRowMapper implements RowMapper<OAuth2AuthorizedClient> {
protected final ClientRegistrationRepository clientRegistrationRepository;
public OAuth2AuthorizedClientRowMapper(ClientRegistrationRepository clientRegistrationRepository) {
@@ -214,17 +226,15 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
@Override
public OAuth2AuthorizedClient mapRow(ResultSet rs, int rowNum) throws SQLException {
String clientRegistrationId = rs.getString("client_registration_id");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(
clientRegistrationId);
ClientRegistration clientRegistration = this.clientRegistrationRepository
.findByRegistrationId(clientRegistrationId);
if (clientRegistration == null) {
throw new DataRetrievalFailureException("The ClientRegistration with id '" +
clientRegistrationId + "' exists in the data source, " +
"however, it was not found in the ClientRegistrationRepository.");
throw new DataRetrievalFailureException(
"The ClientRegistration with id '" + clientRegistrationId + "' exists in the data source, "
+ "however, it was not found in the ClientRegistrationRepository.");
}
OAuth2AccessToken.TokenType tokenType = null;
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(
rs.getString("access_token_type"))) {
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(rs.getString("access_token_type"))) {
tokenType = OAuth2AccessToken.TokenType.BEARER;
}
String tokenValue = new String(rs.getBytes("access_token_value"), StandardCharsets.UTF_8);
@@ -235,9 +245,7 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
if (accessTokenScopes != null) {
scopes = StringUtils.commaDelimitedListToSet(accessTokenScopes);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(
tokenType, tokenValue, issuedAt, expiresAt, scopes);
OAuth2AccessToken accessToken = new OAuth2AccessToken(tokenType, tokenValue, issuedAt, expiresAt, scopes);
OAuth2RefreshToken refreshToken = null;
byte[] refreshTokenValue = rs.getBytes("refresh_token_value");
if (refreshTokenValue != null) {
@@ -249,19 +257,18 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
}
refreshToken = new OAuth2RefreshToken(tokenValue, issuedAt);
}
String principalName = rs.getString("principal_name");
return new OAuth2AuthorizedClient(
clientRegistration, principalName, accessToken, refreshToken);
return new OAuth2AuthorizedClient(clientRegistration, principalName, accessToken, refreshToken);
}
}
/**
* The default {@code Function} that maps {@link OAuth2AuthorizedClientHolder}
* to a {@code List} of {@link SqlParameterValue}.
* The default {@code Function} that maps {@link OAuth2AuthorizedClientHolder} to a
* {@code List} of {@link SqlParameterValue}.
*/
public static class OAuth2AuthorizedClientParametersMapper implements Function<OAuth2AuthorizedClientHolder, List<SqlParameterValue>> {
public static class OAuth2AuthorizedClientParametersMapper
implements Function<OAuth2AuthorizedClientHolder, List<SqlParameterValue>> {
@Override
public List<SqlParameterValue> apply(OAuth2AuthorizedClientHolder authorizedClientHolder) {
@@ -270,26 +277,19 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken();
List<SqlParameterValue> parameters = new ArrayList<>();
parameters.add(new SqlParameterValue(
Types.VARCHAR, clientRegistration.getRegistrationId()));
parameters.add(new SqlParameterValue(
Types.VARCHAR, principal.getName()));
parameters.add(new SqlParameterValue(
Types.VARCHAR, accessToken.getTokenType().getValue()));
parameters.add(new SqlParameterValue(
Types.BLOB, accessToken.getTokenValue().getBytes(StandardCharsets.UTF_8)));
parameters.add(new SqlParameterValue(
Types.TIMESTAMP, Timestamp.from(accessToken.getIssuedAt())));
parameters.add(new SqlParameterValue(
Types.TIMESTAMP, Timestamp.from(accessToken.getExpiresAt())));
parameters.add(new SqlParameterValue(Types.VARCHAR, clientRegistration.getRegistrationId()));
parameters.add(new SqlParameterValue(Types.VARCHAR, principal.getName()));
parameters.add(new SqlParameterValue(Types.VARCHAR, accessToken.getTokenType().getValue()));
parameters.add(
new SqlParameterValue(Types.BLOB, accessToken.getTokenValue().getBytes(StandardCharsets.UTF_8)));
parameters.add(new SqlParameterValue(Types.TIMESTAMP, Timestamp.from(accessToken.getIssuedAt())));
parameters.add(new SqlParameterValue(Types.TIMESTAMP, Timestamp.from(accessToken.getExpiresAt())));
String accessTokenScopes = null;
if (!CollectionUtils.isEmpty(accessToken.getScopes())) {
accessTokenScopes = StringUtils.collectionToDelimitedString(accessToken.getScopes(), ",");
}
parameters.add(new SqlParameterValue(
Types.VARCHAR, accessTokenScopes));
parameters.add(new SqlParameterValue(Types.VARCHAR, accessTokenScopes));
byte[] refreshTokenValue = null;
Timestamp refreshTokenIssuedAt = null;
if (refreshToken != null) {
@@ -298,25 +298,26 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
refreshTokenIssuedAt = Timestamp.from(refreshToken.getIssuedAt());
}
}
parameters.add(new SqlParameterValue(
Types.BLOB, refreshTokenValue));
parameters.add(new SqlParameterValue(
Types.TIMESTAMP, refreshTokenIssuedAt));
parameters.add(new SqlParameterValue(Types.BLOB, refreshTokenValue));
parameters.add(new SqlParameterValue(Types.TIMESTAMP, refreshTokenIssuedAt));
return parameters;
}
}
/**
* A holder for an {@link OAuth2AuthorizedClient} and End-User {@link Authentication} (Resource Owner).
* A holder for an {@link OAuth2AuthorizedClient} and End-User {@link Authentication}
* (Resource Owner).
*/
public static final class OAuth2AuthorizedClientHolder {
private final OAuth2AuthorizedClient authorizedClient;
private final Authentication principal;
/**
* Constructs an {@code OAuth2AuthorizedClientHolder} using the provided parameters.
*
* Constructs an {@code OAuth2AuthorizedClientHolder} using the provided
* parameters.
* @param authorizedClient the authorized client
* @param principal the End-User {@link Authentication} (Resource Owner)
*/
@@ -329,7 +330,6 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
/**
* Returns the {@link OAuth2AuthorizedClient}.
*
* @return the {@link OAuth2AuthorizedClient}
*/
public OAuth2AuthorizedClient getAuthorizedClient() {
@@ -338,11 +338,12 @@ public class JdbcOAuth2AuthorizedClientService implements OAuth2AuthorizedClient
/**
* Returns the End-User {@link Authentication} (Resource Owner).
*
* @return the End-User {@link Authentication} (Resource Owner)
*/
public Authentication getPrincipal() {
return this.principal;
}
}
}

View File

@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.HashMap;
@@ -27,34 +22,50 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* A context that holds authorization-specific state and is used by an {@link OAuth2AuthorizedClientProvider}
* when attempting to authorize (or re-authorize) an OAuth 2.0 Client.
* A context that holds authorization-specific state and is used by an
* {@link OAuth2AuthorizedClientProvider} when attempting to authorize (or re-authorize)
* an OAuth 2.0 Client.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AuthorizedClientProvider
*/
public final class OAuth2AuthorizationContext {
/**
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the "request scope(s)".
* The value of the attribute is a {@code String[]} of scope(s) to be requested by the {@link #getClientRegistration() client}.
*/
public static final String REQUEST_SCOPE_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".REQUEST_SCOPE");
/**
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the resource owner's username.
* The name of the {@link #getAttribute(String) attribute} in the context associated
* to the value for the "request scope(s)". The value of the attribute is a
* {@code String[]} of scope(s) to be requested by the {@link #getClientRegistration()
* client}.
*/
public static final String REQUEST_SCOPE_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName()
.concat(".REQUEST_SCOPE");
/**
* The name of the {@link #getAttribute(String) attribute} in the context associated
* to the value for the resource owner's username.
*/
public static final String USERNAME_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".USERNAME");
/**
* The name of the {@link #getAttribute(String) attribute} in the context associated to the value for the resource owner's password.
* The name of the {@link #getAttribute(String) attribute} in the context associated
* to the value for the resource owner's password.
*/
public static final String PASSWORD_ATTRIBUTE_NAME = OAuth2AuthorizationContext.class.getName().concat(".PASSWORD");
private ClientRegistration clientRegistration;
private OAuth2AuthorizedClient authorizedClient;
private Authentication principal;
private Map<String, Object> attributes;
private OAuth2AuthorizationContext() {
@@ -62,7 +73,6 @@ public final class OAuth2AuthorizationContext {
/**
* Returns the {@link ClientRegistration client registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -70,10 +80,11 @@ public final class OAuth2AuthorizationContext {
}
/**
* Returns the {@link OAuth2AuthorizedClient authorized client} or {@code null}
* if the {@link #withClientRegistration(ClientRegistration) client registration} was supplied.
*
* @return the {@link OAuth2AuthorizedClient} or {@code null} if the client registration was supplied
* Returns the {@link OAuth2AuthorizedClient authorized client} or {@code null} if the
* {@link #withClientRegistration(ClientRegistration) client registration} was
* supplied.
* @return the {@link OAuth2AuthorizedClient} or {@code null} if the client
* registration was supplied
*/
@Nullable
public OAuth2AuthorizedClient getAuthorizedClient() {
@@ -82,7 +93,6 @@ public final class OAuth2AuthorizationContext {
/**
* Returns the {@code Principal} (to be) associated to the authorized client.
*
* @return the {@code Principal} (to be) associated to the authorized client
*/
public Authentication getPrincipal() {
@@ -91,7 +101,6 @@ public final class OAuth2AuthorizationContext {
/**
* Returns the attributes associated to the context.
*
* @return a {@code Map} of the attributes associated to the context
*/
public Map<String, Object> getAttributes() {
@@ -99,8 +108,8 @@ public final class OAuth2AuthorizationContext {
}
/**
* Returns the value of an attribute associated to the context or {@code null} if not available.
*
* Returns the value of an attribute associated to the context or {@code null} if not
* available.
* @param name the name of the attribute
* @param <T> the type of the attribute
* @return the value of the attribute associated to the context
@@ -113,7 +122,6 @@ public final class OAuth2AuthorizationContext {
/**
* Returns a new {@link Builder} initialized with the {@link ClientRegistration}.
*
* @param clientRegistration the {@link ClientRegistration client registration}
* @return the {@link Builder}
*/
@@ -123,7 +131,6 @@ public final class OAuth2AuthorizationContext {
/**
* Returns a new {@link Builder} initialized with the {@link OAuth2AuthorizedClient}.
*
* @param authorizedClient the {@link OAuth2AuthorizedClient authorized client}
* @return the {@link Builder}
*/
@@ -134,10 +141,14 @@ public final class OAuth2AuthorizationContext {
/**
* A builder for {@link OAuth2AuthorizationContext}.
*/
public static class Builder {
public static final class Builder {
private ClientRegistration clientRegistration;
private OAuth2AuthorizedClient authorizedClient;
private Authentication principal;
private Map<String, Object> attributes;
private Builder(ClientRegistration clientRegistration) {
@@ -152,8 +163,8 @@ public final class OAuth2AuthorizationContext {
/**
* Sets the {@code Principal} (to be) associated to the authorized client.
*
* @param principal the {@code Principal} (to be) associated to the authorized client
* @param principal the {@code Principal} (to be) associated to the authorized
* client
* @return the {@link Builder}
*/
public Builder principal(Authentication principal) {
@@ -163,8 +174,8 @@ public final class OAuth2AuthorizationContext {
/**
* Provides a {@link Consumer} access to the attributes associated to the context.
*
* @param attributesConsumer a {@link Consumer} of the attributes associated to the context
* @param attributesConsumer a {@link Consumer} of the attributes associated to
* the context
* @return the {@link OAuth2AuthorizeRequest.Builder}
*/
public Builder attributes(Consumer<Map<String, Object>> attributesConsumer) {
@@ -177,7 +188,6 @@ public final class OAuth2AuthorizationContext {
/**
* Sets an attribute associated to the context.
*
* @param name the name of the attribute
* @param value the value of the attribute
* @return the {@link Builder}
@@ -192,7 +202,6 @@ public final class OAuth2AuthorizationContext {
/**
* Builds a new {@link OAuth2AuthorizationContext}.
*
* @return a {@link OAuth2AuthorizationContext}
*/
public OAuth2AuthorizationContext build() {
@@ -201,14 +210,16 @@ public final class OAuth2AuthorizationContext {
if (this.authorizedClient != null) {
context.clientRegistration = this.authorizedClient.getClientRegistration();
context.authorizedClient = this.authorizedClient;
} else {
}
else {
context.clientRegistration = this.clientRegistration;
}
context.principal = this.principal;
context.attributes = Collections.unmodifiableMap(
CollectionUtils.isEmpty(this.attributes) ?
Collections.emptyMap() : new LinkedHashMap<>(this.attributes));
context.attributes = Collections.unmodifiableMap(CollectionUtils.isEmpty(this.attributes)
? Collections.emptyMap() : new LinkedHashMap<>(this.attributes));
return context;
}
}
}

View File

@@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Map;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import java.util.Map;
/**
* Handles when an OAuth 2.0 Client fails to authorize (or re-authorize)
* via the Authorization Server or Resource Server.
* Handles when an OAuth 2.0 Client fails to authorize (or re-authorize) via the
* Authorization Server or Resource Server.
*
* @author Joe Grandja
* @since 5.3
@@ -33,16 +34,17 @@ import java.util.Map;
public interface OAuth2AuthorizationFailureHandler {
/**
* Called when an OAuth 2.0 Client fails to authorize (or re-authorize)
* via the Authorization Server or Resource Server.
*
* Called when an OAuth 2.0 Client fails to authorize (or re-authorize) via the
* Authorization Server or Resource Server.
* @param authorizationException the exception that contains details about what failed
* @param principal the {@code Principal} associated with the attempted authorization
* @param attributes an immutable {@code Map} of (optional) attributes present under certain conditions.
* For example, this might contain a {@code javax.servlet.http.HttpServletRequest}
* and {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
* within the context of a {@code javax.servlet.ServletContext}.
* @param attributes an immutable {@code Map} of (optional) attributes present under
* certain conditions. For example, this might contain a
* {@code javax.servlet.http.HttpServletRequest} and
* {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
* within the context of a {@code javax.servlet.ServletContext}.
*/
void onAuthorizationFailure(OAuth2AuthorizationException authorizationException,
Authentication principal, Map<String, Object> attributes);
void onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal,
Map<String, Object> attributes);
}

View File

@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.core.Authentication;
package org.springframework.security.oauth2.client;
import java.util.Map;
import org.springframework.security.core.Authentication;
/**
* Handles when an OAuth 2.0 Client has been successfully
* authorized (or re-authorized) via the Authorization Server.
* Handles when an OAuth 2.0 Client has been successfully authorized (or re-authorized)
* via the Authorization Server.
*
* @author Joe Grandja
* @since 5.3
@@ -32,16 +33,18 @@ import java.util.Map;
public interface OAuth2AuthorizationSuccessHandler {
/**
* Called when an OAuth 2.0 Client has been successfully
* authorized (or re-authorized) via the Authorization Server.
*
* @param authorizedClient the client that was successfully authorized (or re-authorized)
* Called when an OAuth 2.0 Client has been successfully authorized (or re-authorized)
* via the Authorization Server.
* @param authorizedClient the client that was successfully authorized (or
* re-authorized)
* @param principal the {@code Principal} associated with the authorized client
* @param attributes an immutable {@code Map} of (optional) attributes present under certain conditions.
* For example, this might contain a {@code javax.servlet.http.HttpServletRequest}
* and {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
* within the context of a {@code javax.servlet.ServletContext}.
* @param attributes an immutable {@code Map} of (optional) attributes present under
* certain conditions. For example, this might contain a
* {@code javax.servlet.http.HttpServletRequest} and
* {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
* within the context of a {@code javax.servlet.ServletContext}.
*/
void onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient,
Authentication principal, Map<String, Object> attributes);
void onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal,
Map<String, Object> attributes);
}

View File

@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
@@ -22,25 +29,24 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
/**
* Represents a request the {@link OAuth2AuthorizedClientManager} uses to
* {@link OAuth2AuthorizedClientManager#authorize(OAuth2AuthorizeRequest) authorize} (or re-authorize)
* the {@link ClientRegistration client} identified by the provided {@link #getClientRegistrationId() clientRegistrationId}.
* {@link OAuth2AuthorizedClientManager#authorize(OAuth2AuthorizeRequest) authorize} (or
* re-authorize) the {@link ClientRegistration client} identified by the provided
* {@link #getClientRegistrationId() clientRegistrationId}.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AuthorizedClientManager
*/
public final class OAuth2AuthorizeRequest {
private String clientRegistrationId;
private OAuth2AuthorizedClient authorizedClient;
private Authentication principal;
private Map<String, Object> attributes;
private OAuth2AuthorizeRequest() {
@@ -48,7 +54,6 @@ public final class OAuth2AuthorizeRequest {
/**
* Returns the identifier for the {@link ClientRegistration client registration}.
*
* @return the identifier for the client registration
*/
public String getClientRegistrationId() {
@@ -56,8 +61,8 @@ public final class OAuth2AuthorizeRequest {
}
/**
* Returns the {@link OAuth2AuthorizedClient authorized client} or {@code null} if it was not provided.
*
* Returns the {@link OAuth2AuthorizedClient authorized client} or {@code null} if it
* was not provided.
* @return the {@link OAuth2AuthorizedClient} or {@code null} if it was not provided
*/
@Nullable
@@ -67,7 +72,6 @@ public final class OAuth2AuthorizeRequest {
/**
* Returns the {@code Principal} (to be) associated to the authorized client.
*
* @return the {@code Principal} (to be) associated to the authorized client
*/
public Authentication getPrincipal() {
@@ -76,7 +80,6 @@ public final class OAuth2AuthorizeRequest {
/**
* Returns the attributes associated to the request.
*
* @return a {@code Map} of the attributes associated to the request
*/
public Map<String, Object> getAttributes() {
@@ -84,8 +87,8 @@ public final class OAuth2AuthorizeRequest {
}
/**
* Returns the value of an attribute associated to the request or {@code null} if not available.
*
* Returns the value of an attribute associated to the request or {@code null} if not
* available.
* @param name the name of the attribute
* @param <T> the type of the attribute
* @return the value of the attribute associated to the request
@@ -97,9 +100,10 @@ public final class OAuth2AuthorizeRequest {
}
/**
* Returns a new {@link Builder} initialized with the identifier for the {@link ClientRegistration client registration}.
*
* @param clientRegistrationId the identifier for the {@link ClientRegistration client registration}
* Returns a new {@link Builder} initialized with the identifier for the
* {@link ClientRegistration client registration}.
* @param clientRegistrationId the identifier for the {@link ClientRegistration client
* registration}
* @return the {@link Builder}
*/
public static Builder withClientRegistrationId(String clientRegistrationId) {
@@ -107,8 +111,8 @@ public final class OAuth2AuthorizeRequest {
}
/**
* Returns a new {@link Builder} initialized with the {@link OAuth2AuthorizedClient authorized client}.
*
* Returns a new {@link Builder} initialized with the {@link OAuth2AuthorizedClient
* authorized client}.
* @param authorizedClient the {@link OAuth2AuthorizedClient authorized client}
* @return the {@link Builder}
*/
@@ -119,10 +123,14 @@ public final class OAuth2AuthorizeRequest {
/**
* A builder for {@link OAuth2AuthorizeRequest}.
*/
public static class Builder {
public static final class Builder {
private String clientRegistrationId;
private OAuth2AuthorizedClient authorizedClient;
private Authentication principal;
private Map<String, Object> attributes;
private Builder(String clientRegistrationId) {
@@ -136,11 +144,12 @@ public final class OAuth2AuthorizeRequest {
}
/**
* Sets the name of the {@code Principal} (to be) associated to the authorized client.
*
* @since 5.3
* @param principalName the name of the {@code Principal} (to be) associated to the authorized client
* Sets the name of the {@code Principal} (to be) associated to the authorized
* client.
* @param principalName the name of the {@code Principal} (to be) associated to
* the authorized client
* @return the {@link Builder}
* @since 5.3
*/
public Builder principal(String principalName) {
return principal(createAuthentication(principalName));
@@ -148,8 +157,8 @@ public final class OAuth2AuthorizeRequest {
private static Authentication createAuthentication(final String principalName) {
Assert.hasText(principalName, "principalName cannot be empty");
return new AbstractAuthenticationToken(null) {
@Override
public Object getCredentials() {
return "";
@@ -159,13 +168,14 @@ public final class OAuth2AuthorizeRequest {
public Object getPrincipal() {
return principalName;
}
};
}
/**
* Sets the {@code Principal} (to be) associated to the authorized client.
*
* @param principal the {@code Principal} (to be) associated to the authorized client
* @param principal the {@code Principal} (to be) associated to the authorized
* client
* @return the {@link Builder}
*/
public Builder principal(Authentication principal) {
@@ -175,8 +185,8 @@ public final class OAuth2AuthorizeRequest {
/**
* Provides a {@link Consumer} access to the attributes associated to the request.
*
* @param attributesConsumer a {@link Consumer} of the attributes associated to the request
* @param attributesConsumer a {@link Consumer} of the attributes associated to
* the request
* @return the {@link Builder}
*/
public Builder attributes(Consumer<Map<String, Object>> attributesConsumer) {
@@ -189,7 +199,6 @@ public final class OAuth2AuthorizeRequest {
/**
* Sets an attribute associated to the request.
*
* @param name the name of the attribute
* @param value the value of the attribute
* @return the {@link Builder}
@@ -204,23 +213,25 @@ public final class OAuth2AuthorizeRequest {
/**
* Builds a new {@link OAuth2AuthorizeRequest}.
*
* @return a {@link OAuth2AuthorizeRequest}
*/
public OAuth2AuthorizeRequest build() {
Assert.notNull(this.principal, "principal cannot be null");
OAuth2AuthorizeRequest authorizeRequest = new OAuth2AuthorizeRequest();
if (this.authorizedClient != null) {
authorizeRequest.clientRegistrationId = this.authorizedClient.getClientRegistration().getRegistrationId();
authorizeRequest.clientRegistrationId = this.authorizedClient.getClientRegistration()
.getRegistrationId();
authorizeRequest.authorizedClient = this.authorizedClient;
} else {
}
else {
authorizeRequest.clientRegistrationId = this.clientRegistrationId;
}
authorizeRequest.principal = this.principal;
authorizeRequest.attributes = Collections.unmodifiableMap(
CollectionUtils.isEmpty(this.attributes) ?
Collections.emptyMap() : new LinkedHashMap<>(this.attributes));
authorizeRequest.attributes = Collections.unmodifiableMap(CollectionUtils.isEmpty(this.attributes)
? Collections.emptyMap() : new LinkedHashMap<>(this.attributes));
return authorizeRequest;
}
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.io.Serializable;
import org.springframework.lang.Nullable;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -22,17 +25,15 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.Assert;
import java.io.Serializable;
/**
* A representation of an OAuth 2.0 &quot;Authorized Client&quot;.
* <p>
* A client is considered &quot;authorized&quot; when the End-User (Resource Owner)
* has granted authorization to the client to access it's protected resources.
* A client is considered &quot;authorized&quot; when the End-User (Resource Owner) has
* granted authorization to the client to access it's protected resources.
* <p>
* This class associates the {@link #getClientRegistration() Client}
* to the {@link #getAccessToken() Access Token}
* granted/authorized by the {@link #getPrincipalName() Resource Owner}.
* This class associates the {@link #getClientRegistration() Client} to the
* {@link #getAccessToken() Access Token} granted/authorized by the
* {@link #getPrincipalName() Resource Owner}.
*
* @author Joe Grandja
* @since 5.0
@@ -41,33 +42,37 @@ import java.io.Serializable;
* @see OAuth2RefreshToken
*/
public class OAuth2AuthorizedClient implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final ClientRegistration clientRegistration;
private final String principalName;
private final OAuth2AccessToken accessToken;
private final OAuth2RefreshToken refreshToken;
/**
* Constructs an {@code OAuth2AuthorizedClient} using the provided parameters.
*
* @param clientRegistration the authorized client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
* @param accessToken the access token credential granted
*/
public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName, OAuth2AccessToken accessToken) {
public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName,
OAuth2AccessToken accessToken) {
this(clientRegistration, principalName, accessToken, null);
}
/**
* Constructs an {@code OAuth2AuthorizedClient} using the provided parameters.
*
* @param clientRegistration the authorized client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
* @param accessToken the access token credential granted
* @param refreshToken the refresh token credential granted
*/
public OAuth2AuthorizedClient(ClientRegistration clientRegistration, String principalName,
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.hasText(principalName, "principalName cannot be empty");
Assert.notNull(accessToken, "accessToken cannot be null");
@@ -79,7 +84,6 @@ public class OAuth2AuthorizedClient implements Serializable {
/**
* Returns the authorized client's {@link ClientRegistration registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -88,7 +92,6 @@ public class OAuth2AuthorizedClient implements Serializable {
/**
* Returns the End-User's {@code Principal} name.
*
* @return the End-User's {@code Principal} name
*/
public String getPrincipalName() {
@@ -97,7 +100,6 @@ public class OAuth2AuthorizedClient implements Serializable {
/**
* Returns the {@link OAuth2AccessToken access token} credential granted.
*
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
@@ -106,11 +108,11 @@ public class OAuth2AuthorizedClient implements Serializable {
/**
* Returns the {@link OAuth2RefreshToken refresh token} credential granted.
*
* @since 5.1
* @return the {@link OAuth2RefreshToken}
* @since 5.1
*/
public @Nullable OAuth2RefreshToken getRefreshToken() {
return this.refreshToken;
}
}

View File

@@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
package org.springframework.security.oauth2.client;
import java.io.Serializable;
import java.util.Objects;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
/**
* The identifier for {@link OAuth2AuthorizedClient}.
*
@@ -30,13 +31,15 @@ import java.util.Objects;
* @see OAuth2AuthorizedClientService
*/
public final class OAuth2AuthorizedClientId implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String clientRegistrationId;
private final String principalName;
/**
* Constructs an {@code OAuth2AuthorizedClientId} using the provided parameters.
*
* @param clientRegistrationId the identifier for the client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
*/
@@ -64,4 +67,5 @@ public final class OAuth2AuthorizedClientId implements Serializable {
public int hashCode() {
return Objects.hash(this.clientRegistrationId, this.principalName);
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.lang.Nullable;
@@ -20,16 +21,16 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
/**
* Implementations of this interface are responsible for the overall management
* of {@link OAuth2AuthorizedClient Authorized Client(s)}.
* Implementations of this interface are responsible for the overall management of
* {@link OAuth2AuthorizedClient Authorized Client(s)}.
*
* <p>
* The primary responsibilities include:
* <ol>
* <li>Authorizing (or re-authorizing) an OAuth 2.0 Client
* by leveraging an {@link OAuth2AuthorizedClientProvider}(s).</li>
* <li>Delegating the persistence of an {@link OAuth2AuthorizedClient},
* typically using an {@link OAuth2AuthorizedClientService} OR {@link OAuth2AuthorizedClientRepository}.</li>
* <li>Authorizing (or re-authorizing) an OAuth 2.0 Client by leveraging an
* {@link OAuth2AuthorizedClientProvider}(s).</li>
* <li>Delegating the persistence of an {@link OAuth2AuthorizedClient}, typically using an
* {@link OAuth2AuthorizedClientService} OR {@link OAuth2AuthorizedClientRepository}.</li>
* </ol>
*
* @author Joe Grandja
@@ -43,20 +44,23 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepo
public interface OAuth2AuthorizedClientManager {
/**
* Attempt to authorize or re-authorize (if required) the {@link ClientRegistration client}
* identified by the provided {@link OAuth2AuthorizeRequest#getClientRegistrationId() clientRegistrationId}.
* Implementations must return {@code null} if authorization is not supported for the specified client,
* e.g. the associated {@link OAuth2AuthorizedClientProvider}(s) does not support
* the {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type configured for the client.
* Attempt to authorize or re-authorize (if required) the {@link ClientRegistration
* client} identified by the provided
* {@link OAuth2AuthorizeRequest#getClientRegistrationId() clientRegistrationId}.
* Implementations must return {@code null} if authorization is not supported for the
* specified client, e.g. the associated {@link OAuth2AuthorizedClientProvider}(s)
* does not support the {@link ClientRegistration#getAuthorizationGrantType()
* authorization grant} type configured for the client.
*
* <p>
* In the case of re-authorization, implementations must return the provided {@link OAuth2AuthorizeRequest#getAuthorizedClient() authorized client}
* if re-authorization is not supported for the client OR is not required,
* e.g. a {@link OAuth2AuthorizedClient#getRefreshToken() refresh token} is not available OR
* In the case of re-authorization, implementations must return the provided
* {@link OAuth2AuthorizeRequest#getAuthorizedClient() authorized client} if
* re-authorization is not supported for the client OR is not required, e.g. a
* {@link OAuth2AuthorizedClient#getRefreshToken() refresh token} is not available OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* @param authorizeRequest the authorize request
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported for the specified client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not
* supported for the specified client
*/
@Nullable
OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest);

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.lang.Nullable;
@@ -20,25 +21,30 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.core.AuthorizationGrantType;
/**
* A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client.
* Implementations will typically implement a specific {@link AuthorizationGrantType authorization grant} type.
* A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. Implementations
* will typically implement a specific {@link AuthorizationGrantType authorization grant}
* type.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AuthorizedClient
* @see OAuth2AuthorizationContext
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
* 1.3 Authorization Grant</a>
*/
@FunctionalInterface
public interface OAuth2AuthorizedClientProvider {
/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context.
* Implementations must return {@code null} if authorization is not supported for the specified client,
* e.g. the provider doesn't support the {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type configured for the client.
*
* Attempt to authorize (or re-authorize) the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* context. Implementations must return {@code null} if authorization is not supported
* for the specified client, e.g. the provider doesn't support the
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type
* configured for the client.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not supported for the specified client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization is not
* supported for the specified client
*/
@Nullable
OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context);

View File

@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.util.Assert;
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
@@ -30,13 +25,19 @@ import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.util.Assert;
/**
* A builder that builds a {@link DelegatingOAuth2AuthorizedClientProvider} composed of
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
* The supported authorization grants are {@link #authorizationCode() authorization_code},
* {@link #refreshToken() refresh_token}, {@link #clientCredentials() client_credentials}
* and {@link #password() password}.
* In addition to the standard authorization grants, an implementation of an extension grant
* one or more {@link OAuth2AuthorizedClientProvider}(s) that implement specific
* authorization grants. The supported authorization grants are
* {@link #authorizationCode() authorization_code}, {@link #refreshToken() refresh_token},
* {@link #clientCredentials() client_credentials} and {@link #password() password}. In
* addition to the standard authorization grants, an implementation of an extension grant
* may be supplied via {@link #provider(OAuth2AuthorizedClientProvider)}.
*
* @author Joe Grandja
@@ -49,14 +50,15 @@ import java.util.function.Consumer;
* @see DelegatingOAuth2AuthorizedClientProvider
*/
public final class OAuth2AuthorizedClientProviderBuilder {
private final Map<Class<?>, Builder> builders = new LinkedHashMap<>();
private OAuth2AuthorizedClientProviderBuilder() {
}
/**
* Returns a new {@link OAuth2AuthorizedClientProviderBuilder} for configuring the supported authorization grant(s).
*
* Returns a new {@link OAuth2AuthorizedClientProviderBuilder} for configuring the
* supported authorization grant(s).
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public static OAuth2AuthorizedClientProviderBuilder builder() {
@@ -64,273 +66,146 @@ public final class OAuth2AuthorizedClientProviderBuilder {
}
/**
* Configures an {@link OAuth2AuthorizedClientProvider} to be composed with the {@link DelegatingOAuth2AuthorizedClientProvider}.
* This may be used for implementations of extension authorization grants.
*
* Configures an {@link OAuth2AuthorizedClientProvider} to be composed with the
* {@link DelegatingOAuth2AuthorizedClientProvider}. This may be used for
* implementations of extension authorization grants.
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder provider(OAuth2AuthorizedClientProvider provider) {
Assert.notNull(provider, "provider cannot be null");
this.builders.computeIfAbsent(provider.getClass(), k -> () -> provider);
this.builders.computeIfAbsent(provider.getClass(), (k) -> () -> provider);
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code authorization_code} grant.
*
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder authorizationCode() {
this.builders.computeIfAbsent(AuthorizationCodeOAuth2AuthorizedClientProvider.class, k -> new AuthorizationCodeGrantBuilder());
this.builders.computeIfAbsent(AuthorizationCodeOAuth2AuthorizedClientProvider.class,
(k) -> new AuthorizationCodeGrantBuilder());
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* A builder for the {@code authorization_code} grant.
*/
public class AuthorizationCodeGrantBuilder implements Builder {
private AuthorizationCodeGrantBuilder() {
}
/**
* Builds an instance of {@link AuthorizationCodeOAuth2AuthorizedClientProvider}.
*
* @return the {@link AuthorizationCodeOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
return new AuthorizationCodeOAuth2AuthorizedClientProvider();
}
}
/**
* Configures support for the {@code refresh_token} grant.
*
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder refreshToken() {
this.builders.computeIfAbsent(RefreshTokenOAuth2AuthorizedClientProvider.class, k -> new RefreshTokenGrantBuilder());
this.builders.computeIfAbsent(RefreshTokenOAuth2AuthorizedClientProvider.class,
(k) -> new RefreshTokenGrantBuilder());
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code refresh_token} grant.
*
* @param builderConsumer a {@code Consumer} of {@link RefreshTokenGrantBuilder} used for further configuration
* @param builderConsumer a {@code Consumer} of {@link RefreshTokenGrantBuilder} used
* for further configuration
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder refreshToken(Consumer<RefreshTokenGrantBuilder> builderConsumer) {
RefreshTokenGrantBuilder builder = (RefreshTokenGrantBuilder) this.builders.computeIfAbsent(
RefreshTokenOAuth2AuthorizedClientProvider.class, k -> new RefreshTokenGrantBuilder());
RefreshTokenOAuth2AuthorizedClientProvider.class, (k) -> new RefreshTokenGrantBuilder());
builderConsumer.accept(builder);
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* A builder for the {@code refresh_token} grant.
*/
public class RefreshTokenGrantBuilder implements Builder {
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private RefreshTokenGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* @param clockSkew the maximum acceptable clock skew
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* @param clock the clock
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Builds an instance of {@link RefreshTokenOAuth2AuthorizedClientProvider}.
*
* @return the {@link RefreshTokenOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}
}
/**
* Configures support for the {@code client_credentials} grant.
*
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder clientCredentials() {
this.builders.computeIfAbsent(ClientCredentialsOAuth2AuthorizedClientProvider.class, k -> new ClientCredentialsGrantBuilder());
this.builders.computeIfAbsent(ClientCredentialsOAuth2AuthorizedClientProvider.class,
(k) -> new ClientCredentialsGrantBuilder());
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code client_credentials} grant.
*
* @param builderConsumer a {@code Consumer} of {@link ClientCredentialsGrantBuilder} used for further configuration
* @param builderConsumer a {@code Consumer} of {@link ClientCredentialsGrantBuilder}
* used for further configuration
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder clientCredentials(Consumer<ClientCredentialsGrantBuilder> builderConsumer) {
public OAuth2AuthorizedClientProviderBuilder clientCredentials(
Consumer<ClientCredentialsGrantBuilder> builderConsumer) {
ClientCredentialsGrantBuilder builder = (ClientCredentialsGrantBuilder) this.builders.computeIfAbsent(
ClientCredentialsOAuth2AuthorizedClientProvider.class, k -> new ClientCredentialsGrantBuilder());
ClientCredentialsOAuth2AuthorizedClientProvider.class, (k) -> new ClientCredentialsGrantBuilder());
builderConsumer.accept(builder);
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* A builder for the {@code client_credentials} grant.
*/
public class ClientCredentialsGrantBuilder implements Builder {
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private ClientCredentialsGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* @param clockSkew the maximum acceptable clock skew
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* @param clock the clock
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Builds an instance of {@link ClientCredentialsOAuth2AuthorizedClientProvider}.
*
* @return the {@link ClientCredentialsOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}
}
/**
* Configures support for the {@code password} grant.
*
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder password() {
this.builders.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
this.builders.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, (k) -> new PasswordGrantBuilder());
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code password} grant.
*
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for
* further configuration
* @return the {@link OAuth2AuthorizedClientProviderBuilder}
*/
public OAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
PasswordOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders
.computeIfAbsent(PasswordOAuth2AuthorizedClientProvider.class, (k) -> new PasswordGrantBuilder());
builderConsumer.accept(builder);
return OAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider} composed of
* one or more {@link OAuth2AuthorizedClientProvider}(s).
* @return the {@link DelegatingOAuth2AuthorizedClientProvider}
*/
public OAuth2AuthorizedClientProvider build() {
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
for (Builder builder : this.builders.values()) {
authorizedClientProviders.add(builder.build());
}
return new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
}
interface Builder {
OAuth2AuthorizedClientProvider build();
}
/**
* A builder for the {@code password} grant.
*/
public class PasswordGrantBuilder implements Builder {
public final class PasswordGrantBuilder implements Builder {
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private PasswordGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link PasswordGrantBuilder}
*/
public PasswordGrantBuilder accessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
public PasswordGrantBuilder accessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link PasswordGrantBuilder}
*/
@@ -340,8 +215,8 @@ public final class OAuth2AuthorizedClientProviderBuilder {
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link PasswordGrantBuilder}
*/
@@ -352,7 +227,6 @@ public final class OAuth2AuthorizedClientProviderBuilder {
/**
* Builds an instance of {@link PasswordOAuth2AuthorizedClientProvider}.
*
* @return the {@link PasswordOAuth2AuthorizedClientProvider}
*/
@Override
@@ -369,23 +243,168 @@ public final class OAuth2AuthorizedClientProviderBuilder {
}
return authorizedClientProvider;
}
}
/**
* Builds an instance of {@link DelegatingOAuth2AuthorizedClientProvider}
* composed of one or more {@link OAuth2AuthorizedClientProvider}(s).
*
* @return the {@link DelegatingOAuth2AuthorizedClientProvider}
* A builder for the {@code client_credentials} grant.
*/
public OAuth2AuthorizedClientProvider build() {
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
for (Builder builder : this.builders.values()) {
authorizedClientProviders.add(builder.build());
public final class ClientCredentialsGrantBuilder implements Builder {
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private ClientCredentialsGrantBuilder() {
}
return new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
/**
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder accessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Builds an instance of {@link ClientCredentialsOAuth2AuthorizedClientProvider}.
* @return the {@link ClientCredentialsOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}
}
interface Builder {
OAuth2AuthorizedClientProvider build();
/**
* A builder for the {@code authorization_code} grant.
*/
public final class AuthorizationCodeGrantBuilder implements Builder {
private AuthorizationCodeGrantBuilder() {
}
/**
* Builds an instance of {@link AuthorizationCodeOAuth2AuthorizedClientProvider}.
* @return the {@link AuthorizationCodeOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
return new AuthorizationCodeOAuth2AuthorizedClientProvider();
}
}
/**
* A builder for the {@code refresh_token} grant.
*/
public final class RefreshTokenGrantBuilder implements Builder {
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private RefreshTokenGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder accessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Builds an instance of {@link RefreshTokenOAuth2AuthorizedClientProvider}.
* @return the {@link RefreshTokenOAuth2AuthorizedClientProvider}
*/
@Override
public OAuth2AuthorizedClientProvider build() {
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.core.Authentication;
@@ -20,12 +21,12 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
import org.springframework.security.oauth2.core.OAuth2AccessToken;
/**
* Implementations of this interface are responsible for the management
* of {@link OAuth2AuthorizedClient Authorized Client(s)}, which provide the purpose
* of associating an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential
* Implementations of this interface are responsible for the management of
* {@link OAuth2AuthorizedClient Authorized Client(s)}, which provide the purpose of
* associating an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential
* to a {@link OAuth2AuthorizedClient#getClientRegistration() Client} and Resource Owner,
* who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal}
* that originally granted the authorization.
* who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal} that originally
* granted the authorization.
*
* @author Joe Grandja
* @since 5.0
@@ -37,10 +38,9 @@ import org.springframework.security.oauth2.core.OAuth2AccessToken;
public interface OAuth2AuthorizedClientService {
/**
* Returns the {@link OAuth2AuthorizedClient} associated to the
* provided client registration identifier and End-User's {@code Principal} name
* or {@code null} if not available.
*
* Returns the {@link OAuth2AuthorizedClient} associated to the provided client
* registration identifier and End-User's {@code Principal} name or {@code null} if
* not available.
* @param clientRegistrationId the identifier for the client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
* @param <T> a type of OAuth2AuthorizedClient
@@ -49,18 +49,16 @@ public interface OAuth2AuthorizedClientService {
<T extends OAuth2AuthorizedClient> T loadAuthorizedClient(String clientRegistrationId, String principalName);
/**
* Saves the {@link OAuth2AuthorizedClient} associating it to
* the provided End-User {@link Authentication} (Resource Owner).
*
* Saves the {@link OAuth2AuthorizedClient} associating it to the provided End-User
* {@link Authentication} (Resource Owner).
* @param authorizedClient the authorized client
* @param principal the End-User {@link Authentication} (Resource Owner)
*/
void saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal);
/**
* Removes the {@link OAuth2AuthorizedClient} associated to the
* provided client registration identifier and End-User's {@code Principal} name.
*
* Removes the {@link OAuth2AuthorizedClient} associated to the provided client
* registration identifier and End-User's {@code Principal} name.
* @param clientRegistrationId the identifier for the client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
*/

View File

@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.endpoint.DefaultPasswordTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
@@ -27,13 +32,9 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenRespon
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
/**
* An implementation of an {@link OAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
* An implementation of an {@link OAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#PASSWORD password} grant.
*
* @author Joe Grandja
* @since 5.2
@@ -41,96 +42,105 @@ import java.time.Instant;
* @see DefaultPasswordTokenResponseClient
*/
public final class PasswordOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
new DefaultPasswordTokenResponseClient();
private OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = new DefaultPasswordTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns {@code null} if authorization (or re-authorization) is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#PASSWORD password} OR
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
* are not available in the provided {@code context} OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
* Attempt to authorize (or re-authorize) the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* {@code context}. Returns {@code null} if authorization (or re-authorization) is not
* supported, e.g. the client's {@link ClientRegistration#getAuthorizationGrantType()
* authorization grant type} is not {@link AuthorizationGrantType#PASSWORD password}
* OR the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes are
* not available in the provided {@code context} OR the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* <p>
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes}
* are supported:
* <ol>
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a
* {@code String} value for the resource owner's username</li>
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a
* {@code String} value for the resource owner's password</li>
* </ol>
*
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or re-authorization) is not supported
* @return the {@link OAuth2AuthorizedClient} or {@code null} if authorization (or
* re-authorization) is not supported
*/
@Override
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
ClientRegistration clientRegistration = context.getClientRegistration();
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType())) {
return null;
}
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return null;
}
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized and access token is NOT expired than no need for re-authorization
// If client is already authorized and access token is NOT expired than no
// need for re-authorization
return null;
}
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken()) && authorizedClient.getRefreshToken() != null) {
// If client is already authorized and access token is expired and a refresh token is available,
// than return and allow RefreshTokenOAuth2AuthorizedClientProvider to handle the refresh
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken())
&& authorizedClient.getRefreshToken() != null) {
// If client is already authorized and access token is expired and a refresh
// token is available, than return and allow
// RefreshTokenOAuth2AuthorizedClientProvider to handle the refresh
return null;
}
OAuth2PasswordGrantRequest passwordGrantRequest =
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
OAuth2AccessTokenResponse tokenResponse;
try {
tokenResponse = this.accessTokenResponseClient.getTokenResponse(passwordGrantRequest);
} catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, username,
password);
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, passwordGrantRequest);
return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
}
private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration,
OAuth2PasswordGrantRequest passwordGrantRequest) {
try {
return this.accessTokenResponseClient.getTokenResponse(passwordGrantRequest);
}
catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex);
}
}
private boolean hasTokenExpired(AbstractOAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code password} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code password} grant
*/
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds.
*
* <p>
* An access token is considered expired if {@code OAuth2AccessToken#getExpiresAt() - clockSkew}
* is before the current time {@code clock#instant()}.
*
* An access token is considered expired if
* {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
@@ -140,12 +150,13 @@ public final class PasswordOAuth2AuthorizedClientProvider implements OAuth2Autho
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactivePasswordTokenResponseClient;
@@ -24,15 +31,10 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#PASSWORD password} grant.
*
* @author Joe Grandja
* @since 5.2
@@ -40,67 +42,71 @@ import java.time.Instant;
* @see WebClientReactivePasswordTokenResponseClient
*/
public final class PasswordReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient =
new WebClientReactivePasswordTokenResponseClient();
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = new WebClientReactivePasswordTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns an empty {@code Mono} if authorization (or re-authorization) is not supported,
* e.g. the client's {@link ClientRegistration#getAuthorizationGrantType() authorization grant type}
* is not {@link AuthorizationGrantType#PASSWORD password} OR
* the {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes
* are not available in the provided {@code context} OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
* Attempt to authorize (or re-authorize) the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* {@code context}. Returns an empty {@code Mono} if authorization (or
* re-authorization) is not supported, e.g. the client's
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant type} is
* not {@link AuthorizationGrantType#PASSWORD password} OR the
* {@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME username} and/or
* {@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME password} attributes are
* not available in the provided {@code context} OR the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* <p>
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes}
* are supported:
* <ol>
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's username</li>
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a {@code String} value for the resource owner's password</li>
* <li>{@link OAuth2AuthorizationContext#USERNAME_ATTRIBUTE_NAME} (required) - a
* {@code String} value for the resource owner's username</li>
* <li>{@link OAuth2AuthorizationContext#PASSWORD_ATTRIBUTE_NAME} (required) - a
* {@code String} value for the resource owner's password</li>
* </ol>
*
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization (or re-authorization) is not supported
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if
* authorization (or re-authorization) is not supported
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
ClientRegistration clientRegistration = context.getClientRegistration();
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (!AuthorizationGrantType.PASSWORD.equals(clientRegistration.getAuthorizationGrantType())) {
return Mono.empty();
}
String username = context.getAttribute(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME);
String password = context.getAttribute(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME);
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) {
return Mono.empty();
}
if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) {
// If client is already authorized and access token is NOT expired than no need for re-authorization
// If client is already authorized and access token is NOT expired than no
// need for re-authorization
return Mono.empty();
}
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken()) && authorizedClient.getRefreshToken() != null) {
// If client is already authorized and access token is expired and a refresh token is available,
// than return and allow RefreshTokenReactiveOAuth2AuthorizedClientProvider to handle the refresh
if (authorizedClient != null && hasTokenExpired(authorizedClient.getAccessToken())
&& authorizedClient.getRefreshToken() != null) {
// If client is already authorized and access token is expired and a refresh
// token is available,
// than return and allow RefreshTokenReactiveOAuth2AuthorizedClientProvider to
// handle the refresh
return Mono.empty();
}
OAuth2PasswordGrantRequest passwordGrantRequest =
new OAuth2PasswordGrantRequest(clientRegistration, username, password);
return Mono.just(passwordGrantRequest)
.flatMap(this.accessTokenResponseClient::getTokenResponse)
OAuth2PasswordGrantRequest passwordGrantRequest = new OAuth2PasswordGrantRequest(clientRegistration, username,
password);
return Mono.just(passwordGrantRequest).flatMap(this.accessTokenResponseClient::getTokenResponse)
.onErrorMap(OAuth2AuthorizationException.class,
e -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(), e))
.map(tokenResponse -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
(e) -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(),
e))
.map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
}
@@ -109,23 +115,26 @@ public final class PasswordReactiveOAuth2AuthorizedClientProvider implements Rea
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code password} grant
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code password} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code password} grant
*/
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
public void setAccessTokenResponseClient(
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds.
*
* <p>
* An access token is considered expired if {@code OAuth2AccessToken#getExpiresAt() - clockSkew}
* is before the current time {@code clock#instant()}.
*
* An access token is considered expired if
* {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
@@ -135,12 +144,13 @@ public final class PasswordReactiveOAuth2AuthorizedClientProvider implements Rea
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@@ -13,18 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import reactor.core.publisher.Mono;
package org.springframework.security.oauth2.client;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
/**
* Handles when an OAuth 2.0 Client
* fails to authorize (or re-authorize)
* via the authorization server or resource server.
* Handles when an OAuth 2.0 Client fails to authorize (or re-authorize) via the
* authorization server or resource server.
*
* @author Phil Clay
* @since 5.3
@@ -33,19 +34,18 @@ import java.util.Map;
public interface ReactiveOAuth2AuthorizationFailureHandler {
/**
* Called when an OAuth 2.0 Client
* fails to authorize (or re-authorize)
* via the authorization server or resource server.
*
* Called when an OAuth 2.0 Client fails to authorize (or re-authorize) via the
* authorization server or resource server.
* @param authorizationException the exception that contains details about what failed
* @param principal the {@code Principal} that was attempted to be authorized
* @param attributes an immutable {@code Map} of extra optional attributes present under certain conditions.
* For example, this might contain a {@link org.springframework.web.server.ServerWebExchange ServerWebExchange}
* if the authorization was performed within the context of a {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished handling the event.
* @param attributes an immutable {@code Map} of extra optional attributes present
* under certain conditions. For example, this might contain a
* {@link org.springframework.web.server.ServerWebExchange ServerWebExchange} if the
* authorization was performed within the context of a {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished
* handling the event.
*/
Mono<Void> onAuthorizationFailure(
OAuth2AuthorizationException authorizationException,
Authentication principal,
Mono<Void> onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal,
Map<String, Object> attributes);
}

View File

@@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.core.Authentication;
import reactor.core.publisher.Mono;
package org.springframework.security.oauth2.client;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
/**
* Handles when an OAuth 2.0 Client
* has been successfully authorized (or re-authorized)
* Handles when an OAuth 2.0 Client has been successfully authorized (or re-authorized)
* via the authorization server.
*
* @author Phil Clay
@@ -32,20 +33,18 @@ import java.util.Map;
public interface ReactiveOAuth2AuthorizationSuccessHandler {
/**
* Called when an OAuth 2.0 Client
* has been successfully authorized (or re-authorized)
* Called when an OAuth 2.0 Client has been successfully authorized (or re-authorized)
* via the authorization server.
*
* @param authorizedClient the client that was successfully authorized
* @param principal the {@code Principal} associated with the authorized client
* @param attributes an immutable {@code Map} of extra optional attributes present under certain conditions.
* For example, this might contain a {@link org.springframework.web.server.ServerWebExchange ServerWebExchange}
* if the authorization was performed within the context of a {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished handling the event.
* @param attributes an immutable {@code Map} of extra optional attributes present
* under certain conditions. For example, this might contain a
* {@link org.springframework.web.server.ServerWebExchange ServerWebExchange} if the
* authorization was performed within the context of a {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished
* handling the event.
*/
Mono<Void> onAuthorizationSuccess(
OAuth2AuthorizedClient authorizedClient,
Authentication principal,
Mono<Void> onAuthorizationSuccess(OAuth2AuthorizedClient authorizedClient, Authentication principal,
Map<String, Object> attributes);
}

View File

@@ -13,23 +13,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import reactor.core.publisher.Mono;
/**
* Implementations of this interface are responsible for the overall management
* of {@link OAuth2AuthorizedClient Authorized Client(s)}.
* Implementations of this interface are responsible for the overall management of
* {@link OAuth2AuthorizedClient Authorized Client(s)}.
*
* <p>
* The primary responsibilities include:
* <ol>
* <li>Authorizing (or re-authorizing) an OAuth 2.0 Client
* by leveraging a {@link ReactiveOAuth2AuthorizedClientProvider}(s).</li>
* <li>Delegating the persistence of an {@link OAuth2AuthorizedClient},
* typically using a {@link ReactiveOAuth2AuthorizedClientService} OR {@link ServerOAuth2AuthorizedClientRepository}.</li>
* <li>Authorizing (or re-authorizing) an OAuth 2.0 Client by leveraging a
* {@link ReactiveOAuth2AuthorizedClientProvider}(s).</li>
* <li>Delegating the persistence of an {@link OAuth2AuthorizedClient}, typically using a
* {@link ReactiveOAuth2AuthorizedClientService} OR
* {@link ServerOAuth2AuthorizedClientRepository}.</li>
* </ol>
*
* @author Joe Grandja
@@ -43,20 +46,24 @@ import reactor.core.publisher.Mono;
public interface ReactiveOAuth2AuthorizedClientManager {
/**
* Attempt to authorize or re-authorize (if required) the {@link ClientRegistration client}
* identified by the provided {@link OAuth2AuthorizeRequest#getClientRegistrationId() clientRegistrationId}.
* Implementations must return an empty {@code Mono} if authorization is not supported for the specified client,
* e.g. the associated {@link ReactiveOAuth2AuthorizedClientProvider}(s) does not support
* the {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type configured for the client.
* Attempt to authorize or re-authorize (if required) the {@link ClientRegistration
* client} identified by the provided
* {@link OAuth2AuthorizeRequest#getClientRegistrationId() clientRegistrationId}.
* Implementations must return an empty {@code Mono} if authorization is not supported
* for the specified client, e.g. the associated
* {@link ReactiveOAuth2AuthorizedClientProvider}(s) does not support the
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type
* configured for the client.
*
* <p>
* In the case of re-authorization, implementations must return the provided {@link OAuth2AuthorizeRequest#getAuthorizedClient() authorized client}
* if re-authorization is not supported for the client OR is not required,
* e.g. a {@link OAuth2AuthorizedClient#getRefreshToken() refresh token} is not available OR
* In the case of re-authorization, implementations must return the provided
* {@link OAuth2AuthorizeRequest#getAuthorizedClient() authorized client} if
* re-authorization is not supported for the client OR is not required, e.g. a
* {@link OAuth2AuthorizedClient#getRefreshToken() refresh token} is not available OR
* the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* @param authorizeRequest the authorize request
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization is not supported for the specified client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if
* authorization is not supported for the specified client
*/
Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest);

View File

@@ -13,32 +13,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import reactor.core.publisher.Mono;
/**
* A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client.
* Implementations will typically implement a specific {@link AuthorizationGrantType authorization grant} type.
* A strategy for authorizing (or re-authorizing) an OAuth 2.0 Client. Implementations
* will typically implement a specific {@link AuthorizationGrantType authorization grant}
* type.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AuthorizedClient
* @see OAuth2AuthorizationContext
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
* 1.3 Authorization Grant</a>
*/
@FunctionalInterface
public interface ReactiveOAuth2AuthorizedClientProvider {
/**
* Attempt to authorize (or re-authorize) the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided context.
* Implementations must return an empty {@code Mono} if authorization is not supported for the specified client,
* e.g. the provider doesn't support the {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type configured for the client.
*
* Attempt to authorize (or re-authorize) the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* context. Implementations must return an empty {@code Mono} if authorization is not
* supported for the specified client, e.g. the provider doesn't support the
* {@link ClientRegistration#getAuthorizationGrantType() authorization grant} type
* configured for the client.
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if authorization is not supported for the specified client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if
* authorization is not supported for the specified client
*/
Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context);

View File

@@ -13,13 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.util.Assert;
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
@@ -30,13 +25,19 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.util.Assert;
/**
* A builder that builds a {@link DelegatingReactiveOAuth2AuthorizedClientProvider} composed of
* one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s) that implement specific authorization grants.
* The supported authorization grants are {@link #authorizationCode() authorization_code},
* {@link #refreshToken() refresh_token}, {@link #clientCredentials() client_credentials}
* and {@link #password() password}.
* In addition to the standard authorization grants, an implementation of an extension grant
* A builder that builds a {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
* composed of one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s) that
* implement specific authorization grants. The supported authorization grants are
* {@link #authorizationCode() authorization_code}, {@link #refreshToken() refresh_token},
* {@link #clientCredentials() client_credentials} and {@link #password() password}. In
* addition to the standard authorization grants, an implementation of an extension grant
* may be supplied via {@link #provider(ReactiveOAuth2AuthorizedClientProvider)}.
*
* @author Joe Grandja
@@ -49,14 +50,15 @@ import java.util.stream.Collectors;
* @see DelegatingReactiveOAuth2AuthorizedClientProvider
*/
public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
private final Map<Class<?>, Builder> builders = new LinkedHashMap<>();
private ReactiveOAuth2AuthorizedClientProviderBuilder() {
}
/**
* Returns a new {@link ReactiveOAuth2AuthorizedClientProviderBuilder} for configuring the supported authorization grant(s).
*
* Returns a new {@link ReactiveOAuth2AuthorizedClientProviderBuilder} for configuring
* the supported authorization grant(s).
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public static ReactiveOAuth2AuthorizedClientProviderBuilder builder() {
@@ -64,184 +66,167 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
}
/**
* Configures a {@link ReactiveOAuth2AuthorizedClientProvider} to be composed with the {@link DelegatingReactiveOAuth2AuthorizedClientProvider}.
* This may be used for implementations of extension authorization grants.
*
* Configures a {@link ReactiveOAuth2AuthorizedClientProvider} to be composed with the
* {@link DelegatingReactiveOAuth2AuthorizedClientProvider}. This may be used for
* implementations of extension authorization grants.
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder provider(ReactiveOAuth2AuthorizedClientProvider provider) {
Assert.notNull(provider, "provider cannot be null");
this.builders.computeIfAbsent(provider.getClass(), k -> () -> provider);
this.builders.computeIfAbsent(provider.getClass(), (k) -> () -> provider);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code authorization_code} grant.
*
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder authorizationCode() {
this.builders.computeIfAbsent(AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class, k -> new AuthorizationCodeGrantBuilder());
this.builders.computeIfAbsent(AuthorizationCodeReactiveOAuth2AuthorizedClientProvider.class,
(k) -> new AuthorizationCodeGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code refresh_token} grant.
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder refreshToken() {
this.builders.computeIfAbsent(RefreshTokenReactiveOAuth2AuthorizedClientProvider.class,
(k) -> new RefreshTokenGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code refresh_token} grant.
* @param builderConsumer a {@code Consumer} of {@link RefreshTokenGrantBuilder} used
* for further configuration
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder refreshToken(
Consumer<RefreshTokenGrantBuilder> builderConsumer) {
RefreshTokenGrantBuilder builder = (RefreshTokenGrantBuilder) this.builders.computeIfAbsent(
RefreshTokenReactiveOAuth2AuthorizedClientProvider.class, (k) -> new RefreshTokenGrantBuilder());
builderConsumer.accept(builder);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code client_credentials} grant.
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder clientCredentials() {
this.builders.computeIfAbsent(ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class,
(k) -> new ClientCredentialsGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code client_credentials} grant.
* @param builderConsumer a {@code Consumer} of {@link ClientCredentialsGrantBuilder}
* used for further configuration
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder clientCredentials(
Consumer<ClientCredentialsGrantBuilder> builderConsumer) {
ClientCredentialsGrantBuilder builder = (ClientCredentialsGrantBuilder) this.builders.computeIfAbsent(
ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class,
(k) -> new ClientCredentialsGrantBuilder());
builderConsumer.accept(builder);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code password} grant.
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder password() {
this.builders.computeIfAbsent(PasswordReactiveOAuth2AuthorizedClientProvider.class,
(k) -> new PasswordGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code password} grant.
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for
* further configuration
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
PasswordReactiveOAuth2AuthorizedClientProvider.class, (k) -> new PasswordGrantBuilder());
builderConsumer.accept(builder);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Builds an instance of {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
* composed of one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s).
* @return the {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
*/
public ReactiveOAuth2AuthorizedClientProvider build() {
List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders = this.builders.values().stream()
.map(Builder::build).collect(Collectors.toList());
return new DelegatingReactiveOAuth2AuthorizedClientProvider(authorizedClientProviders);
}
interface Builder {
ReactiveOAuth2AuthorizedClientProvider build();
}
/**
* A builder for the {@code authorization_code} grant.
*/
public class AuthorizationCodeGrantBuilder implements Builder {
public final class AuthorizationCodeGrantBuilder implements Builder {
private AuthorizationCodeGrantBuilder() {
}
/**
* Builds an instance of {@link AuthorizationCodeReactiveOAuth2AuthorizedClientProvider}.
*
* Builds an instance of
* {@link AuthorizationCodeReactiveOAuth2AuthorizedClientProvider}.
* @return the {@link AuthorizationCodeReactiveOAuth2AuthorizedClientProvider}
*/
@Override
public ReactiveOAuth2AuthorizedClientProvider build() {
return new AuthorizationCodeReactiveOAuth2AuthorizedClientProvider();
}
}
/**
* Configures support for the {@code refresh_token} grant.
*
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder refreshToken() {
this.builders.computeIfAbsent(RefreshTokenReactiveOAuth2AuthorizedClientProvider.class, k -> new RefreshTokenGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code refresh_token} grant.
*
* @param builderConsumer a {@code Consumer} of {@link RefreshTokenGrantBuilder} used for further configuration
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder refreshToken(Consumer<RefreshTokenGrantBuilder> builderConsumer) {
RefreshTokenGrantBuilder builder = (RefreshTokenGrantBuilder) this.builders.computeIfAbsent(
RefreshTokenReactiveOAuth2AuthorizedClientProvider.class, k -> new RefreshTokenGrantBuilder());
builderConsumer.accept(builder);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* A builder for the {@code refresh_token} grant.
*/
public class RefreshTokenGrantBuilder implements Builder {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private RefreshTokenGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder accessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* @param clockSkew the maximum acceptable clock skew
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* @param clock the clock
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Builds an instance of {@link RefreshTokenReactiveOAuth2AuthorizedClientProvider}.
*
* @return the {@link RefreshTokenReactiveOAuth2AuthorizedClientProvider}
*/
@Override
public ReactiveOAuth2AuthorizedClientProvider build() {
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}
}
/**
* Configures support for the {@code client_credentials} grant.
*
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder clientCredentials() {
this.builders.computeIfAbsent(ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class, k -> new ClientCredentialsGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code client_credentials} grant.
*
* @param builderConsumer a {@code Consumer} of {@link ClientCredentialsGrantBuilder} used for further configuration
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder clientCredentials(Consumer<ClientCredentialsGrantBuilder> builderConsumer) {
ClientCredentialsGrantBuilder builder = (ClientCredentialsGrantBuilder) this.builders.computeIfAbsent(
ClientCredentialsReactiveOAuth2AuthorizedClientProvider.class, k -> new ClientCredentialsGrantBuilder());
builderConsumer.accept(builder);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* A builder for the {@code client_credentials} grant.
*/
public class ClientCredentialsGrantBuilder implements Builder {
public final class ClientCredentialsGrantBuilder implements Builder {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private ClientCredentialsGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link ClientCredentialsGrantBuilder}
*/
public ClientCredentialsGrantBuilder accessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
public ClientCredentialsGrantBuilder accessTokenResponseClient(
ReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link ClientCredentialsGrantBuilder}
*/
@@ -251,8 +236,8 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link ClientCredentialsGrantBuilder}
*/
@@ -262,8 +247,8 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
}
/**
* Builds an instance of {@link ClientCredentialsReactiveOAuth2AuthorizedClientProvider}.
*
* Builds an instance of
* {@link ClientCredentialsReactiveOAuth2AuthorizedClientProvider}.
* @return the {@link ClientCredentialsReactiveOAuth2AuthorizedClientProvider}
*/
@Override
@@ -280,57 +265,40 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
}
return authorizedClientProvider;
}
}
/**
* Configures support for the {@code password} grant.
*
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder password() {
this.builders.computeIfAbsent(PasswordReactiveOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* Configures support for the {@code password} grant.
*
* @param builderConsumer a {@code Consumer} of {@link PasswordGrantBuilder} used for further configuration
* @return the {@link ReactiveOAuth2AuthorizedClientProviderBuilder}
*/
public ReactiveOAuth2AuthorizedClientProviderBuilder password(Consumer<PasswordGrantBuilder> builderConsumer) {
PasswordGrantBuilder builder = (PasswordGrantBuilder) this.builders.computeIfAbsent(
PasswordReactiveOAuth2AuthorizedClientProvider.class, k -> new PasswordGrantBuilder());
builderConsumer.accept(builder);
return ReactiveOAuth2AuthorizedClientProviderBuilder.this;
}
/**
* A builder for the {@code password} grant.
*/
public class PasswordGrantBuilder implements Builder {
public final class PasswordGrantBuilder implements Builder {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private PasswordGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link PasswordGrantBuilder}
*/
public PasswordGrantBuilder accessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
public PasswordGrantBuilder accessTokenResponseClient(
ReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access token expiry.
* An access token is considered expired if it's before {@code Instant.now(this.clock) - clockSkew}.
*
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link PasswordGrantBuilder}
*/
@@ -340,8 +308,8 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link PasswordGrantBuilder}
*/
@@ -352,7 +320,6 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
/**
* Builds an instance of {@link PasswordReactiveOAuth2AuthorizedClientProvider}.
*
* @return the {@link PasswordReactiveOAuth2AuthorizedClientProvider}
*/
@Override
@@ -369,23 +336,79 @@ public final class ReactiveOAuth2AuthorizedClientProviderBuilder {
}
return authorizedClientProvider;
}
}
/**
* Builds an instance of {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
* composed of one or more {@link ReactiveOAuth2AuthorizedClientProvider}(s).
*
* @return the {@link DelegatingReactiveOAuth2AuthorizedClientProvider}
* A builder for the {@code refresh_token} grant.
*/
public ReactiveOAuth2AuthorizedClientProvider build() {
List<ReactiveOAuth2AuthorizedClientProvider> authorizedClientProviders =
this.builders.values().stream()
.map(Builder::build)
.collect(Collectors.toList());
return new DelegatingReactiveOAuth2AuthorizedClientProvider(authorizedClientProviders);
public final class RefreshTokenGrantBuilder implements Builder {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient;
private Duration clockSkew;
private Clock clock;
private RefreshTokenGrantBuilder() {
}
/**
* Sets the client used when requesting an access token credential at the Token
* Endpoint.
* @param accessTokenResponseClient the client used when requesting an access
* token credential at the Token Endpoint
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder accessTokenResponseClient(
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
this.accessTokenResponseClient = accessTokenResponseClient;
return this;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the access
* token expiry. An access token is considered expired if it's before
* {@code Instant.now(this.clock) - clockSkew}.
* @param clockSkew the maximum acceptable clock skew
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clockSkew(Duration clockSkew) {
this.clockSkew = clockSkew;
return this;
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the
* access token expiry.
* @param clock the clock
* @return the {@link RefreshTokenGrantBuilder}
*/
public RefreshTokenGrantBuilder clock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Builds an instance of
* {@link RefreshTokenReactiveOAuth2AuthorizedClientProvider}.
* @return the {@link RefreshTokenReactiveOAuth2AuthorizedClientProvider}
*/
@Override
public ReactiveOAuth2AuthorizedClientProvider build() {
RefreshTokenReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = new RefreshTokenReactiveOAuth2AuthorizedClientProvider();
if (this.accessTokenResponseClient != null) {
authorizedClientProvider.setAccessTokenResponseClient(this.accessTokenResponseClient);
}
if (this.clockSkew != null) {
authorizedClientProvider.setClockSkew(this.clockSkew);
}
if (this.clock != null) {
authorizedClientProvider.setClock(this.clock);
}
return authorizedClientProvider;
}
}
interface Builder {
ReactiveOAuth2AuthorizedClientProvider build();
}
}

View File

@@ -13,21 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import reactor.core.publisher.Mono;
/**
* Implementations of this interface are responsible for the management
* of {@link OAuth2AuthorizedClient Authorized Client(s)}, which provide the purpose
* of associating an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential
* Implementations of this interface are responsible for the management of
* {@link OAuth2AuthorizedClient Authorized Client(s)}, which provide the purpose of
* associating an {@link OAuth2AuthorizedClient#getAccessToken() Access Token} credential
* to a {@link OAuth2AuthorizedClient#getClientRegistration() Client} and Resource Owner,
* who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal}
* that originally granted the authorization.
* who is the {@link OAuth2AuthorizedClient#getPrincipalName() Principal} that originally
* granted the authorization.
*
* @author Rob Winch
* @since 5.1
@@ -39,32 +40,27 @@ import reactor.core.publisher.Mono;
public interface ReactiveOAuth2AuthorizedClientService {
/**
* Returns the {@link OAuth2AuthorizedClient} associated to the
* provided client registration identifier and End-User's {@code Principal} name
* or {@code null} if not available.
*
* Returns the {@link OAuth2AuthorizedClient} associated to the provided client
* registration identifier and End-User's {@code Principal} name or {@code null} if
* not available.
* @param clientRegistrationId the identifier for the client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
* @param <T> a type of OAuth2AuthorizedClient
* @return the {@link OAuth2AuthorizedClient} or {@code null} if not available
*/
<T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId,
String principalName);
<T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName);
/**
* Saves the {@link OAuth2AuthorizedClient} associating it to
* the provided End-User {@link Authentication} (Resource Owner).
*
* Saves the {@link OAuth2AuthorizedClient} associating it to the provided End-User
* {@link Authentication} (Resource Owner).
* @param authorizedClient the authorized client
* @param principal the End-User {@link Authentication} (Resource Owner)
*/
Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient,
Authentication principal);
Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal);
/**
* Removes the {@link OAuth2AuthorizedClient} associated to the
* provided client registration identifier and End-User's {@code Principal} name.
*
* Removes the {@link OAuth2AuthorizedClient} associated to the provided client
* registration identifier and End-User's {@code Principal} name.
* @param clientRegistrationId the identifier for the client's registration
* @param principalName the name of the End-User {@code Principal} (Resource Owner)
*/

View File

@@ -13,8 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.springframework.lang.Nullable;
import org.springframework.security.oauth2.client.endpoint.DefaultRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
@@ -25,17 +34,9 @@ import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.util.Assert;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* An implementation of an {@link OAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
* An implementation of an {@link OAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
*
* @author Joe Grandja
* @since 5.2
@@ -43,84 +44,93 @@ import java.util.Set;
* @see DefaultRefreshTokenTokenResponseClient
*/
public final class RefreshTokenOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider {
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient =
new DefaultRefreshTokenTokenResponseClient();
private OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
/**
* Attempt to re-authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns {@code null} if re-authorization is not supported,
* e.g. the client is not authorized OR the {@link OAuth2AuthorizedClient#getRefreshToken() refresh token}
* is not available for the authorized client OR the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
* Attempt to re-authorize the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* {@code context}. Returns {@code null} if re-authorization is not supported, e.g.
* the client is not authorized OR the {@link OAuth2AuthorizedClient#getRefreshToken()
* refresh token} is not available for the authorized client OR the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
*
* <p>
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes}
* are supported:
* <ol>
* <li>{@link OAuth2AuthorizationContext#REQUEST_SCOPE_ATTRIBUTE_NAME} (optional) - a {@code String[]} of scope(s)
* to be requested by the {@link OAuth2AuthorizationContext#getClientRegistration() client}</li>
* <li>{@link OAuth2AuthorizationContext#REQUEST_SCOPE_ATTRIBUTE_NAME} (optional) - a
* {@code String[]} of scope(s) to be requested by the
* {@link OAuth2AuthorizationContext#getClientRegistration() client}</li>
* </ol>
*
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or {@code null} if re-authorization is not supported
* @return the {@link OAuth2AuthorizedClient} or {@code null} if re-authorization is
* not supported
*/
@Override
@Nullable
public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient == null ||
authorizedClient.getRefreshToken() == null ||
!hasTokenExpired(authorizedClient.getAccessToken())) {
if (authorizedClient == null || authorizedClient.getRefreshToken() == null
|| !hasTokenExpired(authorizedClient.getAccessToken())) {
return null;
}
Object requestScope = context.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
Set<String> scopes = Collections.emptySet();
if (requestScope != null) {
Assert.isInstanceOf(String[].class, requestScope,
"The context attribute must be of type String[] '" + OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME + "'");
Assert.isInstanceOf(String[].class, requestScope, "The context attribute must be of type String[] '"
+ OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME + "'");
scopes = new HashSet<>(Arrays.asList((String[]) requestScope));
}
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
authorizedClient.getClientRegistration(), authorizedClient.getAccessToken(),
authorizedClient.getRefreshToken(), scopes);
OAuth2AccessTokenResponse tokenResponse;
try {
tokenResponse = this.accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest);
} catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(), authorizedClient.getClientRegistration().getRegistrationId(), ex);
}
OAuth2AccessTokenResponse tokenResponse = getTokenResponse(authorizedClient, refreshTokenGrantRequest);
return new OAuth2AuthorizedClient(context.getAuthorizedClient().getClientRegistration(),
context.getPrincipal().getName(), tokenResponse.getAccessToken(), tokenResponse.getRefreshToken());
}
private OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizedClient authorizedClient,
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
try {
return this.accessTokenResponseClient.getTokenResponse(refreshTokenGrantRequest);
}
catch (OAuth2AuthorizationException ex) {
throw new ClientAuthorizationException(ex.getError(),
authorizedClient.getClientRegistration().getRegistrationId(), ex);
}
}
private boolean hasTokenExpired(AbstractOAuth2Token token) {
return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew));
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code refresh_token} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code refresh_token} grant
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code refresh_token} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code refresh_token} grant
*/
public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
public void setAccessTokenResponseClient(
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds.
*
* <p>
* An access token is considered expired if {@code OAuth2AccessToken#getExpiresAt() - clockSkew}
* is before the current time {@code clock#instant()}.
*
* An access token is considered expired if
* {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
@@ -130,12 +140,13 @@ public final class RefreshTokenOAuth2AuthorizedClientProvider implements OAuth2A
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@@ -13,17 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
package org.springframework.security.oauth2.client;
import java.time.Clock;
import java.time.Duration;
@@ -33,65 +24,79 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.WebClientReactiveRefreshTokenTokenResponseClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.util.Assert;
/**
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider}
* for the {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
* An implementation of a {@link ReactiveOAuth2AuthorizedClientProvider} for the
* {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AuthorizedClientProvider
* @see WebClientReactiveRefreshTokenTokenResponseClient
*/
public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider implements ReactiveOAuth2AuthorizedClientProvider {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient =
new WebClientReactiveRefreshTokenTokenResponseClient();
public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider
implements ReactiveOAuth2AuthorizedClientProvider {
private ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = new WebClientReactiveRefreshTokenTokenResponseClient();
private Duration clockSkew = Duration.ofSeconds(60);
private Clock clock = Clock.systemUTC();
/**
* Attempt to re-authorize the {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided {@code context}.
* Returns an empty {@code Mono} if re-authorization is not supported,
* e.g. the client is not authorized OR the {@link OAuth2AuthorizedClient#getRefreshToken() refresh token}
* is not available for the authorized client OR the {@link OAuth2AuthorizedClient#getAccessToken() access token} is not expired.
* Attempt to re-authorize the
* {@link OAuth2AuthorizationContext#getClientRegistration() client} in the provided
* {@code context}. Returns an empty {@code Mono} if re-authorization is not
* supported, e.g. the client is not authorized OR the
* {@link OAuth2AuthorizedClient#getRefreshToken() refresh token} is not available for
* the authorized client OR the {@link OAuth2AuthorizedClient#getAccessToken() access
* token} is not expired.
*
* <p>
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes} are supported:
* The following {@link OAuth2AuthorizationContext#getAttributes() context attributes}
* are supported:
* <ol>
* <li>{@code "org.springframework.security.oauth2.client.REQUEST_SCOPE"} (optional) - a {@code String[]} of scope(s)
* to be requested by the {@link OAuth2AuthorizationContext#getClientRegistration() client}</li>
* <li>{@code "org.springframework.security.oauth2.client.REQUEST_SCOPE"} (optional) -
* a {@code String[]} of scope(s) to be requested by the
* {@link OAuth2AuthorizationContext#getClientRegistration() client}</li>
* </ol>
*
* @param context the context that holds authorization-specific state for the client
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if re-authorization is not supported
* @return the {@link OAuth2AuthorizedClient} or an empty {@code Mono} if
* re-authorization is not supported
*/
@Override
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizationContext context) {
Assert.notNull(context, "context cannot be null");
OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient();
if (authorizedClient == null ||
authorizedClient.getRefreshToken() == null ||
!hasTokenExpired(authorizedClient.getAccessToken())) {
if (authorizedClient == null || authorizedClient.getRefreshToken() == null
|| !hasTokenExpired(authorizedClient.getAccessToken())) {
return Mono.empty();
}
Object requestScope = context.getAttribute(OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME);
Set<String> scopes = Collections.emptySet();
if (requestScope != null) {
Assert.isInstanceOf(String[].class, requestScope,
"The context attribute must be of type String[] '" + OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME + "'");
Assert.isInstanceOf(String[].class, requestScope, "The context attribute must be of type String[] '"
+ OAuth2AuthorizationContext.REQUEST_SCOPE_ATTRIBUTE_NAME + "'");
scopes = new HashSet<>(Arrays.asList((String[]) requestScope));
}
ClientRegistration clientRegistration = context.getClientRegistration();
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
clientRegistration, authorizedClient.getAccessToken(), authorizedClient.getRefreshToken(), scopes);
return Mono.just(refreshTokenGrantRequest)
.flatMap(this.accessTokenResponseClient::getTokenResponse)
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration,
authorizedClient.getAccessToken(), authorizedClient.getRefreshToken(), scopes);
return Mono.just(refreshTokenGrantRequest).flatMap(this.accessTokenResponseClient::getTokenResponse)
.onErrorMap(OAuth2AuthorizationException.class,
e -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(), e))
.map(tokenResponse -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
(e) -> new ClientAuthorizationException(e.getError(), clientRegistration.getRegistrationId(),
e))
.map((tokenResponse) -> new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(),
tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()));
}
@@ -100,23 +105,26 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider implements
}
/**
* Sets the client used when requesting an access token credential at the Token Endpoint for the {@code refresh_token} grant.
*
* @param accessTokenResponseClient the client used when requesting an access token credential at the Token Endpoint for the {@code refresh_token} grant
* Sets the client used when requesting an access token credential at the Token
* Endpoint for the {@code refresh_token} grant.
* @param accessTokenResponseClient the client used when requesting an access token
* credential at the Token Endpoint for the {@code refresh_token} grant
*/
public void setAccessTokenResponseClient(ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
public void setAccessTokenResponseClient(
ReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
/**
* Sets the maximum acceptable clock skew, which is used when checking the
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is 60 seconds.
* {@link OAuth2AuthorizedClient#getAccessToken() access token} expiry. The default is
* 60 seconds.
*
* <p>
* An access token is considered expired if {@code OAuth2AccessToken#getExpiresAt() - clockSkew}
* is before the current time {@code clock#instant()}.
*
* An access token is considered expired if
* {@code OAuth2AccessToken#getExpiresAt() - clockSkew} is before the current time
* {@code clock#instant()}.
* @param clockSkew the maximum acceptable clock skew
*/
public void setClockSkew(Duration clockSkew) {
@@ -126,12 +134,13 @@ public final class RefreshTokenReactiveOAuth2AuthorizedClientProvider implements
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access token expiry.
*
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when checking the access
* token expiry.
* @param clock the clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
@@ -22,16 +29,10 @@ import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.util.Assert;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* An {@link OAuth2AuthorizationFailureHandler} that removes an {@link OAuth2AuthorizedClient}
* when the {@link OAuth2Error#getErrorCode()} matches
* one of the configured {@link OAuth2ErrorCodes OAuth 2.0 error codes}.
* An {@link OAuth2AuthorizationFailureHandler} that removes an
* {@link OAuth2AuthorizedClient} when the {@link OAuth2Error#getErrorCode()} matches one
* of the configured {@link OAuth2ErrorCodes OAuth 2.0 error codes}.
*
* @author Joe Grandja
* @since 5.3
@@ -42,29 +43,28 @@ import java.util.Set;
public class RemoveAuthorizedClientOAuth2AuthorizationFailureHandler implements OAuth2AuthorizationFailureHandler {
/**
* The default OAuth 2.0 error codes that will trigger removal of an {@link OAuth2AuthorizedClient}.
* The default OAuth 2.0 error codes that will trigger removal of an
* {@link OAuth2AuthorizedClient}.
* @see OAuth2ErrorCodes
*/
public static final Set<String> DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
/*
* Returned from Resource Servers when an access token provided is expired, revoked,
* malformed, or invalid for other reasons.
*
* Note that this is needed because ServletOAuth2AuthorizedClientExchangeFilterFunction
* delegates this type of failure received from a Resource Server
* to this failure handler.
*/
OAuth2ErrorCodes.INVALID_TOKEN,
/*
* Returned from Authorization Servers when the authorization grant or refresh token is invalid, expired, revoked,
* does not match the redirection URI used in the authorization request, or was issued to another client.
*/
OAuth2ErrorCodes.INVALID_GRANT
)));
public static final Set<String> DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES;
static {
Set<String> codes = new LinkedHashSet<>();
// Returned from Resource Servers when an access token provided is expired,
// revoked, malformed, or invalid for other reasons. Note that this is needed
// because ServletOAuth2AuthorizedClientExchangeFilterFunction delegates this type
// of failure received from a Resource Server to this failure handler.
codes.add(OAuth2ErrorCodes.INVALID_TOKEN);
// Returned from Authorization Servers when the authorization grant or refresh
// token is invalid, expired, revoked, does not match the redirection URI used in
// the authorization request, or was issued to another client.
codes.add(OAuth2ErrorCodes.INVALID_GRANT);
DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES = Collections.unmodifiableSet(codes);
}
/**
* The OAuth 2.0 error codes which will trigger removal of an {@link OAuth2AuthorizedClient}.
* The OAuth 2.0 error codes which will trigger removal of an
* {@link OAuth2AuthorizedClient}.
* @see OAuth2ErrorCodes
*/
private final Set<String> removeAuthorizedClientErrorCodes;
@@ -76,6 +76,59 @@ public class RemoveAuthorizedClientOAuth2AuthorizationFailureHandler implements
*/
private final OAuth2AuthorizedClientRemover delegate;
/**
* Constructs a {@code RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} using
* the provided parameters.
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for
* removing an {@link OAuth2AuthorizedClient} if the error code is one of the
* {@link #DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES}.
*/
public RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
OAuth2AuthorizedClientRemover authorizedClientRemover) {
this(authorizedClientRemover, DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES);
}
/**
* Constructs a {@code RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} using
* the provided parameters.
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for
* removing an {@link OAuth2AuthorizedClient} if the error code is one of the
* {@link #removeAuthorizedClientErrorCodes}.
* @param removeAuthorizedClientErrorCodes the OAuth 2.0 error codes which will
* trigger removal of an authorized client.
* @see OAuth2ErrorCodes
*/
public RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
OAuth2AuthorizedClientRemover authorizedClientRemover, Set<String> removeAuthorizedClientErrorCodes) {
Assert.notNull(authorizedClientRemover, "authorizedClientRemover cannot be null");
Assert.notNull(removeAuthorizedClientErrorCodes, "removeAuthorizedClientErrorCodes cannot be null");
this.removeAuthorizedClientErrorCodes = Collections
.unmodifiableSet(new HashSet<>(removeAuthorizedClientErrorCodes));
this.delegate = authorizedClientRemover;
}
@Override
public void onAuthorizationFailure(OAuth2AuthorizationException authorizationException, Authentication principal,
Map<String, Object> attributes) {
if (authorizationException instanceof ClientAuthorizationException
&& hasRemovalErrorCode(authorizationException)) {
ClientAuthorizationException clientAuthorizationException = (ClientAuthorizationException) authorizationException;
this.delegate.removeAuthorizedClient(clientAuthorizationException.getClientRegistrationId(), principal,
attributes);
}
}
/**
* Returns true if the given exception has an error code that indicates that the
* authorized client should be removed.
* @param authorizationException the exception that caused the authorization failure
* @return true if the given exception has an error code that indicates that the
* authorized client should be removed.
*/
private boolean hasRemovalErrorCode(OAuth2AuthorizationException authorizationException) {
return this.removeAuthorizedClientErrorCodes.contains(authorizationException.getError().getErrorCode());
}
/**
* Removes an {@link OAuth2AuthorizedClient} from an
* {@link OAuth2AuthorizedClientRepository} or {@link OAuth2AuthorizedClientService}.
@@ -84,68 +137,19 @@ public class RemoveAuthorizedClientOAuth2AuthorizationFailureHandler implements
public interface OAuth2AuthorizedClientRemover {
/**
* Removes the {@link OAuth2AuthorizedClient} associated to the
* provided client registration identifier and End-User {@link Authentication} (Resource Owner).
*
* Removes the {@link OAuth2AuthorizedClient} associated to the provided client
* registration identifier and End-User {@link Authentication} (Resource Owner).
* @param clientRegistrationId the identifier for the client's registration
* @param principal the End-User {@link Authentication} (Resource Owner)
* @param attributes an immutable {@code Map} of (optional) attributes present under certain conditions.
* For example, this might contain a {@code javax.servlet.http.HttpServletRequest}
* and {@code javax.servlet.http.HttpServletResponse} if the authorization was performed
* within the context of a {@code javax.servlet.ServletContext}.
* @param attributes an immutable {@code Map} of (optional) attributes present
* under certain conditions. For example, this might contain a
* {@code javax.servlet.http.HttpServletRequest} and
* {@code javax.servlet.http.HttpServletResponse} if the authorization was
* performed within the context of a {@code javax.servlet.ServletContext}.
*/
void removeAuthorizedClient(String clientRegistrationId, Authentication principal, Map<String, Object> attributes);
void removeAuthorizedClient(String clientRegistrationId, Authentication principal,
Map<String, Object> attributes);
}
/**
* Constructs a {@code RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} using the provided parameters.
*
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for removing an {@link OAuth2AuthorizedClient}
* if the error code is one of the {@link #DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES}.
*/
public RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(OAuth2AuthorizedClientRemover authorizedClientRemover) {
this(authorizedClientRemover, DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES);
}
/**
* Constructs a {@code RemoveAuthorizedClientOAuth2AuthorizationFailureHandler} using the provided parameters.
*
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for removing an {@link OAuth2AuthorizedClient}
* if the error code is one of the {@link #removeAuthorizedClientErrorCodes}.
* @param removeAuthorizedClientErrorCodes the OAuth 2.0 error codes which will trigger removal of an authorized client.
* @see OAuth2ErrorCodes
*/
public RemoveAuthorizedClientOAuth2AuthorizationFailureHandler(
OAuth2AuthorizedClientRemover authorizedClientRemover,
Set<String> removeAuthorizedClientErrorCodes) {
Assert.notNull(authorizedClientRemover, "authorizedClientRemover cannot be null");
Assert.notNull(removeAuthorizedClientErrorCodes, "removeAuthorizedClientErrorCodes cannot be null");
this.removeAuthorizedClientErrorCodes = Collections.unmodifiableSet(new HashSet<>(removeAuthorizedClientErrorCodes));
this.delegate = authorizedClientRemover;
}
@Override
public void onAuthorizationFailure(OAuth2AuthorizationException authorizationException,
Authentication principal, Map<String, Object> attributes) {
if (authorizationException instanceof ClientAuthorizationException &&
hasRemovalErrorCode(authorizationException)) {
ClientAuthorizationException clientAuthorizationException = (ClientAuthorizationException) authorizationException;
this.delegate.removeAuthorizedClient(
clientAuthorizationException.getClientRegistrationId(), principal, attributes);
}
}
/**
* Returns true if the given exception has an error code that
* indicates that the authorized client should be removed.
*
* @param authorizationException the exception that caused the authorization failure
* @return true if the given exception has an error code that
* indicates that the authorized client should be removed.
*/
private boolean hasRemovalErrorCode(OAuth2AuthorizationException authorizationException) {
return this.removeAuthorizedClientErrorCodes.contains(authorizationException.getError().getErrorCode());
}
}

View File

@@ -13,56 +13,60 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A {@link ReactiveOAuth2AuthorizationFailureHandler} that removes an {@link OAuth2AuthorizedClient}
* when the {@link OAuth2Error#getErrorCode()} matches
* one of the configured {@link OAuth2ErrorCodes OAuth 2.0 error codes}.
* A {@link ReactiveOAuth2AuthorizationFailureHandler} that removes an
* {@link OAuth2AuthorizedClient} when the {@link OAuth2Error#getErrorCode()} matches one
* of the configured {@link OAuth2ErrorCodes OAuth 2.0 error codes}.
*
* @author Phil Clay
* @since 5.3
*/
public class RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler implements ReactiveOAuth2AuthorizationFailureHandler {
public class RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler
implements ReactiveOAuth2AuthorizationFailureHandler {
/**
* The default OAuth 2.0 error codes that will trigger removal of the authorized client.
* The default OAuth 2.0 error codes that will trigger removal of the authorized
* client.
* @see OAuth2ErrorCodes
*/
public static final Set<String> DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
/*
* Returned from resource servers when an access token provided is expired, revoked,
* malformed, or invalid for other reasons.
*
* Note that this is needed because the ServerOAuth2AuthorizedClientExchangeFilterFunction
* delegates this type of failure received from a resource server
* to this failure handler.
*/
OAuth2ErrorCodes.INVALID_TOKEN,
/*
* Returned from authorization servers when a refresh token is invalid, expired, revoked,
* does not match the redirection URI used in the authorization request, or was issued to another client.
*/
OAuth2ErrorCodes.INVALID_GRANT)));
public static final Set<String> DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES;
static {
Set<String> codes = new LinkedHashSet<>();
// Returned from resource servers when an access token provided is expired,
// revoked, malformed, or invalid for other reasons. Note that this is needed
// because the ServerOAuth2AuthorizedClientExchangeFilterFunction delegates this
// type of failure received from a resource server to this failure handler.
codes.add(OAuth2ErrorCodes.INVALID_TOKEN);
// Returned from authorization servers when a refresh token is invalid, expired,
// revoked, does not match the redirection URI used in the authorization request,
// or was issued to another client.
codes.add(OAuth2ErrorCodes.INVALID_GRANT);
DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES = Collections.unmodifiableSet(codes);
}
/**
* A delegate that removes an {@link OAuth2AuthorizedClient} from a
* {@link ServerOAuth2AuthorizedClientRepository} or {@link ReactiveOAuth2AuthorizedClientService}
* if the error code is one of the {@link #removeAuthorizedClientErrorCodes}.
* {@link ServerOAuth2AuthorizedClientRepository} or
* {@link ReactiveOAuth2AuthorizedClientService} if the error code is one of the
* {@link #removeAuthorizedClientErrorCodes}.
*/
private final OAuth2AuthorizedClientRemover delegate;
@@ -73,77 +77,85 @@ public class RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler imp
private final Set<String> removeAuthorizedClientErrorCodes;
/**
* Removes an {@link OAuth2AuthorizedClient} from a
* {@link ServerOAuth2AuthorizedClientRepository} or {@link ReactiveOAuth2AuthorizedClientService}.
* Constructs a
* {@code RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} using the
* provided parameters.
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for
* removing an {@link OAuth2AuthorizedClient} if the error code is one of the
* {@link #DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES}.
*/
@FunctionalInterface
public interface OAuth2AuthorizedClientRemover {
/**
* Removes the {@link OAuth2AuthorizedClient} associated to the
* provided client registration identifier and End-User {@link Authentication} (Resource Owner).
*
* @param clientRegistrationId the identifier for the client's registration
* @param principal the End-User {@link Authentication} (Resource Owner)
* @param attributes an immutable {@code Map} of extra optional attributes present under certain conditions.
* For example, this might contain a {@link org.springframework.web.server.ServerWebExchange ServerWebExchange}
* if the authorization was performed within the context of a {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished handling the event.
*/
Mono<Void> removeAuthorizedClient(String clientRegistrationId, Authentication principal, Map<String, Object> attributes);
}
/**
* Constructs a {@code RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} using the provided parameters.
*
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for removing an {@link OAuth2AuthorizedClient}
* if the error code is one of the {@link #DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES}.
*/
public RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(OAuth2AuthorizedClientRemover authorizedClientRemover) {
public RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
OAuth2AuthorizedClientRemover authorizedClientRemover) {
this(authorizedClientRemover, DEFAULT_REMOVE_AUTHORIZED_CLIENT_ERROR_CODES);
}
/**
* Constructs a {@code RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} using the provided parameters.
*
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for removing an {@link OAuth2AuthorizedClient}
* if the error code is one of the {@link #removeAuthorizedClientErrorCodes}.
* @param removeAuthorizedClientErrorCodes the OAuth 2.0 error codes which will trigger removal of an authorized client.
* Constructs a
* {@code RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler} using the
* provided parameters.
* @param authorizedClientRemover the {@link OAuth2AuthorizedClientRemover} used for
* removing an {@link OAuth2AuthorizedClient} if the error code is one of the
* {@link #removeAuthorizedClientErrorCodes}.
* @param removeAuthorizedClientErrorCodes the OAuth 2.0 error codes which will
* trigger removal of an authorized client.
* @see OAuth2ErrorCodes
*/
public RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler(
OAuth2AuthorizedClientRemover authorizedClientRemover,
Set<String> removeAuthorizedClientErrorCodes) {
OAuth2AuthorizedClientRemover authorizedClientRemover, Set<String> removeAuthorizedClientErrorCodes) {
Assert.notNull(authorizedClientRemover, "authorizedClientRemover cannot be null");
Assert.notNull(removeAuthorizedClientErrorCodes, "removeAuthorizedClientErrorCodes cannot be null");
this.removeAuthorizedClientErrorCodes = Collections.unmodifiableSet(new HashSet<>(removeAuthorizedClientErrorCodes));
this.removeAuthorizedClientErrorCodes = Collections
.unmodifiableSet(new HashSet<>(removeAuthorizedClientErrorCodes));
this.delegate = authorizedClientRemover;
}
@Override
public Mono<Void> onAuthorizationFailure(OAuth2AuthorizationException authorizationException,
Authentication principal, Map<String, Object> attributes) {
if (authorizationException instanceof ClientAuthorizationException
&& hasRemovalErrorCode(authorizationException)) {
ClientAuthorizationException clientAuthorizationException = (ClientAuthorizationException) authorizationException;
return this.delegate.removeAuthorizedClient(
clientAuthorizationException.getClientRegistrationId(), principal, attributes);
} else {
return Mono.empty();
return this.delegate.removeAuthorizedClient(clientAuthorizationException.getClientRegistrationId(),
principal, attributes);
}
return Mono.empty();
}
/**
* Returns true if the given exception has an error code that
* indicates that the authorized client should be removed.
*
* Returns true if the given exception has an error code that indicates that the
* authorized client should be removed.
* @param authorizationException the exception that caused the authorization failure
* @return true if the given exception has an error code that
* indicates that the authorized client should be removed.
* @return true if the given exception has an error code that indicates that the
* authorized client should be removed.
*/
private boolean hasRemovalErrorCode(OAuth2AuthorizationException authorizationException) {
return this.removeAuthorizedClientErrorCodes.contains(authorizationException.getError().getErrorCode());
}
/**
* Removes an {@link OAuth2AuthorizedClient} from a
* {@link ServerOAuth2AuthorizedClientRepository} or
* {@link ReactiveOAuth2AuthorizedClientService}.
*/
@FunctionalInterface
public interface OAuth2AuthorizedClientRemover {
/**
* Removes the {@link OAuth2AuthorizedClient} associated to the provided client
* registration identifier and End-User {@link Authentication} (Resource Owner).
* @param clientRegistrationId the identifier for the client's registration
* @param principal the End-User {@link Authentication} (Resource Owner)
* @param attributes an immutable {@code Map} of extra optional attributes present
* under certain conditions. For example, this might contain a
* {@link org.springframework.web.server.ServerWebExchange ServerWebExchange} if
* the authorization was performed within the context of a
* {@code ServerWebExchange}.
* @return an empty {@link Mono} that completes after this handler has finished
* handling the event.
*/
Mono<Void> removeAuthorizedClient(String clientRegistrationId, Authentication principal,
Map<String, Object> attributes);
}
}

View File

@@ -13,11 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.annotation;
import org.springframework.core.annotation.AliasFor;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver;
package org.springframework.security.oauth2.client.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
@@ -25,13 +22,16 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.web.method.annotation.OAuth2AuthorizedClientArgumentResolver;
/**
* This annotation may be used to resolve a method parameter
* to an argument value of type {@link OAuth2AuthorizedClient}.
* This annotation may be used to resolve a method parameter to an argument value of type
* {@link OAuth2AuthorizedClient}.
*
* <p>
* For example:
* <pre>
* For example: <pre>
* &#64;Controller
* public class MyController {
* &#64;GetMapping("/authorized-client")
@@ -52,18 +52,16 @@ public @interface RegisteredOAuth2AuthorizedClient {
/**
* Sets the client registration identifier.
*
* @return the client registration identifier
*/
@AliasFor("value")
String registrationId() default "";
/**
* The default attribute for this annotation.
* This is an alias for {@link #registrationId()}.
* For example, {@code @RegisteredOAuth2AuthorizedClient("login-client")} is equivalent to
* The default attribute for this annotation. This is an alias for
* {@link #registrationId()}. For example,
* {@code @RegisteredOAuth2AuthorizedClient("login-client")} is equivalent to
* {@code @RegisteredOAuth2AuthorizedClient(registrationId="login-client")}.
*
* @return the client registration identifier
*/
@AliasFor("registrationId")

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -23,16 +26,14 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import java.util.Collection;
/**
* An implementation of an {@link AbstractAuthenticationToken}
* that represents an OAuth 2.0 {@link Authentication}.
* An implementation of an {@link AbstractAuthenticationToken} that represents an OAuth
* 2.0 {@link Authentication}.
* <p>
* The {@link Authentication} associates an {@link OAuth2User} {@code Principal}
* to the identifier of the {@link #getAuthorizedClientRegistrationId() Authorized Client},
* which the End-User ({@code Principal}) granted authorization to
* so that it can access it's protected resources at the UserInfo Endpoint.
* The {@link Authentication} associates an {@link OAuth2User} {@code Principal} to the
* identifier of the {@link #getAuthorizedClientRegistrationId() Authorized Client}, which
* the End-User ({@code Principal}) granted authorization to so that it can access it's
* protected resources at the UserInfo Endpoint.
*
* @author Joe Grandja
* @since 5.0
@@ -41,20 +42,22 @@ import java.util.Collection;
* @see OAuth2AuthorizedClient
*/
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final OAuth2User principal;
private final String authorizedClientRegistrationId;
/**
* Constructs an {@code OAuth2AuthenticationToken} using the provided parameters.
*
* @param principal the user {@code Principal} registered with the OAuth 2.0 Provider
* @param authorities the authorities granted to the user
* @param authorizedClientRegistrationId the registration identifier of the {@link OAuth2AuthorizedClient Authorized Client}
* @param authorizedClientRegistrationId the registration identifier of the
* {@link OAuth2AuthorizedClient Authorized Client}
*/
public OAuth2AuthenticationToken(OAuth2User principal,
Collection<? extends GrantedAuthority> authorities,
String authorizedClientRegistrationId) {
public OAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
String authorizedClientRegistrationId) {
super(authorities);
Assert.notNull(principal, "principal cannot be null");
Assert.hasText(authorizedClientRegistrationId, "authorizedClientRegistrationId cannot be empty");
@@ -75,11 +78,12 @@ public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
}
/**
* Returns the registration identifier of the {@link OAuth2AuthorizedClient Authorized Client}.
*
* Returns the registration identifier of the {@link OAuth2AuthorizedClient Authorized
* Client}.
* @return the registration identifier of the Authorized Client.
*/
public String getAuthorizedClientRegistrationId() {
return this.authorizedClientRegistrationId;
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import org.springframework.security.authentication.AuthenticationProvider;
@@ -28,70 +29,67 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResp
import org.springframework.util.Assert;
/**
* An implementation of an {@link AuthenticationProvider} for the OAuth 2.0 Authorization Code Grant.
* An implementation of an {@link AuthenticationProvider} for the OAuth 2.0 Authorization
* Code Grant.
*
* <p>
* This {@link AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
* This {@link AuthenticationProvider} is responsible for authenticating an Authorization
* Code credential with the Authorization Server's Token Endpoint and if valid, exchanging
* it for an Access Token credential.
*
* @author Joe Grandja
* @since 5.1
* @see OAuth2AuthorizationCodeAuthenticationToken
* @see OAuth2AccessTokenResponseClient
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token
* Request</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token
* Response</a>
*/
public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
/**
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
*
* @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint
* Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the
* provided parameters.
* @param accessTokenResponseClient the client used for requesting the access token
* credential from the Token Endpoint
*/
public OAuth2AuthorizationCodeAuthenticationProvider(
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
(OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationResponse();
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthorizationException(authorizationResponse.getError());
}
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationRequest();
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest();
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthorizationException(oauth2Error);
}
OAuth2AccessTokenResponse accessTokenResponse =
this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
OAuth2AuthorizationCodeAuthenticationToken authenticationResult =
new OAuth2AuthorizationCodeAuthenticationToken(
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken(),
accessTokenResponse.getAdditionalParameters());
authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken(), accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
@@ -99,4 +97,5 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica
public boolean supports(Class<?> authentication) {
return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.SpringSecurityCoreVersion;
@@ -24,10 +29,6 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* An {@link AbstractAuthenticationToken} for the OAuth 2.0 Authorization Code Grant.
*
@@ -37,24 +38,31 @@ import java.util.Map;
* @see ClientRegistration
* @see OAuth2AuthorizationExchange
* @see OAuth2AccessToken
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
*/
public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private Map<String, Object> additionalParameters = new HashMap<>();
private ClientRegistration clientRegistration;
private OAuth2AuthorizationExchange authorizationExchange;
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
/**
* This constructor should be used when the Authorization Request/Response is complete.
*
* This constructor should be used when the Authorization Request/Response is
* complete.
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
*/
public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange) {
OAuth2AuthorizationExchange authorizationExchange) {
super(Collections.emptyList());
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.notNull(authorizationExchange, "authorizationExchange cannot be null");
@@ -65,35 +73,32 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
/**
* This constructor should be used when the Access Token Request/Response is complete,
* which indicates that the Authorization Code Grant flow has fully completed.
*
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
* @param accessToken the access token credential
*/
public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange,
OAuth2AccessToken accessToken) {
OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken) {
this(clientRegistration, authorizationExchange, accessToken, null);
}
/**
* This constructor should be used when the Access Token Request/Response is complete,
* which indicates that the Authorization Code Grant flow has fully completed.
*
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
* @param accessToken the access token credential
* @param refreshToken the refresh token credential
*/
public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange,
OAuth2AccessToken accessToken,
@Nullable OAuth2RefreshToken refreshToken) {
OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken,
@Nullable OAuth2RefreshToken refreshToken) {
this(clientRegistration, authorizationExchange, accessToken, refreshToken, Collections.emptyMap());
}
public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration, OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken, OAuth2RefreshToken refreshToken,
Map<String, Object> additionalParameters) {
public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange, OAuth2AccessToken accessToken,
OAuth2RefreshToken refreshToken, Map<String, Object> additionalParameters) {
this(clientRegistration, authorizationExchange);
Assert.notNull(accessToken, "accessToken cannot be null");
this.accessToken = accessToken;
@@ -109,14 +114,12 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
@Override
public Object getCredentials() {
return this.accessToken != null ?
this.accessToken.getTokenValue() :
this.authorizationExchange.getAuthorizationResponse().getCode();
return (this.accessToken != null) ? this.accessToken.getTokenValue()
: this.authorizationExchange.getAuthorizationResponse().getCode();
}
/**
* Returns the {@link ClientRegistration client registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -125,7 +128,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
/**
* Returns the {@link OAuth2AuthorizationExchange authorization exchange}.
*
* @return the {@link OAuth2AuthorizationExchange}
*/
public OAuth2AuthorizationExchange getAuthorizationExchange() {
@@ -134,7 +136,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
/**
* Returns the {@link OAuth2AccessToken access token}.
*
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
@@ -143,7 +144,6 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
/**
* Returns the {@link OAuth2RefreshToken refresh token}.
*
* @return the {@link OAuth2RefreshToken}
*/
public @Nullable OAuth2RefreshToken getRefreshToken() {
@@ -152,10 +152,10 @@ public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenti
/**
* Returns the additional parameters
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}

View File

@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
@@ -31,23 +36,22 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.util.function.Function;
/**
* An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
* An implementation of an
* {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth
* 2.0 Login, which leverages the OAuth 2.0 Authorization Code Grant Flow.
*
* This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
* This {@link org.springframework.security.authentication.AuthenticationProvider} is
* responsible for authenticating an Authorization Code credential with the Authorization
* Server's Token Endpoint and if valid, exchanging it for an Access Token credential.
* <p>
* It will also obtain the user attributes of the End-User (Resource Owner)
* from the UserInfo Endpoint using an {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService},
* which will create a {@code Principal} in the form of an {@link OAuth2User}.
* The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken}
* to complete the authentication.
* It will also obtain the user attributes of the End-User (Resource Owner) from the
* UserInfo Endpoint using an
* {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService}, which
* will create a {@code Principal} in the form of an {@link OAuth2User}. The
* {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken} to
* complete the authentication.
*
* @author Rob Winch
* @since 5.1
@@ -55,12 +59,19 @@ import java.util.function.Function;
* @see ReactiveOAuth2AccessTokenResponseClient
* @see ReactiveOAuth2UserService
* @see OAuth2User
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token
* Request</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token
* Response</a>
*/
public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
public OAuth2AuthorizationCodeReactiveAuthenticationManager(
@@ -73,34 +84,33 @@ public class OAuth2AuthorizationCodeReactiveAuthenticationManager implements Rea
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.defer(() -> {
OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange().getAuthorizationResponse();
OAuth2AuthorizationResponse authorizationResponse = token.getAuthorizationExchange()
.getAuthorizationResponse();
if (authorizationResponse.statusError()) {
return Mono.error(new OAuth2AuthorizationException(authorizationResponse.getError()));
}
OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange().getAuthorizationRequest();
OAuth2AuthorizationRequest authorizationRequest = token.getAuthorizationExchange()
.getAuthorizationRequest();
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
return Mono.error(new OAuth2AuthorizationException(oauth2Error));
}
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
token.getClientRegistration(),
token.getAuthorizationExchange());
return this.accessTokenResponseClient.getTokenResponse(authzRequest)
.map(onSuccess(token));
token.getClientRegistration(), token.getAuthorizationExchange());
return this.accessTokenResponseClient.getTokenResponse(authzRequest).map(onSuccess(token));
});
}
private Function<OAuth2AccessTokenResponse, OAuth2AuthorizationCodeAuthenticationToken> onSuccess(OAuth2AuthorizationCodeAuthenticationToken token) {
return accessTokenResponse -> {
private Function<OAuth2AccessTokenResponse, OAuth2AuthorizationCodeAuthenticationToken> onSuccess(
OAuth2AuthorizationCodeAuthenticationToken token) {
return (accessTokenResponse) -> {
ClientRegistration registration = token.getClientRegistration();
OAuth2AuthorizationExchange exchange = token.getAuthorizationExchange();
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenResponse.getRefreshToken();
return new OAuth2AuthorizationCodeAuthenticationToken(registration, exchange, accessToken, refreshToken, accessTokenResponse.getAdditionalParameters());
return new OAuth2AuthorizationCodeAuthenticationToken(registration, exchange, accessToken, refreshToken,
accessTokenResponse.getAdditionalParameters());
};
}
}

View File

@@ -13,8 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -31,22 +35,19 @@ import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.Map;
/**
* An implementation of an {@link AuthenticationProvider} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
* An implementation of an {@link AuthenticationProvider} for OAuth 2.0 Login, which
* leverages the OAuth 2.0 Authorization Code Grant Flow.
*
* This {@link AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
* This {@link AuthenticationProvider} is responsible for authenticating an Authorization
* Code credential with the Authorization Server's Token Endpoint and if valid, exchanging
* it for an Access Token credential.
* <p>
* It will also obtain the user attributes of the End-User (Resource Owner)
* from the UserInfo Endpoint using an {@link OAuth2UserService},
* which will create a {@code Principal} in the form of an {@link OAuth2User}.
* The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken}
* to complete the authentication.
* It will also obtain the user attributes of the End-User (Resource Owner) from the
* UserInfo Endpoint using an {@link OAuth2UserService}, which will create a
* {@code Principal} in the form of an {@link OAuth2User}. The {@code OAuth2User} is then
* associated to the {@link OAuth2LoginAuthenticationToken} to complete the
* authentication.
*
* @author Joe Grandja
* @since 5.0
@@ -54,82 +55,82 @@ import java.util.Map;
* @see OAuth2AccessTokenResponseClient
* @see OAuth2UserService
* @see OAuth2User
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token
* Request</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token
* Response</a>
*/
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider;
private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);
/**
* Constructs an {@code OAuth2LoginAuthenticationProvider} using the provided parameters.
*
* @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint
* @param userService the service used for obtaining the user attributes of the End-User from the UserInfo Endpoint
* Constructs an {@code OAuth2LoginAuthenticationProvider} using the provided
* parameters.
* @param accessTokenResponseClient the client used for requesting the access token
* credential from the Token Endpoint
* @param userService the service used for obtaining the user attributes of the
* End-User from the UserInfo Endpoint
*/
public OAuth2LoginAuthenticationProvider(
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(userService, "userService cannot be null");
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(
accessTokenResponseClient);
this.userService = userService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken loginAuthenticationToken =
(OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (loginAuthenticationToken.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request -
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains("openid")) {
// This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null;
}
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
}
catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
authorizationCodeAuthenticationToken.getRefreshToken());
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
/**
* Sets the {@link GrantedAuthoritiesMapper} used for mapping {@link OAuth2User#getAuthorities()}
* to a new set of authorities which will be associated to the {@link OAuth2LoginAuthenticationToken}.
*
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities
* Sets the {@link GrantedAuthoritiesMapper} used for mapping
* {@link OAuth2User#getAuthorities()} to a new set of authorities which will be
* associated to the {@link OAuth2LoginAuthenticationToken}.
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
* user's authorities
*/
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
@@ -140,4 +141,5 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
public boolean supports(Class<?> authentication) {
return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -13,8 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import java.util.Collections;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@@ -26,12 +30,9 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.Collections;
/**
* An {@link AbstractAuthenticationToken} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
* An {@link AbstractAuthenticationToken} for OAuth 2.0 Login, which leverages the OAuth
* 2.0 Authorization Code Grant Flow.
*
* @author Joe Grandja
* @since 5.0
@@ -40,25 +41,31 @@ import java.util.Collections;
* @see ClientRegistration
* @see OAuth2AuthorizationExchange
* @see OAuth2AccessToken
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
*/
public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private OAuth2User principal;
private ClientRegistration clientRegistration;
private OAuth2AuthorizationExchange authorizationExchange;
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
/**
* This constructor should be used when the Authorization Request/Response is complete.
*
* This constructor should be used when the Authorization Request/Response is
* complete.
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
*/
public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange) {
OAuth2AuthorizationExchange authorizationExchange) {
super(Collections.emptyList());
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.notNull(authorizationExchange, "authorizationExchange cannot be null");
@@ -69,9 +76,8 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
/**
* This constructor should be used when the Access Token Request/Response is complete,
* which indicates that the Authorization Code Grant flow has fully completed
* and OAuth 2.0 Login has been achieved.
*
* which indicates that the Authorization Code Grant flow has fully completed and
* OAuth 2.0 Login has been achieved.
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
* @param principal the user {@code Principal} registered with the OAuth 2.0 Provider
@@ -79,18 +85,15 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
* @param accessToken the access token credential
*/
public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange,
OAuth2User principal,
Collection<? extends GrantedAuthority> authorities,
OAuth2AccessToken accessToken) {
OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal,
Collection<? extends GrantedAuthority> authorities, OAuth2AccessToken accessToken) {
this(clientRegistration, authorizationExchange, principal, authorities, accessToken, null);
}
/**
* This constructor should be used when the Access Token Request/Response is complete,
* which indicates that the Authorization Code Grant flow has fully completed
* and OAuth 2.0 Login has been achieved.
*
* which indicates that the Authorization Code Grant flow has fully completed and
* OAuth 2.0 Login has been achieved.
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
* @param principal the user {@code Principal} registered with the OAuth 2.0 Provider
@@ -99,11 +102,9 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
* @param refreshToken the refresh token credential
*/
public OAuth2LoginAuthenticationToken(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange,
OAuth2User principal,
Collection<? extends GrantedAuthority> authorities,
OAuth2AccessToken accessToken,
@Nullable OAuth2RefreshToken refreshToken) {
OAuth2AuthorizationExchange authorizationExchange, OAuth2User principal,
Collection<? extends GrantedAuthority> authorities, OAuth2AccessToken accessToken,
@Nullable OAuth2RefreshToken refreshToken) {
super(authorities);
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.notNull(authorizationExchange, "authorizationExchange cannot be null");
@@ -129,7 +130,6 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
/**
* Returns the {@link ClientRegistration client registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -138,7 +138,6 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
/**
* Returns the {@link OAuth2AuthorizationExchange authorization exchange}.
*
* @return the {@link OAuth2AuthorizationExchange}
*/
public OAuth2AuthorizationExchange getAuthorizationExchange() {
@@ -147,7 +146,6 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
/**
* Returns the {@link OAuth2AccessToken access token}.
*
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
@@ -156,11 +154,11 @@ public class OAuth2LoginAuthenticationToken extends AbstractAuthenticationToken
/**
* Returns the {@link OAuth2RefreshToken refresh token}.
*
* @since 5.1
* @return the {@link OAuth2RefreshToken}
* @since 5.1
*/
public @Nullable OAuth2RefreshToken getRefreshToken() {
return this.refreshToken;
}
}

View File

@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.authentication;
import java.util.Collection;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -28,24 +34,22 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.util.Collection;
import java.util.Map;
/**
* An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
* An implementation of an
* {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth
* 2.0 Login, which leverages the OAuth 2.0 Authorization Code Grant Flow.
*
* This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
* This {@link org.springframework.security.authentication.AuthenticationProvider} is
* responsible for authenticating an Authorization Code credential with the Authorization
* Server's Token Endpoint and if valid, exchanging it for an Access Token credential.
* <p>
* It will also obtain the user attributes of the End-User (Resource Owner)
* from the UserInfo Endpoint using an {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService},
* which will create a {@code Principal} in the form of an {@link OAuth2User}.
* The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken}
* to complete the authentication.
* It will also obtain the user attributes of the End-User (Resource Owner) from the
* UserInfo Endpoint using an
* {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService}, which
* will create a {@code Principal} in the form of an {@link OAuth2User}. The
* {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken} to
* complete the authentication.
*
* @author Rob Winch
* @since 5.1
@@ -53,24 +57,30 @@ import java.util.Map;
* @see org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient
* @see org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService
* @see OAuth2User
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token
* Request</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token
* Response</a>
*/
public class OAuth2LoginReactiveAuthenticationManager implements
ReactiveAuthenticationManager {
public class OAuth2LoginReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private final ReactiveAuthenticationManager authorizationCodeManager;
private final ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);
public OAuth2LoginReactiveAuthenticationManager(
ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
Assert.notNull(userService, "userService cannot be null");
this.authorizationCodeManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(accessTokenResponseClient);
this.authorizationCodeManager = new OAuth2AuthorizationCodeReactiveAuthenticationManager(
accessTokenResponseClient);
this.userService = userService;
}
@@ -78,29 +88,29 @@ public class OAuth2LoginReactiveAuthenticationManager implements
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.defer(() -> {
OAuth2AuthorizationCodeAuthenticationToken token = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (token.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
// Section 3.1.2.1 Authentication Request -
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (token.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {
// This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeReactiveAuthenticationManager handle it instead once one is created
// and let OidcAuthorizationCodeReactiveAuthenticationManager handle it
// instead once one is created
return Mono.empty();
}
return this.authorizationCodeManager.authenticate(token)
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()))
.cast(OAuth2AuthorizationCodeAuthenticationToken.class)
.flatMap(this::onSuccess);
.onErrorMap(OAuth2AuthorizationException.class,
(e) -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()))
.cast(OAuth2AuthorizationCodeAuthenticationToken.class).flatMap(this::onSuccess);
});
}
/**
* Sets the {@link GrantedAuthoritiesMapper} used for mapping {@link OAuth2User#getAuthorities()}
* to a new set of authorities which will be associated to the {@link OAuth2LoginAuthenticationToken}.
*
* Sets the {@link GrantedAuthoritiesMapper} used for mapping
* {@link OAuth2User#getAuthorities()} to a new set of authorities which will be
* associated to the {@link OAuth2LoginAuthenticationToken}.
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
* user's authorities
* @since 5.4
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities
*/
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
@@ -110,20 +120,16 @@ public class OAuth2LoginReactiveAuthenticationManager implements
private Mono<OAuth2LoginAuthenticationToken> onSuccess(OAuth2AuthorizationCodeAuthenticationToken authentication) {
OAuth2AccessToken accessToken = authentication.getAccessToken();
Map<String, Object> additionalParameters = authentication.getAdditionalParameters();
OAuth2UserRequest userRequest = new OAuth2UserRequest(authentication.getClientRegistration(), accessToken, additionalParameters);
return this.userService.loadUser(userRequest)
.map(oauth2User -> {
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authentication.getClientRegistration(),
authentication.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
authentication.getRefreshToken());
return authenticationResult;
});
OAuth2UserRequest userRequest = new OAuth2UserRequest(authentication.getClientRegistration(), accessToken,
additionalParameters);
return this.userService.loadUser(userRequest).map((oauth2User) -> {
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authentication.getClientRegistration(), authentication.getAuthorizationExchange(), oauth2User,
mappedAuthorities, accessToken, authentication.getRefreshToken());
return authenticationResult;
});
}
}

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support classes and interfaces for authenticating and authorizing a client
* with an OAuth 2.0 Authorization Server using a specific authorization grant flow.
* Support classes and interfaces for authenticating and authorizing a client with an
* OAuth 2.0 Authorization Server using a specific authorization grant flow.
*/
package org.springframework.security.oauth2.client.authentication;

View File

@@ -13,27 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* Base implementation of an OAuth 2.0 Authorization Grant request
* that holds an authorization grant credential and is used when
* initiating a request to the Authorization Server's Token Endpoint.
* Base implementation of an OAuth 2.0 Authorization Grant request that holds an
* authorization grant credential and is used when initiating a request to the
* Authorization Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.0
* @see AuthorizationGrantType
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
* 1.3 Authorization Grant</a>
*/
public abstract class AbstractOAuth2AuthorizationGrantRequest {
private final AuthorizationGrantType authorizationGrantType;
/**
* Sub-class constructor.
*
* @param authorizationGrantType the authorization grant type
*/
protected AbstractOAuth2AuthorizationGrantRequest(AuthorizationGrantType authorizationGrantType) {
@@ -43,10 +45,10 @@ public abstract class AbstractOAuth2AuthorizationGrantRequest {
/**
* Returns the authorization grant type.
*
* @return the authorization grant type
*/
public AuthorizationGrantType getGrantType() {
return this.authorizationGrantType;
}
}

View File

@@ -13,63 +13,74 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Collections;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.Set;
import static org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse;
/**
* Abstract base class for all of the {@code WebClientReactive*TokenResponseClient}s
* that communicate to the Authorization Server's Token Endpoint.
* Abstract base class for all of the {@code WebClientReactive*TokenResponseClient}s that
* communicate to the Authorization Server's Token Endpoint.
*
* <p>Submits a form request body specific to the type of grant request.</p>
* <p>
* Submits a form request body specific to the type of grant request.
* </p>
*
* <p>Accepts a JSON response body containing an OAuth 2.0 Access token or error.</p>
* <p>
* Accepts a JSON response body containing an OAuth 2.0 Access token or error.
* </p>
*
* @param <T> type of grant request
* @author Phil Clay
* @since 5.3
* @param <T> type of grant request
* @see <a href="https://tools.ietf.org/html/rfc6749#section-3.2">RFC-6749 Token Endpoint</a>
* @see <a href="https://tools.ietf.org/html/rfc6749#section-3.2">RFC-6749 Token
* Endpoint</a>
* @see WebClientReactiveAuthorizationCodeTokenResponseClient
* @see WebClientReactiveClientCredentialsTokenResponseClient
* @see WebClientReactivePasswordTokenResponseClient
* @see WebClientReactiveRefreshTokenTokenResponseClient
*/
abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
implements ReactiveOAuth2AccessTokenResponseClient<T> {
private WebClient webClient = WebClient.builder().build();
AbstractWebClientReactiveOAuth2AccessTokenResponseClient() {
}
@Override
public Mono<OAuth2AccessTokenResponse> getTokenResponse(T grantRequest) {
Assert.notNull(grantRequest, "grantRequest cannot be null");
// @formatter:off
return Mono.defer(() -> this.webClient.post()
.uri(clientRegistration(grantRequest).getProviderDetails().getTokenUri())
.headers(headers -> populateTokenRequestHeaders(grantRequest, headers))
.headers((headers) -> populateTokenRequestHeaders(grantRequest, headers))
.body(createTokenRequestBody(grantRequest))
.exchange()
.flatMap(response -> readTokenResponse(grantRequest, response)));
.flatMap((response) -> readTokenResponse(grantRequest, response))
);
// @formatter:on
}
/**
* Returns the {@link ClientRegistration} for the given {@code grantRequest}.
*
* @param grantRequest the grant request
* @return the {@link ClientRegistration} for the given {@code grantRequest}.
*/
@@ -77,7 +88,6 @@ abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extend
/**
* Populates the headers for the token request.
*
* @param grantRequest the grant request
* @param headers the headers to populate
*/
@@ -93,30 +103,34 @@ abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extend
/**
* Creates and returns the body for the token request.
*
* <p>This method pre-populates the body with some standard properties,
* and then delegates to {@link #populateTokenRequestBody(AbstractOAuth2AuthorizationGrantRequest, BodyInserters.FormInserter)}
* for subclasses to further populate the body before returning.</p>
*
* <p>
* This method pre-populates the body with some standard properties, and then
* delegates to
* {@link #populateTokenRequestBody(AbstractOAuth2AuthorizationGrantRequest, BodyInserters.FormInserter)}
* for subclasses to further populate the body before returning.
* </p>
* @param grantRequest the grant request
* @return the body for the token request.
*/
private BodyInserters.FormInserter<String> createTokenRequestBody(T grantRequest) {
BodyInserters.FormInserter<String> body = BodyInserters
.fromFormData(OAuth2ParameterNames.GRANT_TYPE, grantRequest.getGrantType().getValue());
BodyInserters.FormInserter<String> body = BodyInserters.fromFormData(OAuth2ParameterNames.GRANT_TYPE,
grantRequest.getGrantType().getValue());
return populateTokenRequestBody(grantRequest, body);
}
/**
* Populates the body of the token request.
*
* <p>By default, populates properties that are common to all grant types.
* Subclasses can extend this method to populate grant type specific properties.</p>
*
* <p>
* By default, populates properties that are common to all grant types. Subclasses can
* extend this method to populate grant type specific properties.
* </p>
* @param grantRequest the grant request
* @param body the body to populate
* @return the populated body
*/
BodyInserters.FormInserter<String> populateTokenRequestBody(T grantRequest, BodyInserters.FormInserter<String> body) {
BodyInserters.FormInserter<String> populateTokenRequestBody(T grantRequest,
BodyInserters.FormInserter<String> body) {
ClientRegistration clientRegistration = clientRegistration(grantRequest);
if (!ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
body.with(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
@@ -126,31 +140,30 @@ abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extend
}
Set<String> scopes = scopes(grantRequest);
if (!CollectionUtils.isEmpty(scopes)) {
body.with(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(scopes, " "));
body.with(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString(scopes, " "));
}
return body;
}
/**
* Returns the scopes to include as a property in the token request.
*
* @param grantRequest the grant request
* @return the scopes to include as a property in the token request.
*/
abstract Set<String> scopes(T grantRequest);
/**
* Returns the scopes to include in the response if the authorization
* server returned no scopes in the response.
*
* <p>As per <a href="https://tools.ietf.org/html/rfc6749#section-5.1">RFC-6749 Section 5.1 Successful Access Token Response</a>,
* if AccessTokenResponse.scope is empty, then default to the scope
* originally requested by the client in the Token Request.</p>
* Returns the scopes to include in the response if the authorization server returned
* no scopes in the response.
*
* <p>
* As per <a href="https://tools.ietf.org/html/rfc6749#section-5.1">RFC-6749 Section
* 5.1 Successful Access Token Response</a>, if AccessTokenResponse.scope is empty,
* then default to the scope originally requested by the client in the Token Request.
* </p>
* @param grantRequest the grant request
* @return the scopes to include in the response if the authorization
* server returned no scopes.
* @return the scopes to include in the response if the authorization server returned
* no scopes.
*/
Set<String> defaultScopes(T grantRequest) {
return scopes(grantRequest);
@@ -158,41 +171,45 @@ abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T extend
/**
* Reads the token response from the response body.
*
* @param grantRequest the request for which the response was received.
* @param response the client response from which to read
* @return the token response from the response body.
*/
private Mono<OAuth2AccessTokenResponse> readTokenResponse(T grantRequest, ClientResponse response) {
return response.body(oauth2AccessTokenResponse())
.map(tokenResponse -> populateTokenResponse(grantRequest, tokenResponse));
return response.body(OAuth2BodyExtractors.oauth2AccessTokenResponse())
.map((tokenResponse) -> populateTokenResponse(grantRequest, tokenResponse));
}
/**
* Populates the given {@link OAuth2AccessTokenResponse} with additional details
* from the grant request.
*
* Populates the given {@link OAuth2AccessTokenResponse} with additional details from
* the grant request.
* @param grantRequest the request for which the response was received.
* @param tokenResponse the original token response
* @return a token response optionally populated with additional details from the request.
* @return a token response optionally populated with additional details from the
* request.
*/
OAuth2AccessTokenResponse populateTokenResponse(T grantRequest, OAuth2AccessTokenResponse tokenResponse) {
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
Set<String> defaultScopes = defaultScopes(grantRequest);
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
// @formatter:off
tokenResponse = OAuth2AccessTokenResponse
.withResponse(tokenResponse)
.scopes(defaultScopes)
.build();
// @formatter:on
}
return tokenResponse;
}
/**
* Sets the {@link WebClient} used when requesting the OAuth 2.0 Access Token Response.
*
* @param webClient the {@link WebClient} used when requesting the Access Token Response
* Sets the {@link WebClient} used when requesting the OAuth 2.0 Access Token
* Response.
* @param webClient the {@link WebClient} used when requesting the Access Token
* Response
*/
public void setWebClient(WebClient webClient) {
Assert.notNull(webClient, "webClient cannot be null");
this.webClient = webClient;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Arrays;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -33,92 +36,105 @@ import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
/**
* The default implementation of an {@link OAuth2AccessTokenResponseClient}
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
* This implementation uses a {@link RestOperations} when requesting
* an access token credential at the Authorization Server's Token Endpoint.
* The default implementation of an {@link OAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant. This
* implementation uses a {@link RestOperations} when requesting an access token credential
* at the Authorization Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.1
* @see OAuth2AccessTokenResponseClient
* @see OAuth2AuthorizationCodeGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
* (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
* (Authorization Code Grant)</a>
*/
public final class DefaultAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
public final class DefaultAuthorizationCodeTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter =
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
private RestOperations restOperations;
public DefaultAuthorizationCodeTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response;
try {
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
OAuth2AccessTokenResponse tokenResponse = response.getBody();
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
// As per spec, in Section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
// If AccessTokenResponse.scope is empty, then default to the scope
// originally requested by the client in the Token Request
// @formatter:off
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
.scopes(authorizationCodeGrantRequest.getClientRegistration().getScopes())
.build();
// @formatter:on
}
return tokenResponse;
}
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
}
catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
/**
* Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationCodeGrantRequest}
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
*
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
* Sets the {@link Converter} used for converting the
* {@link OAuth2AuthorizationCodeGrantRequest} to a {@link RequestEntity}
* representation of the OAuth 2.0 Access Token Request.
* @param requestEntityConverter the {@link Converter} used for converting to a
* {@link RequestEntity} representation of the Access Token Request
*/
public void setRequestEntityConverter(Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter) {
public void setRequestEntityConverter(
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token
* Response.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured
* with the following:
* <ol>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and
* {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
* @param restOperations the {@link RestOperations} used when requesting the Access
* Token Response
*/
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Arrays;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -33,92 +36,105 @@ import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
/**
* The default implementation of an {@link OAuth2AccessTokenResponseClient}
* for the {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant.
* This implementation uses a {@link RestOperations} when requesting
* an access token credential at the Authorization Server's Token Endpoint.
* The default implementation of an {@link OAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#CLIENT_CREDENTIALS client_credentials} grant. This
* implementation uses a {@link RestOperations} when requesting an access token credential
* at the Authorization Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.1
* @see OAuth2AccessTokenResponseClient
* @see OAuth2ClientCredentialsGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4.2">Section 4.4.2 Access Token Request (Client Credentials Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.4.3">Section 4.4.3 Access Token Response (Client Credentials Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.4.2">Section 4.4.2 Access Token Request
* (Client Credentials Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.4.3">Section 4.4.3 Access Token Response
* (Client Credentials Grant)</a>
*/
public final class DefaultClientCredentialsTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
public final class DefaultClientCredentialsTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
private Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
private Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2ClientCredentialsGrantRequestEntityConverter();
private RestOperations restOperations;
public DefaultClientCredentialsTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
public OAuth2AccessTokenResponse getTokenResponse(
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
Assert.notNull(clientCredentialsGrantRequest, "clientCredentialsGrantRequest cannot be null");
RequestEntity<?> request = this.requestEntityConverter.convert(clientCredentialsGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response;
try {
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
OAuth2AccessTokenResponse tokenResponse = response.getBody();
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
// As per spec, in Section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
// If AccessTokenResponse.scope is empty, then default to the scope
// originally requested by the client in the Token Request
// @formatter:off
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
.scopes(clientCredentialsGrantRequest.getClientRegistration().getScopes())
.build();
// @formatter:on
}
return tokenResponse;
}
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
}
catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
/**
* Sets the {@link Converter} used for converting the {@link OAuth2ClientCredentialsGrantRequest}
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
*
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
* Sets the {@link Converter} used for converting the
* {@link OAuth2ClientCredentialsGrantRequest} to a {@link RequestEntity}
* representation of the OAuth 2.0 Access Token Request.
* @param requestEntityConverter the {@link Converter} used for converting to a
* {@link RequestEntity} representation of the Access Token Request
*/
public void setRequestEntityConverter(Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> requestEntityConverter) {
public void setRequestEntityConverter(
Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token
* Response.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured
* with the following:
* <ol>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and
* {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
* @param restOperations the {@link RestOperations} used when requesting the Access
* Token Response
*/
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Arrays;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -33,33 +36,36 @@ import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
/**
* The default implementation of an {@link OAuth2AccessTokenResponseClient}
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
* This implementation uses a {@link RestOperations} when requesting
* an access token credential at the Authorization Server's Token Endpoint.
* The default implementation of an {@link OAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#PASSWORD password} grant. This implementation uses a
* {@link RestOperations} when requesting an access token credential at the Authorization
* Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AccessTokenResponseClient
* @see OAuth2PasswordGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request
* (Resource Owner Password Credentials Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response
* (Resource Owner Password Credentials Grant)</a>
*/
public final class DefaultPasswordTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
public final class DefaultPasswordTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
private Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter =
new OAuth2PasswordGrantRequestEntityConverter();
private Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2PasswordGrantRequestEntityConverter();
private RestOperations restOperations;
public DefaultPasswordTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
@@ -67,58 +73,64 @@ public final class DefaultPasswordTokenResponseClient implements OAuth2AccessTok
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2PasswordGrantRequest passwordGrantRequest) {
Assert.notNull(passwordGrantRequest, "passwordGrantRequest cannot be null");
RequestEntity<?> request = this.requestEntityConverter.convert(passwordGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response;
try {
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
OAuth2AccessTokenResponse tokenResponse = response.getBody();
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
// As per spec, in Section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
// If AccessTokenResponse.scope is empty, then default to the scope
// originally requested by the client in the Token Request
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
.scopes(passwordGrantRequest.getClientRegistration().getScopes())
.build();
.scopes(passwordGrantRequest.getClientRegistration().getScopes()).build();
}
return tokenResponse;
}
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
}
catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
/**
* Sets the {@link Converter} used for converting the {@link OAuth2PasswordGrantRequest}
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
*
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
* Sets the {@link Converter} used for converting the
* {@link OAuth2PasswordGrantRequest} to a {@link RequestEntity} representation of the
* OAuth 2.0 Access Token Request.
* @param requestEntityConverter the {@link Converter} used for converting to a
* {@link RequestEntity} representation of the Access Token Request
*/
public void setRequestEntityConverter(Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter) {
public void setRequestEntityConverter(
Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token
* Response.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured
* with the following:
* <ol>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and
* {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
* @param restOperations the {@link RestOperations} used when requesting the Access
* Token Response
*/
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Arrays;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
@@ -33,32 +36,32 @@ import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
/**
* The default implementation of an {@link OAuth2AccessTokenResponseClient}
* for the {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
* This implementation uses a {@link RestOperations} when requesting
* an access token credential at the Authorization Server's Token Endpoint.
* The default implementation of an {@link OAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant. This implementation
* uses a {@link RestOperations} when requesting an access token credential at the
* Authorization Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.2
* @see OAuth2AccessTokenResponseClient
* @see OAuth2RefreshTokenGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6
* Refreshing an Access Token</a>
*/
public final class DefaultRefreshTokenTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
public final class DefaultRefreshTokenTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
private Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter =
new OAuth2RefreshTokenGrantRequestEntityConverter();
private Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2RefreshTokenGrantRequestEntityConverter();
private RestOperations restOperations;
public DefaultRefreshTokenTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
RestTemplate restTemplate = new RestTemplate(
Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
@@ -66,24 +69,13 @@ public final class DefaultRefreshTokenTokenResponseClient implements OAuth2Acces
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
Assert.notNull(refreshTokenGrantRequest, "refreshTokenGrantRequest cannot be null");
RequestEntity<?> request = this.requestEntityConverter.convert(refreshTokenGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response;
try {
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
OAuth2AccessTokenResponse tokenResponse = response.getBody();
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes()) ||
tokenResponse.getRefreshToken() == null) {
OAuth2AccessTokenResponse.Builder tokenResponseBuilder = OAuth2AccessTokenResponse.withResponse(tokenResponse);
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())
|| tokenResponse.getRefreshToken() == null) {
OAuth2AccessTokenResponse.Builder tokenResponseBuilder = OAuth2AccessTokenResponse
.withResponse(tokenResponse);
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
// As per spec, in Section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
@@ -91,43 +83,59 @@ public final class DefaultRefreshTokenTokenResponseClient implements OAuth2Acces
// originally requested by the client in the Token Request
tokenResponseBuilder.scopes(refreshTokenGrantRequest.getAccessToken().getScopes());
}
if (tokenResponse.getRefreshToken() == null) {
// Reuse existing refresh token
tokenResponseBuilder.refreshToken(refreshTokenGrantRequest.getRefreshToken().getTokenValue());
}
tokenResponse = tokenResponseBuilder.build();
}
return tokenResponse;
}
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
}
catch (RestClientException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
/**
* Sets the {@link Converter} used for converting the {@link OAuth2RefreshTokenGrantRequest}
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
*
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
* Sets the {@link Converter} used for converting the
* {@link OAuth2RefreshTokenGrantRequest} to a {@link RequestEntity} representation of
* the OAuth 2.0 Access Token Request.
* @param requestEntityConverter the {@link Converter} used for converting to a
* {@link RequestEntity} representation of the Access Token Request
*/
public void setRequestEntityConverter(Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter) {
public void setRequestEntityConverter(
Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
/**
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token
* Response.
*
* <p>
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured
* with the following:
* <ol>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and
* {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
* </ol>
*
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
* @param restOperations the {@link RestOperations} used when requesting the Access
* Token Response
*/
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}

View File

@@ -13,8 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.io.IOException;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
@@ -30,6 +37,7 @@ import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
import com.nimbusds.oauth2.sdk.auth.Secret;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.id.ClientID;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
@@ -40,16 +48,9 @@ import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.util.CollectionUtils;
import java.io.IOException;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* An implementation of an {@link OAuth2AccessTokenResponseClient} that &quot;exchanges&quot;
* an authorization code credential for an access token credential
* An implementation of an {@link OAuth2AccessTokenResponseClient} that
* &quot;exchanges&quot; an authorization code credential for an access token credential
* at the Authorization Server's Token Endpoint.
*
* <p>
@@ -61,36 +62,76 @@ import java.util.Set;
* @see OAuth2AccessTokenResponseClient
* @see OAuth2AuthorizationCodeGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0
* SDK</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
* (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
* (Authorization Code Grant)</a>
*/
@Deprecated
public class NimbusAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
public class NimbusAuthorizationCodeTokenResponseClient
implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
@Override
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest) {
ClientRegistration clientRegistration = authorizationGrantRequest.getClientRegistration();
// Build the authorization code grant request for the token endpoint
AuthorizationCode authorizationCode = new AuthorizationCode(
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
URI redirectUri = toURI(authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode());
URI redirectUri = toURI(
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getRedirectUri());
AuthorizationGrant authorizationCodeGrant = new AuthorizationCodeGrant(authorizationCode, redirectUri);
URI tokenUri = toURI(clientRegistration.getProviderDetails().getTokenUri());
// Set the credentials to authenticate the client at the token endpoint
ClientID clientId = new ClientID(clientRegistration.getClientId());
Secret clientSecret = new Secret(clientRegistration.getClientSecret());
ClientAuthentication clientAuthentication;
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
clientAuthentication = new ClientSecretPost(clientId, clientSecret);
} else {
clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
boolean isPost = ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod());
ClientAuthentication clientAuthentication = isPost ? new ClientSecretPost(clientId, clientSecret)
: new ClientSecretBasic(clientId, clientSecret);
com.nimbusds.oauth2.sdk.TokenResponse tokenResponse = getTokenResponse(authorizationCodeGrant, tokenUri,
clientAuthentication);
if (!tokenResponse.indicatesSuccess()) {
TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse;
ErrorObject errorObject = tokenErrorResponse.getErrorObject();
throw new OAuth2AuthorizationException(getOAuthError(errorObject));
}
AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse;
String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue();
OAuth2AccessToken.TokenType accessTokenType = null;
if (OAuth2AccessToken.TokenType.BEARER.getValue()
.equalsIgnoreCase(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) {
accessTokenType = OAuth2AccessToken.TokenType.BEARER;
}
long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime();
// As per spec, in section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
// If AccessTokenResponse.scope is empty, then default to the scope
// originally requested by the client in the Authorization Request
Set<String> scopes = getScopes(authorizationGrantRequest, accessTokenResponse);
String refreshToken = null;
if (accessTokenResponse.getTokens().getRefreshToken() != null) {
refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue();
}
Map<String, Object> additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters());
// @formatter:off
return OAuth2AccessTokenResponse.withToken(accessToken)
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.scopes(scopes)
.refreshToken(refreshToken)
.additionalParameters(additionalParameters)
.build();
// @formatter:on
}
com.nimbusds.oauth2.sdk.TokenResponse tokenResponse;
private com.nimbusds.oauth2.sdk.TokenResponse getTokenResponse(AuthorizationGrant authorizationCodeGrant,
URI tokenUri, ClientAuthentication clientAuthentication) {
try {
// Send the Access Token request
TokenRequest tokenRequest = new TokenRequest(tokenUri, clientAuthentication, authorizationCodeGrant);
@@ -98,71 +139,43 @@ public class NimbusAuthorizationCodeTokenResponseClient implements OAuth2AccessT
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
httpRequest.setConnectTimeout(30000);
httpRequest.setReadTimeout(30000);
tokenResponse = com.nimbusds.oauth2.sdk.TokenResponse.parse(httpRequest.send());
} catch (ParseException | IOException ex) {
return com.nimbusds.oauth2.sdk.TokenResponse.parse(httpRequest.send());
}
catch (ParseException | IOException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
+ ex.getMessage(),
null);
throw new OAuth2AuthorizationException(oauth2Error, ex);
}
}
if (!tokenResponse.indicatesSuccess()) {
TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse;
ErrorObject errorObject = tokenErrorResponse.getErrorObject();
OAuth2Error oauth2Error;
if (errorObject == null) {
oauth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR);
} else {
oauth2Error = new OAuth2Error(
errorObject.getCode() != null ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR,
errorObject.getDescription(),
errorObject.getURI() != null ? errorObject.getURI().toString() : null);
}
throw new OAuth2AuthorizationException(oauth2Error);
}
AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse;
String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue();
OAuth2AccessToken.TokenType accessTokenType = null;
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) {
accessTokenType = OAuth2AccessToken.TokenType.BEARER;
}
long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime();
// As per spec, in section 5.1 Successful Access Token Response
// https://tools.ietf.org/html/rfc6749#section-5.1
// If AccessTokenResponse.scope is empty, then default to the scope
// originally requested by the client in the Authorization Request
Set<String> scopes;
private Set<String> getScopes(OAuth2AuthorizationCodeGrantRequest authorizationGrantRequest,
AccessTokenResponse accessTokenResponse) {
if (CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) {
scopes = new LinkedHashSet<>(
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes());
} else {
scopes = new LinkedHashSet<>(
accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
return new LinkedHashSet<>(
authorizationGrantRequest.getAuthorizationExchange().getAuthorizationRequest().getScopes());
}
return new LinkedHashSet<>(accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
}
String refreshToken = null;
if (accessTokenResponse.getTokens().getRefreshToken() != null) {
refreshToken = accessTokenResponse.getTokens().getRefreshToken().getValue();
private OAuth2Error getOAuthError(ErrorObject errorObject) {
if (errorObject == null) {
return new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR);
}
Map<String, Object> additionalParameters = new LinkedHashMap<>(accessTokenResponse.getCustomParameters());
return OAuth2AccessTokenResponse.withToken(accessToken)
.tokenType(accessTokenType)
.expiresIn(expiresIn)
.scopes(scopes)
.refreshToken(refreshToken)
.additionalParameters(additionalParameters)
.build();
String errorCode = (errorObject.getCode() != null) ? errorObject.getCode() : OAuth2ErrorCodes.SERVER_ERROR;
String description = errorObject.getDescription();
String uri = (errorObject.getURI() != null) ? errorObject.getURI().toString() : null;
return new OAuth2Error(errorCode, description, uri);
}
private static URI toURI(String uriStr) {
try {
return new URI(uriStr);
} catch (Exception ex) {
}
catch (Exception ex) {
throw new IllegalArgumentException("An error occurred parsing URI: " + uriStr, ex);
}
}
}

View File

@@ -13,37 +13,45 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
package org.springframework.security.oauth2.client.endpoint;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
/**
* A strategy for &quot;exchanging&quot; an authorization grant credential
* (e.g. an Authorization Code) for an access token credential
* at the Authorization Server's Token Endpoint.
* A strategy for &quot;exchanging&quot; an authorization grant credential (e.g. an
* Authorization Code) for an access token credential at the Authorization Server's Token
* Endpoint.
*
* @author Joe Grandja
* @since 5.0
* @see AbstractOAuth2AuthorizationGrantRequest
* @see OAuth2AccessTokenResponse
* @see AuthorizationGrantType
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
* 1.3 Authorization Grant</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
* (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
* (Authorization Code Grant)</a>
*/
@FunctionalInterface
public interface OAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest> {
public interface OAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest> {
/**
* Exchanges the authorization grant credential, provided in the authorization grant request,
* for an access token credential at the Authorization Server's Token Endpoint.
*
* @param authorizationGrantRequest the authorization grant request that contains the authorization grant credential
* @return an {@link OAuth2AccessTokenResponse} that contains the {@link OAuth2AccessTokenResponse#getAccessToken() access token} credential
* @throws OAuth2AuthorizationException if an error occurs while attempting to exchange for the access token credential
* Exchanges the authorization grant credential, provided in the authorization grant
* request, for an access token credential at the Authorization Server's Token
* Endpoint.
* @param authorizationGrantRequest the authorization grant request that contains the
* authorization grant credential
* @return an {@link OAuth2AccessTokenResponse} that contains the
* {@link OAuth2AccessTokenResponse#getAccessToken() access token} credential
* @throws OAuth2AuthorizationException if an error occurs while attempting to
* exchange for the access token credential
*/
OAuth2AccessTokenResponse getTokenResponse(T authorizationGrantRequest);

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -21,28 +22,33 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExch
import org.springframework.util.Assert;
/**
* An OAuth 2.0 Authorization Code Grant request that holds an Authorization Code credential,
* which was granted by the Resource Owner to the {@link #getClientRegistration() Client}.
* An OAuth 2.0 Authorization Code Grant request that holds an Authorization Code
* credential, which was granted by the Resource Owner to the
* {@link #getClientRegistration() Client}.
*
* @author Joe Grandja
* @since 5.0
* @see AbstractOAuth2AuthorizationGrantRequest
* @see ClientRegistration
* @see OAuth2AuthorizationExchange
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.1">Section 1.3.1 Authorization Code Grant</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-1.3.1">Section 1.3.1 Authorization Code
* Grant</a>
*/
public class OAuth2AuthorizationCodeGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
private final ClientRegistration clientRegistration;
private final OAuth2AuthorizationExchange authorizationExchange;
/**
* Constructs an {@code OAuth2AuthorizationCodeGrantRequest} using the provided parameters.
*
* Constructs an {@code OAuth2AuthorizationCodeGrantRequest} using the provided
* parameters.
* @param clientRegistration the client registration
* @param authorizationExchange the authorization exchange
*/
public OAuth2AuthorizationCodeGrantRequest(ClientRegistration clientRegistration,
OAuth2AuthorizationExchange authorizationExchange) {
OAuth2AuthorizationExchange authorizationExchange) {
super(AuthorizationGrantType.AUTHORIZATION_CODE);
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.notNull(authorizationExchange, "authorizationExchange cannot be null");
@@ -52,7 +58,6 @@ public class OAuth2AuthorizationCodeGrantRequest extends AbstractOAuth2Authoriza
/**
* Returns the {@link ClientRegistration client registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -61,10 +66,10 @@ public class OAuth2AuthorizationCodeGrantRequest extends AbstractOAuth2Authoriza
/**
* Returns the {@link OAuth2AuthorizationExchange authorization exchange}.
*
* @return the {@link OAuth2AuthorizationExchange}
*/
public OAuth2AuthorizationExchange getAuthorizationExchange() {
return this.authorizationExchange;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.net.URI;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -28,12 +31,10 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
/**
* A {@link Converter} that converts the provided {@link OAuth2AuthorizationCodeGrantRequest}
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
* for the Authorization Code Grant.
* A {@link Converter} that converts the provided
* {@link OAuth2AuthorizationCodeGrantRequest} to a {@link RequestEntity} representation
* of an OAuth 2.0 Access Token Request for the Authorization Code Grant.
*
* @author Joe Grandja
* @since 5.1
@@ -41,42 +42,41 @@ import java.net.URI;
* @see OAuth2AuthorizationCodeGrantRequest
* @see RequestEntity
*/
public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
public class OAuth2AuthorizationCodeGrantRequestEntityConverter
implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
/**
* Returns the {@link RequestEntity} used for the Access Token Request.
*
* @param authorizationCodeGrantRequest the authorization code grant request
* @return the {@link RequestEntity} used for the Access Token Request
*/
@Override
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
MultiValueMap<String, String> formParameters = this.buildFormParameters(authorizationCodeGrantRequest);
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
.build()
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri()).build()
.toUri();
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
}
/**
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
*
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body.
* @param authorizationCodeGrantRequest the authorization code grant request
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
* @return a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body
*/
private MultiValueMap<String, String> buildFormParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
private MultiValueMap<String, String> buildFormParameters(
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
String codeVerifier = authorizationExchange.getAuthorizationRequest()
.getAttribute(PkceParameterNames.CODE_VERIFIER);
if (redirectUri != null) {
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
}
@@ -89,7 +89,7 @@ public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Conve
if (codeVerifier != null) {
formParameters.add(PkceParameterNames.CODE_VERIFIER, codeVerifier);
}
return formParameters;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Collections;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -22,15 +25,11 @@ import org.springframework.http.RequestEntity;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import java.util.Collections;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
/**
* Utility methods used by the {@link Converter}'s that convert
* from an implementation of an {@link AbstractOAuth2AuthorizationGrantRequest}
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
* for the specific Authorization Grant.
* Utility methods used by the {@link Converter}'s that convert from an implementation of
* an {@link AbstractOAuth2AuthorizationGrantRequest} to a {@link RequestEntity}
* representation of an OAuth 2.0 Access Token Request for the specific Authorization
* Grant.
*
* @author Joe Grandja
* @since 5.1
@@ -38,8 +37,12 @@ import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VAL
* @see OAuth2ClientCredentialsGrantRequestEntityConverter
*/
final class OAuth2AuthorizationGrantRequestEntityUtils {
private static HttpHeaders DEFAULT_TOKEN_REQUEST_HEADERS = getDefaultTokenRequestHeaders();
private OAuth2AuthorizationGrantRequestEntityUtils() {
}
static HttpHeaders getTokenRequestHeaders(ClientRegistration clientRegistration) {
HttpHeaders headers = new HttpHeaders();
headers.addAll(DEFAULT_TOKEN_REQUEST_HEADERS);
@@ -52,8 +55,9 @@ final class OAuth2AuthorizationGrantRequestEntityUtils {
private static HttpHeaders getDefaultTokenRequestHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
final MediaType contentType = MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
final MediaType contentType = MediaType.valueOf(MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
headers.setContentType(contentType);
return headers;
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -20,21 +21,24 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* An OAuth 2.0 Client Credentials Grant request that holds
* the client's credentials in {@link #getClientRegistration()}.
* An OAuth 2.0 Client Credentials Grant request that holds the client's credentials in
* {@link #getClientRegistration()}.
*
* @author Joe Grandja
* @since 5.1
* @see AbstractOAuth2AuthorizationGrantRequest
* @see ClientRegistration
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.4">Section 1.3.4 Client Credentials Grant</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-1.3.4">Section 1.3.4 Client Credentials
* Grant</a>
*/
public class OAuth2ClientCredentialsGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
private final ClientRegistration clientRegistration;
/**
* Constructs an {@code OAuth2ClientCredentialsGrantRequest} using the provided parameters.
*
* Constructs an {@code OAuth2ClientCredentialsGrantRequest} using the provided
* parameters.
* @param clientRegistration the client registration
*/
public OAuth2ClientCredentialsGrantRequest(ClientRegistration clientRegistration) {
@@ -47,10 +51,10 @@ public class OAuth2ClientCredentialsGrantRequest extends AbstractOAuth2Authoriza
/**
* Returns the {@link ClientRegistration client registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
return this.clientRegistration;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.net.URI;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -28,12 +31,10 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
/**
* A {@link Converter} that converts the provided {@link OAuth2ClientCredentialsGrantRequest}
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
* for the Client Credentials Grant.
* A {@link Converter} that converts the provided
* {@link OAuth2ClientCredentialsGrantRequest} to a {@link RequestEntity} representation
* of an OAuth 2.0 Access Token Request for the Client Credentials Grant.
*
* @author Joe Grandja
* @since 5.1
@@ -41,36 +42,34 @@ import java.net.URI;
* @see OAuth2ClientCredentialsGrantRequest
* @see RequestEntity
*/
public class OAuth2ClientCredentialsGrantRequestEntityConverter implements Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> {
public class OAuth2ClientCredentialsGrantRequestEntityConverter
implements Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> {
/**
* Returns the {@link RequestEntity} used for the Access Token Request.
*
* @param clientCredentialsGrantRequest the client credentials grant request
* @return the {@link RequestEntity} used for the Access Token Request
*/
@Override
public RequestEntity<?> convert(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
MultiValueMap<String, String> formParameters = this.buildFormParameters(clientCredentialsGrantRequest);
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
.build()
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri()).build()
.toUri();
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
}
/**
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
*
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body.
* @param clientCredentialsGrantRequest the client credentials grant request
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
* @return a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body
*/
private MultiValueMap<String, String> buildFormParameters(OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
private MultiValueMap<String, String> buildFormParameters(
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue());
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
@@ -81,7 +80,7 @@ public class OAuth2ClientCredentialsGrantRequestEntityConverter implements Conve
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
return formParameters;
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
@@ -20,22 +21,26 @@ import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;
/**
* An OAuth 2.0 Resource Owner Password Credentials Grant request
* that holds the resource owner's credentials.
* An OAuth 2.0 Resource Owner Password Credentials Grant request that holds the resource
* owner's credentials.
*
* @author Joe Grandja
* @since 5.2
* @see AbstractOAuth2AuthorizationGrantRequest
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.3">Section 1.3.3 Resource Owner Password Credentials</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-1.3.3">Section 1.3.3 Resource Owner
* Password Credentials</a>
*/
public class OAuth2PasswordGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
private final ClientRegistration clientRegistration;
private final String username;
private final String password;
/**
* Constructs an {@code OAuth2PasswordGrantRequest} using the provided parameters.
*
* @param clientRegistration the client registration
* @param username the resource owner's username
* @param password the resource owner's password
@@ -54,7 +59,6 @@ public class OAuth2PasswordGrantRequest extends AbstractOAuth2AuthorizationGrant
/**
* Returns the {@link ClientRegistration client registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -63,7 +67,6 @@ public class OAuth2PasswordGrantRequest extends AbstractOAuth2AuthorizationGrant
/**
* Returns the resource owner's username.
*
* @return the resource owner's username
*/
public String getUsername() {
@@ -72,10 +75,10 @@ public class OAuth2PasswordGrantRequest extends AbstractOAuth2AuthorizationGrant
/**
* Returns the resource owner's password.
*
* @return the resource owner's password
*/
public String getPassword() {
return this.password;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.net.URI;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -28,12 +31,10 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
/**
* A {@link Converter} that converts the provided {@link OAuth2PasswordGrantRequest}
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
* for the Resource Owner Password Credentials Grant.
* A {@link Converter} that converts the provided {@link OAuth2PasswordGrantRequest} to a
* {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
* Resource Owner Password Credentials Grant.
*
* @author Joe Grandja
* @since 5.2
@@ -41,36 +42,33 @@ import java.net.URI;
* @see OAuth2PasswordGrantRequest
* @see RequestEntity
*/
public class OAuth2PasswordGrantRequestEntityConverter implements Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> {
public class OAuth2PasswordGrantRequestEntityConverter
implements Converter<OAuth2PasswordGrantRequest, RequestEntity<?>> {
/**
* Returns the {@link RequestEntity} used for the Access Token Request.
*
* @param passwordGrantRequest the password grant request
* @return the {@link RequestEntity} used for the Access Token Request
*/
@Override
public RequestEntity<?> convert(OAuth2PasswordGrantRequest passwordGrantRequest) {
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
MultiValueMap<String, String> formParameters = buildFormParameters(passwordGrantRequest);
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
.build()
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri()).build()
.toUri();
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
}
/**
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
*
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body.
* @param passwordGrantRequest the password grant request
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
* @return a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body
*/
private MultiValueMap<String, String> buildFormParameters(OAuth2PasswordGrantRequest passwordGrantRequest) {
ClientRegistration clientRegistration = passwordGrantRequest.getClientRegistration();
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, passwordGrantRequest.getGrantType().getValue());
formParameters.add(OAuth2ParameterNames.USERNAME, passwordGrantRequest.getUsername());
@@ -83,7 +81,7 @@ public class OAuth2PasswordGrantRequestEntityConverter implements Converter<OAut
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
return formParameters;
}
}

View File

@@ -13,56 +13,60 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* An OAuth 2.0 Refresh Token Grant request that holds the {@link OAuth2RefreshToken refresh token} credential
* granted to the {@link #getClientRegistration() client}.
* An OAuth 2.0 Refresh Token Grant request that holds the {@link OAuth2RefreshToken
* refresh token} credential granted to the {@link #getClientRegistration() client}.
*
* @author Joe Grandja
* @since 5.2
* @see AbstractOAuth2AuthorizationGrantRequest
* @see OAuth2RefreshToken
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6
* Refreshing an Access Token</a>
*/
public class OAuth2RefreshTokenGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
private final ClientRegistration clientRegistration;
private final OAuth2AccessToken accessToken;
private final OAuth2RefreshToken refreshToken;
private final Set<String> scopes;
/**
* Constructs an {@code OAuth2RefreshTokenGrantRequest} using the provided parameters.
*
* @param clientRegistration the authorized client's registration
* @param accessToken the access token credential granted
* @param refreshToken the refresh token credential granted
*/
public OAuth2RefreshTokenGrantRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken,
OAuth2RefreshToken refreshToken) {
OAuth2RefreshToken refreshToken) {
this(clientRegistration, accessToken, refreshToken, Collections.emptySet());
}
/**
* Constructs an {@code OAuth2RefreshTokenGrantRequest} using the provided parameters.
*
* @param clientRegistration the authorized client's registration
* @param accessToken the access token credential granted
* @param refreshToken the refresh token credential granted
* @param scopes the scopes to request
*/
public OAuth2RefreshTokenGrantRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken,
OAuth2RefreshToken refreshToken, Set<String> scopes) {
OAuth2RefreshToken refreshToken, Set<String> scopes) {
super(AuthorizationGrantType.REFRESH_TOKEN);
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
Assert.notNull(accessToken, "accessToken cannot be null");
@@ -70,13 +74,12 @@ public class OAuth2RefreshTokenGrantRequest extends AbstractOAuth2AuthorizationG
this.clientRegistration = clientRegistration;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.scopes = Collections.unmodifiableSet(scopes != null ?
new LinkedHashSet<>(scopes) : Collections.emptySet());
this.scopes = Collections
.unmodifiableSet((scopes != null) ? new LinkedHashSet<>(scopes) : Collections.emptySet());
}
/**
* Returns the authorized client's {@link ClientRegistration registration}.
*
* @return the {@link ClientRegistration}
*/
public ClientRegistration getClientRegistration() {
@@ -85,7 +88,6 @@ public class OAuth2RefreshTokenGrantRequest extends AbstractOAuth2AuthorizationG
/**
* Returns the {@link OAuth2AccessToken access token} credential granted.
*
* @return the {@link OAuth2AccessToken}
*/
public OAuth2AccessToken getAccessToken() {
@@ -94,7 +96,6 @@ public class OAuth2RefreshTokenGrantRequest extends AbstractOAuth2AuthorizationG
/**
* Returns the {@link OAuth2RefreshToken refresh token} credential granted.
*
* @return the {@link OAuth2RefreshToken}
*/
public OAuth2RefreshToken getRefreshToken() {
@@ -103,10 +104,10 @@ public class OAuth2RefreshTokenGrantRequest extends AbstractOAuth2AuthorizationG
/**
* Returns the scope(s) to request.
*
* @return the scope(s) to request
*/
public Set<String> getScopes() {
return this.scopes;
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.net.URI;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -28,12 +31,10 @@ import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
/**
* A {@link Converter} that converts the provided {@link OAuth2RefreshTokenGrantRequest}
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
* for the Refresh Token Grant.
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request for the
* Refresh Token Grant.
*
* @author Joe Grandja
* @since 5.2
@@ -41,36 +42,33 @@ import java.net.URI;
* @see OAuth2RefreshTokenGrantRequest
* @see RequestEntity
*/
public class OAuth2RefreshTokenGrantRequestEntityConverter implements Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> {
public class OAuth2RefreshTokenGrantRequestEntityConverter
implements Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> {
/**
* Returns the {@link RequestEntity} used for the Access Token Request.
*
* @param refreshTokenGrantRequest the refresh token grant request
* @return the {@link RequestEntity} used for the Access Token Request
*/
@Override
public RequestEntity<?> convert(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
ClientRegistration clientRegistration = refreshTokenGrantRequest.getClientRegistration();
HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
MultiValueMap<String, String> formParameters = buildFormParameters(refreshTokenGrantRequest);
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
.build()
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri()).build()
.toUri();
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
}
/**
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
*
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body.
* @param refreshTokenGrantRequest the refresh token grant request
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
* @return a {@link MultiValueMap} of the form parameters used for the Access Token
* Request body
*/
private MultiValueMap<String, String> buildFormParameters(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
ClientRegistration clientRegistration = refreshTokenGrantRequest.getClientRegistration();
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, refreshTokenGrantRequest.getGrantType().getValue());
formParameters.add(OAuth2ParameterNames.REFRESH_TOKEN,
@@ -83,7 +81,7 @@ public class OAuth2RefreshTokenGrantRequestEntityConverter implements Converter<
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
return formParameters;
}
}

View File

@@ -13,37 +13,47 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import reactor.core.publisher.Mono;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import reactor.core.publisher.Mono;
/**
* A reactive strategy for &quot;exchanging&quot; an authorization grant credential
* (e.g. an Authorization Code) for an access token credential
* at the Authorization Server's Token Endpoint.
* A reactive strategy for &quot;exchanging&quot; an authorization grant credential (e.g.
* an Authorization Code) for an access token credential at the Authorization Server's
* Token Endpoint.
*
* @author Rob Winch
* @since 5.1
* @see AbstractOAuth2AuthorizationGrantRequest
* @see OAuth2AccessTokenResponse
* @see AuthorizationGrantType
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section
* 1.3 Authorization Grant</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
* (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
* (Authorization Code Grant)</a>
*/
@FunctionalInterface
public interface ReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest> {
public interface ReactiveOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest> {
/**
* Exchanges the authorization grant credential, provided in the authorization grant request,
* for an access token credential at the Authorization Server's Token Endpoint.
*
* @param authorizationGrantRequest the authorization grant request that contains the authorization grant credential
* @return an {@link OAuth2AccessTokenResponse} that contains the {@link OAuth2AccessTokenResponse#getAccessToken() access token} credential
* @throws OAuth2AuthorizationException if an error occurs while attempting to exchange for the access token credential
* Exchanges the authorization grant credential, provided in the authorization grant
* request, for an access token credential at the Authorization Server's Token
* Endpoint.
* @param authorizationGrantRequest the authorization grant request that contains the
* authorization grant credential
* @return an {@link OAuth2AccessTokenResponse} that contains the
* {@link OAuth2AccessTokenResponse#getAccessToken() access token} credential
* @throws OAuth2AuthorizationException if an error occurs while attempting to
* exchange for the access token credential
*/
Mono<OAuth2AccessTokenResponse> getTokenResponse(T authorizationGrantRequest);

View File

@@ -13,8 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Collections;
import java.util.Set;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
@@ -23,12 +27,9 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
import org.springframework.web.reactive.function.BodyInserters;
import java.util.Collections;
import java.util.Set;
/**
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} that &quot;exchanges&quot;
* an authorization code credential for an access token credential
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} that
* &quot;exchanges&quot; an authorization code credential for an access token credential
* at the Authorization Server's Token Endpoint.
*
* <p>
@@ -39,13 +40,20 @@ import java.util.Set;
* @see ReactiveOAuth2AccessTokenResponseClient
* @see OAuth2AuthorizationCodeGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">Section 4.2 Client Creates the Code Challenge</a>
* @see <a target="_blank" href=
* "https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0
* SDK</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
* (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
* (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">Section
* 4.2 Client Creates the Code Challenge</a>
*/
public class WebClientReactiveAuthorizationCodeTokenResponseClient extends
AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
public class WebClientReactiveAuthorizationCodeTokenResponseClient
extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
@Override
ClientRegistration clientRegistration(OAuth2AuthorizationCodeGrantRequest grantRequest) {
@@ -63,8 +71,7 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient extends
}
@Override
BodyInserters.FormInserter<String> populateTokenRequestBody(
OAuth2AuthorizationCodeGrantRequest grantRequest,
BodyInserters.FormInserter<String> populateTokenRequestBody(OAuth2AuthorizationCodeGrantRequest grantRequest,
BodyInserters.FormInserter<String> body) {
super.populateTokenRequestBody(grantRequest, body);
OAuth2AuthorizationExchange authorizationExchange = grantRequest.getAuthorizationExchange();
@@ -74,10 +81,12 @@ public class WebClientReactiveAuthorizationCodeTokenResponseClient extends
if (redirectUri != null) {
body.with(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
}
String codeVerifier = authorizationExchange.getAuthorizationRequest().getAttribute(PkceParameterNames.CODE_VERIFIER);
String codeVerifier = authorizationExchange.getAuthorizationRequest()
.getAttribute(PkceParameterNames.CODE_VERIFIER);
if (codeVerifier != null) {
body.with(PkceParameterNames.CODE_VERIFIER, codeVerifier);
}
return body;
}
}

View File

@@ -13,29 +13,36 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Set;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import java.util.Set;
/**
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} that &quot;exchanges&quot;
* a client credential for an access token credential
* at the Authorization Server's Token Endpoint.
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} that
* &quot;exchanges&quot; a client credential for an access token credential at the
* Authorization Server's Token Endpoint.
*
* @author Rob Winch
* @since 5.1
* @see ReactiveOAuth2AccessTokenResponseClient
* @see OAuth2AuthorizationCodeGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0
* SDK</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
* (Authorization Code Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
* (Authorization Code Grant)</a>
*/
public class WebClientReactiveClientCredentialsTokenResponseClient extends
AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
public class WebClientReactiveClientCredentialsTokenResponseClient
extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
@Override
ClientRegistration clientRegistration(OAuth2ClientCredentialsGrantRequest grantRequest) {

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Set;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
@@ -22,24 +25,26 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Set;
/**
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient}
* for the {@link AuthorizationGrantType#PASSWORD password} grant.
* This implementation uses {@link WebClient} when requesting
* an access token credential at the Authorization Server's Token Endpoint.
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#PASSWORD password} grant. This implementation uses
* {@link WebClient} when requesting an access token credential at the Authorization
* Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AccessTokenResponseClient
* @see OAuth2PasswordGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request (Resource Owner Password Credentials Grant)</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response (Resource Owner Password Credentials Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.3.2">Section 4.3.2 Access Token Request
* (Resource Owner Password Credentials Grant)</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.3.3">Section 4.3.3 Access Token Response
* (Resource Owner Password Credentials Grant)</a>
*/
public final class WebClientReactivePasswordTokenResponseClient extends
AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
public final class WebClientReactivePasswordTokenResponseClient
extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> {
@Override
ClientRegistration clientRegistration(OAuth2PasswordGrantRequest grantRequest) {
@@ -52,12 +57,11 @@ public final class WebClientReactivePasswordTokenResponseClient extends
}
@Override
BodyInserters.FormInserter<String> populateTokenRequestBody(
OAuth2PasswordGrantRequest grantRequest,
BodyInserters.FormInserter<String> populateTokenRequestBody(OAuth2PasswordGrantRequest grantRequest,
BodyInserters.FormInserter<String> body) {
return super.populateTokenRequestBody(grantRequest, body)
.with(OAuth2ParameterNames.USERNAME, grantRequest.getUsername())
.with(OAuth2ParameterNames.PASSWORD, grantRequest.getPassword());
.with(OAuth2ParameterNames.USERNAME, grantRequest.getUsername())
.with(OAuth2ParameterNames.PASSWORD, grantRequest.getPassword());
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.endpoint;
import java.util.Set;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
@@ -23,23 +26,22 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import java.util.Set;
/**
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient}
* for the {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant.
* This implementation uses {@link WebClient} when requesting
* an access token credential at the Authorization Server's Token Endpoint.
* An implementation of a {@link ReactiveOAuth2AccessTokenResponseClient} for the
* {@link AuthorizationGrantType#REFRESH_TOKEN refresh_token} grant. This implementation
* uses {@link WebClient} when requesting an access token credential at the Authorization
* Server's Token Endpoint.
*
* @author Joe Grandja
* @since 5.2
* @see ReactiveOAuth2AccessTokenResponseClient
* @see OAuth2RefreshTokenGrantRequest
* @see OAuth2AccessTokenResponse
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6
* Refreshing an Access Token</a>
*/
public final class WebClientReactiveRefreshTokenTokenResponseClient extends
AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
public final class WebClientReactiveRefreshTokenTokenResponseClient
extends AbstractWebClientReactiveOAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
@Override
ClientRegistration clientRegistration(OAuth2RefreshTokenGrantRequest grantRequest) {
@@ -57,24 +59,21 @@ public final class WebClientReactiveRefreshTokenTokenResponseClient extends
}
@Override
BodyInserters.FormInserter<String> populateTokenRequestBody(
OAuth2RefreshTokenGrantRequest grantRequest,
BodyInserters.FormInserter<String> populateTokenRequestBody(OAuth2RefreshTokenGrantRequest grantRequest,
BodyInserters.FormInserter<String> body) {
return super.populateTokenRequestBody(grantRequest, body)
.with(OAuth2ParameterNames.REFRESH_TOKEN, grantRequest.getRefreshToken().getTokenValue());
return super.populateTokenRequestBody(grantRequest, body).with(OAuth2ParameterNames.REFRESH_TOKEN,
grantRequest.getRefreshToken().getTokenValue());
}
@Override
OAuth2AccessTokenResponse populateTokenResponse(
OAuth2RefreshTokenGrantRequest grantRequest,
OAuth2AccessTokenResponse populateTokenResponse(OAuth2RefreshTokenGrantRequest grantRequest,
OAuth2AccessTokenResponse accessTokenResponse) {
if (!CollectionUtils.isEmpty(accessTokenResponse.getAccessToken().getScopes()) &&
accessTokenResponse.getRefreshToken() != null) {
if (!CollectionUtils.isEmpty(accessTokenResponse.getAccessToken().getScopes())
&& accessTokenResponse.getRefreshToken() != null) {
return accessTokenResponse;
}
OAuth2AccessTokenResponse.Builder tokenResponseBuilder = OAuth2AccessTokenResponse.withResponse(accessTokenResponse);
OAuth2AccessTokenResponse.Builder tokenResponseBuilder = OAuth2AccessTokenResponse
.withResponse(accessTokenResponse);
if (CollectionUtils.isEmpty(accessTokenResponse.getAccessToken().getScopes())) {
tokenResponseBuilder.scopes(defaultScopes(grantRequest));
}

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Classes and interfaces providing support to the client
* for initiating requests to the Authorization Server's Protocol Endpoints.
* Classes and interfaces providing support to the client for initiating requests to the
* Authorization Server's Protocol Endpoints.
*/
package org.springframework.security.oauth2.client.endpoint;

View File

@@ -13,9 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.http;
import java.io.IOException;
import com.nimbusds.oauth2.sdk.token.BearerTokenError;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
@@ -27,18 +31,18 @@ import org.springframework.util.StringUtils;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.ResponseErrorHandler;
import java.io.IOException;
/**
* A {@link ResponseErrorHandler} that handles an {@link OAuth2Error OAuth 2.0 Error}.
*
* @see ResponseErrorHandler
* @see OAuth2Error
* @author Joe Grandja
* @since 5.1
* @see ResponseErrorHandler
* @see OAuth2Error
*/
public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler {
private final OAuth2ErrorHttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter();
private final ResponseErrorHandler defaultErrorHandler = new DefaultResponseErrorHandler();
@Override
@@ -51,14 +55,12 @@ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler {
if (!HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
this.defaultErrorHandler.handleError(response);
}
// A Bearer Token Error may be in the WWW-Authenticate response header
// See https://tools.ietf.org/html/rfc6750#section-3
OAuth2Error oauth2Error = this.readErrorFromWwwAuthenticate(response.getHeaders());
if (oauth2Error == null) {
oauth2Error = this.oauth2ErrorConverter.read(OAuth2Error.class, response);
}
throw new OAuth2AuthorizationException(oauth2Error);
}
@@ -67,20 +69,21 @@ public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler {
if (!StringUtils.hasText(wwwAuthenticateHeader)) {
return null;
}
BearerTokenError bearerTokenError;
try {
bearerTokenError = BearerTokenError.parse(wwwAuthenticateHeader);
} catch (Exception ex) {
return null;
}
String errorCode = bearerTokenError.getCode() != null ?
bearerTokenError.getCode() : OAuth2ErrorCodes.SERVER_ERROR;
BearerTokenError bearerTokenError = getBearerToken(wwwAuthenticateHeader);
String errorCode = (bearerTokenError.getCode() != null) ? bearerTokenError.getCode()
: OAuth2ErrorCodes.SERVER_ERROR;
String errorDescription = bearerTokenError.getDescription();
String errorUri = bearerTokenError.getURI() != null ?
bearerTokenError.getURI().toString() : null;
String errorUri = (bearerTokenError.getURI() != null) ? bearerTokenError.getURI().toString() : null;
return new OAuth2Error(errorCode, errorDescription, errorUri);
}
private BearerTokenError getBearerToken(String wwwAuthenticateHeader) {
try {
return BearerTokenError.parse(wwwAuthenticateHeader);
}
catch (Exception ex) {
return null;
}
}
}

View File

@@ -13,27 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.AuthenticationMethod;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import java.io.IOException;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.MAP_TYPE_REFERENCE;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.SET_TYPE_REFERENCE;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findObjectNode;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findStringValue;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findValue;
/**
* A {@code JsonDeserializer} for {@link ClientRegistration}.
*
@@ -43,43 +39,41 @@ import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.
* @see ClientRegistrationMixin
*/
final class ClientRegistrationDeserializer extends JsonDeserializer<ClientRegistration> {
private static final StdConverter<JsonNode, ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHOD_CONVERTER =
new StdConverters.ClientAuthenticationMethodConverter();
private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER =
new StdConverters.AuthorizationGrantTypeConverter();
private static final StdConverter<JsonNode, AuthenticationMethod> AUTHENTICATION_METHOD_CONVERTER =
new StdConverters.AuthenticationMethodConverter();
private static final StdConverter<JsonNode, ClientAuthenticationMethod> CLIENT_AUTHENTICATION_METHOD_CONVERTER = new StdConverters.ClientAuthenticationMethodConverter();
private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER = new StdConverters.AuthorizationGrantTypeConverter();
private static final StdConverter<JsonNode, AuthenticationMethod> AUTHENTICATION_METHOD_CONVERTER = new StdConverters.AuthenticationMethodConverter();
@Override
public ClientRegistration deserialize(JsonParser parser, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode clientRegistrationNode = mapper.readTree(parser);
JsonNode providerDetailsNode = findObjectNode(clientRegistrationNode, "providerDetails");
JsonNode userInfoEndpointNode = findObjectNode(providerDetailsNode, "userInfoEndpoint");
JsonNode providerDetailsNode = JsonNodeUtils.findObjectNode(clientRegistrationNode, "providerDetails");
JsonNode userInfoEndpointNode = JsonNodeUtils.findObjectNode(providerDetailsNode, "userInfoEndpoint");
return ClientRegistration
.withRegistrationId(findStringValue(clientRegistrationNode, "registrationId"))
.clientId(findStringValue(clientRegistrationNode, "clientId"))
.clientSecret(findStringValue(clientRegistrationNode, "clientSecret"))
.clientAuthenticationMethod(
CLIENT_AUTHENTICATION_METHOD_CONVERTER.convert(
findObjectNode(clientRegistrationNode, "clientAuthenticationMethod")))
.authorizationGrantType(
AUTHORIZATION_GRANT_TYPE_CONVERTER.convert(
findObjectNode(clientRegistrationNode, "authorizationGrantType")))
.redirectUri(findStringValue(clientRegistrationNode, "redirectUri"))
.scope(findValue(clientRegistrationNode, "scopes", SET_TYPE_REFERENCE, mapper))
.clientName(findStringValue(clientRegistrationNode, "clientName"))
.authorizationUri(findStringValue(providerDetailsNode, "authorizationUri"))
.tokenUri(findStringValue(providerDetailsNode, "tokenUri"))
.userInfoUri(findStringValue(userInfoEndpointNode, "uri"))
.userInfoAuthenticationMethod(
AUTHENTICATION_METHOD_CONVERTER.convert(
findObjectNode(userInfoEndpointNode, "authenticationMethod")))
.userNameAttributeName(findStringValue(userInfoEndpointNode, "userNameAttributeName"))
.jwkSetUri(findStringValue(providerDetailsNode, "jwkSetUri"))
.issuerUri(findStringValue(providerDetailsNode, "issuerUri"))
.providerConfigurationMetadata(findValue(providerDetailsNode, "configurationMetadata", MAP_TYPE_REFERENCE, mapper))
.withRegistrationId(JsonNodeUtils.findStringValue(clientRegistrationNode, "registrationId"))
.clientId(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientId"))
.clientSecret(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientSecret"))
.clientAuthenticationMethod(CLIENT_AUTHENTICATION_METHOD_CONVERTER
.convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "clientAuthenticationMethod")))
.authorizationGrantType(AUTHORIZATION_GRANT_TYPE_CONVERTER
.convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "authorizationGrantType")))
.redirectUri(JsonNodeUtils.findStringValue(clientRegistrationNode, "redirectUri"))
.scope(JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, mapper))
.clientName(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName"))
.authorizationUri(JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri"))
.tokenUri(JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri"))
.userInfoUri(JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri"))
.userInfoAuthenticationMethod(AUTHENTICATION_METHOD_CONVERTER
.convert(JsonNodeUtils.findObjectNode(userInfoEndpointNode, "authenticationMethod")))
.userNameAttributeName(JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName"))
.jwkSetUri(JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri"))
.issuerUri(JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri"))
.providerConfigurationMetadata(JsonNodeUtils.findValue(providerDetailsNode, "configurationMetadata",
JsonNodeUtils.STRING_OBJECT_MAP, mapper))
.build();
}
}

View File

@@ -13,17 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
/**
* This mixin class is used to serialize/deserialize {@link ClientRegistration}.
* It also registers a custom deserializer {@link ClientRegistrationDeserializer}.
* This mixin class is used to serialize/deserialize {@link ClientRegistration}. It also
* registers a custom deserializer {@link ClientRegistrationDeserializer}.
*
* @author Joe Grandja
* @since 5.3
@@ -37,4 +39,5 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class ClientRegistrationMixin {
}

View File

@@ -13,19 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Collection;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import java.util.Collection;
import java.util.Map;
/**
* This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}.
*
@@ -41,9 +43,9 @@ import java.util.Map;
abstract class DefaultOAuth2UserMixin {
@JsonCreator
DefaultOAuth2UserMixin(
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
DefaultOAuth2UserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("attributes") Map<String, Object> attributes,
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
}
}

View File

@@ -13,20 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import java.util.Collection;
/**
* This mixin class is used to serialize/deserialize {@link DefaultOidcUser}.
*
@@ -38,14 +40,13 @@ import java.util.Collection;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(value = {"attributes"}, ignoreUnknown = true)
@JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true)
abstract class DefaultOidcUserMixin {
@JsonCreator
DefaultOidcUserMixin(
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("idToken") OidcIdToken idToken,
@JsonProperty("userInfo") OidcUserInfo userInfo,
DefaultOidcUserMixin(@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("idToken") OidcIdToken idToken, @JsonProperty("userInfo") OidcUserInfo userInfo,
@JsonProperty("nameAttributeKey") String nameAttributeKey) {
}
}

View File

@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.Set;
/**
* Utility class for {@code JsonNode}.
*
@@ -29,40 +30,36 @@ import java.util.Set;
* @since 5.3
*/
abstract class JsonNodeUtils {
static final TypeReference<Set<String>> SET_TYPE_REFERENCE = new TypeReference<Set<String>>() {};
static final TypeReference<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {};
static final TypeReference<Set<String>> STRING_SET = new TypeReference<Set<String>>() {
};
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
};
static String findStringValue(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode nodeValue = jsonNode.findValue(fieldName);
if (nodeValue != null && nodeValue.isTextual()) {
return nodeValue.asText();
}
return null;
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isTextual()) ? value.asText() : null;
}
static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference, ObjectMapper mapper) {
static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
ObjectMapper mapper) {
if (jsonNode == null) {
return null;
}
JsonNode nodeValue = jsonNode.findValue(fieldName);
if (nodeValue != null && nodeValue.isContainerNode()) {
return (T) mapper.convertValue(nodeValue, valueTypeReference);
}
return null;
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
}
static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode nodeValue = jsonNode.findValue(fieldName);
if (nodeValue != null && nodeValue.isObject()) {
return nodeValue;
}
return null;
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isObject()) ? value : null;
}
}

View File

@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.time.Instant;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import java.time.Instant;
import java.util.Set;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
/**
* This mixin class is used to serialize/deserialize {@link OAuth2AccessToken}.
@@ -42,10 +44,10 @@ abstract class OAuth2AccessTokenMixin {
@JsonCreator
OAuth2AccessTokenMixin(
@JsonProperty("tokenType") @JsonDeserialize(converter = StdConverters.AccessTokenTypeConverter.class) OAuth2AccessToken.TokenType tokenType,
@JsonProperty("tokenValue") String tokenValue,
@JsonProperty("issuedAt") Instant issuedAt,
@JsonProperty("expiresAt") Instant expiresAt,
@JsonProperty("scopes") Set<String> scopes) {
@JsonProperty("tokenType") @JsonDeserialize(
converter = StdConverters.AccessTokenTypeConverter.class) OAuth2AccessToken.TokenType tokenType,
@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt,
@JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("scopes") Set<String> scopes) {
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -20,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
@@ -34,13 +36,13 @@ import org.springframework.security.oauth2.core.OAuth2Error;
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true, value = {"cause", "stackTrace", "suppressedExceptions"})
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true, value = { "cause", "stackTrace", "suppressedExceptions" })
abstract class OAuth2AuthenticationExceptionMixin {
@JsonCreator
OAuth2AuthenticationExceptionMixin(
@JsonProperty("error") OAuth2Error error,
OAuth2AuthenticationExceptionMixin(@JsonProperty("error") OAuth2Error error,
@JsonProperty("detailMessage") String message) {
}
}

View File

@@ -13,19 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
/**
* This mixin class is used to serialize/deserialize {@link OAuth2AuthenticationToken}.
*
@@ -37,13 +39,13 @@ import java.util.Collection;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(value = {"authenticated"}, ignoreUnknown = true)
@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true)
abstract class OAuth2AuthenticationTokenMixin {
@JsonCreator
OAuth2AuthenticationTokenMixin(
@JsonProperty("principal") OAuth2User principal,
OAuth2AuthenticationTokenMixin(@JsonProperty("principal") OAuth2User principal,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("authorizedClientRegistrationId") String authorizedClientRegistrationId) {
}
}

View File

@@ -13,8 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
@@ -22,16 +25,10 @@ import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import java.io.IOException;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.MAP_TYPE_REFERENCE;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.SET_TYPE_REFERENCE;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findObjectNode;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findStringValue;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findValue;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder;
/**
* A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}.
@@ -42,35 +39,43 @@ import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.
* @see OAuth2AuthorizationRequestMixin
*/
final class OAuth2AuthorizationRequestDeserializer extends JsonDeserializer<OAuth2AuthorizationRequest> {
private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER =
new StdConverters.AuthorizationGrantTypeConverter();
private static final StdConverter<JsonNode, AuthorizationGrantType> AUTHORIZATION_GRANT_TYPE_CONVERTER = new StdConverters.AuthorizationGrantTypeConverter();
@Override
public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context) throws IOException {
public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode authorizationRequestNode = mapper.readTree(parser);
AuthorizationGrantType authorizationGrantType = AUTHORIZATION_GRANT_TYPE_CONVERTER.convert(
findObjectNode(authorizationRequestNode, "authorizationGrantType"));
OAuth2AuthorizationRequest.Builder builder;
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
builder = OAuth2AuthorizationRequest.authorizationCode();
} else if (AuthorizationGrantType.IMPLICIT.equals(authorizationGrantType)) {
builder = OAuth2AuthorizationRequest.implicit();
} else {
throw new JsonParseException(parser, "Invalid authorizationGrantType");
}
return builder
.authorizationUri(findStringValue(authorizationRequestNode, "authorizationUri"))
.clientId(findStringValue(authorizationRequestNode, "clientId"))
.redirectUri(findStringValue(authorizationRequestNode, "redirectUri"))
.scopes(findValue(authorizationRequestNode, "scopes", SET_TYPE_REFERENCE, mapper))
.state(findStringValue(authorizationRequestNode, "state"))
.additionalParameters(findValue(authorizationRequestNode, "additionalParameters", MAP_TYPE_REFERENCE, mapper))
.authorizationRequestUri(findStringValue(authorizationRequestNode, "authorizationRequestUri"))
.attributes(findValue(authorizationRequestNode, "attributes", MAP_TYPE_REFERENCE, mapper))
.build();
JsonNode root = mapper.readTree(parser);
return deserialize(parser, mapper, root);
}
private OAuth2AuthorizationRequest deserialize(JsonParser parser, ObjectMapper mapper, JsonNode root)
throws JsonParseException {
AuthorizationGrantType authorizationGrantType = AUTHORIZATION_GRANT_TYPE_CONVERTER
.convert(JsonNodeUtils.findObjectNode(root, "authorizationGrantType"));
Builder builder = getBuilder(parser, authorizationGrantType);
builder.authorizationUri(JsonNodeUtils.findStringValue(root, "authorizationUri"));
builder.clientId(JsonNodeUtils.findStringValue(root, "clientId"));
builder.redirectUri(JsonNodeUtils.findStringValue(root, "redirectUri"));
builder.scopes(JsonNodeUtils.findValue(root, "scopes", JsonNodeUtils.STRING_SET, mapper));
builder.state(JsonNodeUtils.findStringValue(root, "state"));
builder.additionalParameters(
JsonNodeUtils.findValue(root, "additionalParameters", JsonNodeUtils.STRING_OBJECT_MAP, mapper));
builder.authorizationRequestUri(JsonNodeUtils.findStringValue(root, "authorizationRequestUri"));
builder.attributes(JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, mapper));
return builder.build();
}
private OAuth2AuthorizationRequest.Builder getBuilder(JsonParser parser,
AuthorizationGrantType authorizationGrantType) throws JsonParseException {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
return OAuth2AuthorizationRequest.authorizationCode();
}
if (AuthorizationGrantType.IMPLICIT.equals(authorizationGrantType)) {
return OAuth2AuthorizationRequest.implicit();
}
throw new JsonParseException(parser, "Invalid authorizationGrantType");
}
}

View File

@@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
/**
@@ -37,4 +39,5 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class OAuth2AuthorizationRequestMixin {
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -20,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
@@ -40,10 +42,10 @@ import org.springframework.security.oauth2.core.OAuth2RefreshToken;
abstract class OAuth2AuthorizedClientMixin {
@JsonCreator
OAuth2AuthorizedClientMixin(
@JsonProperty("clientRegistration") ClientRegistration clientRegistration,
OAuth2AuthorizedClientMixin(@JsonProperty("clientRegistration") ClientRegistration clientRegistration,
@JsonProperty("principalName") String principalName,
@JsonProperty("accessToken") OAuth2AccessToken accessToken,
@JsonProperty("refreshToken") OAuth2RefreshToken refreshToken) {
}
}

View File

@@ -13,10 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Collections;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
@@ -33,39 +37,38 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import java.util.Collections;
/**
* Jackson {@code Module} for {@code spring-security-oauth2-client},
* that registers the following mix-in annotations:
* Jackson {@code Module} for {@code spring-security-oauth2-client}, that registers the
* following mix-in annotations:
*
* <ul>
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
* <li>{@link ClientRegistrationMixin}</li>
* <li>{@link OAuth2AccessTokenMixin}</li>
* <li>{@link OAuth2RefreshTokenMixin}</li>
* <li>{@link OAuth2AuthorizedClientMixin}</li>
* <li>{@link OAuth2UserAuthorityMixin}</li>
* <li>{@link DefaultOAuth2UserMixin}</li>
* <li>{@link OidcIdTokenMixin}</li>
* <li>{@link OidcUserInfoMixin}</li>
* <li>{@link OidcUserAuthorityMixin}</li>
* <li>{@link DefaultOidcUserMixin}</li>
* <li>{@link OAuth2AuthenticationTokenMixin}</li>
* <li>{@link OAuth2AuthenticationExceptionMixin}</li>
* <li>{@link OAuth2ErrorMixin}</li>
* <li>{@link OAuth2AuthorizationRequestMixin}</li>
* <li>{@link ClientRegistrationMixin}</li>
* <li>{@link OAuth2AccessTokenMixin}</li>
* <li>{@link OAuth2RefreshTokenMixin}</li>
* <li>{@link OAuth2AuthorizedClientMixin}</li>
* <li>{@link OAuth2UserAuthorityMixin}</li>
* <li>{@link DefaultOAuth2UserMixin}</li>
* <li>{@link OidcIdTokenMixin}</li>
* <li>{@link OidcUserInfoMixin}</li>
* <li>{@link OidcUserAuthorityMixin}</li>
* <li>{@link DefaultOidcUserMixin}</li>
* <li>{@link OAuth2AuthenticationTokenMixin}</li>
* <li>{@link OAuth2AuthenticationExceptionMixin}</li>
* <li>{@link OAuth2ErrorMixin}</li>
* </ul>
*
* If not already enabled, default typing will be automatically enabled
* as type info is required to properly serialize/deserialize objects.
* In order to use this module just add it to your {@code ObjectMapper} configuration.
* If not already enabled, default typing will be automatically enabled as type info is
* required to properly serialize/deserialize objects. In order to use this module just
* add it to your {@code ObjectMapper} configuration.
*
* <pre>
* ObjectMapper mapper = new ObjectMapper();
* mapper.registerModule(new OAuth2ClientJackson2Module());
* </pre>
*
* <b>NOTE:</b> Use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get a list of all security modules.
* <b>NOTE:</b> Use {@link SecurityJackson2Modules#getModules(ClassLoader)} to get a list
* of all security modules.
*
* @author Joe Grandja
* @since 5.3
@@ -94,7 +97,8 @@ public class OAuth2ClientJackson2Module extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
SecurityJackson2Modules.enableDefaultTyping(context.getOwner());
context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(), UnmodifiableMapMixin.class);
context.setMixInAnnotations(Collections.unmodifiableMap(Collections.emptyMap()).getClass(),
UnmodifiableMapMixin.class);
context.setMixInAnnotations(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class);
context.setMixInAnnotations(ClientRegistration.class, ClientRegistrationMixin.class);
context.setMixInAnnotations(OAuth2AccessToken.class, OAuth2AccessTokenMixin.class);
@@ -110,4 +114,5 @@ public class OAuth2ClientJackson2Module extends SimpleModule {
context.setMixInAnnotations(OAuth2AuthenticationException.class, OAuth2AuthenticationExceptionMixin.class);
context.setMixInAnnotations(OAuth2Error.class, OAuth2ErrorMixin.class);
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -20,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.OAuth2Error;
/**
@@ -34,14 +36,13 @@ import org.springframework.security.oauth2.core.OAuth2Error;
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class OAuth2ErrorMixin {
@JsonCreator
OAuth2ErrorMixin(
@JsonProperty("errorCode") String errorCode,
@JsonProperty("description") String description,
OAuth2ErrorMixin(@JsonProperty("errorCode") String errorCode, @JsonProperty("description") String description,
@JsonProperty("uri") String uri) {
}
}

View File

@@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.time.Instant;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import java.time.Instant;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
/**
* This mixin class is used to serialize/deserialize {@link OAuth2RefreshToken}.
@@ -39,8 +41,7 @@ import java.time.Instant;
abstract class OAuth2RefreshTokenMixin {
@JsonCreator
OAuth2RefreshTokenMixin(
@JsonProperty("tokenValue") String tokenValue,
@JsonProperty("issuedAt") Instant issuedAt) {
OAuth2RefreshTokenMixin(@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt) {
}
}

View File

@@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import java.util.Map;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
/**
* This mixin class is used to serialize/deserialize {@link OAuth2UserAuthority}.
@@ -39,8 +41,8 @@ import java.util.Map;
abstract class OAuth2UserAuthorityMixin {
@JsonCreator
OAuth2UserAuthorityMixin(
@JsonProperty("authority") String authority,
OAuth2UserAuthorityMixin(@JsonProperty("authority") String authority,
@JsonProperty("attributes") Map<String, Object> attributes) {
}
}

View File

@@ -13,17 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.time.Instant;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import java.time.Instant;
import java.util.Map;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
/**
* This mixin class is used to serialize/deserialize {@link OidcIdToken}.
@@ -40,10 +42,8 @@ import java.util.Map;
abstract class OidcIdTokenMixin {
@JsonCreator
OidcIdTokenMixin(
@JsonProperty("tokenValue") String tokenValue,
@JsonProperty("issuedAt") Instant issuedAt,
@JsonProperty("expiresAt") Instant expiresAt,
@JsonProperty("claims") Map<String, Object> claims) {
OidcIdTokenMixin(@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt,
@JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("claims") Map<String, Object> claims) {
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
@@ -20,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
@@ -35,13 +37,12 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(value = {"attributes"}, ignoreUnknown = true)
@JsonIgnoreProperties(value = { "attributes" }, ignoreUnknown = true)
abstract class OidcUserAuthorityMixin {
@JsonCreator
OidcUserAuthorityMixin(
@JsonProperty("authority") String authority,
@JsonProperty("idToken") OidcIdToken idToken,
OidcUserAuthorityMixin(@JsonProperty("authority") String authority, @JsonProperty("idToken") OidcIdToken idToken,
@JsonProperty("userInfo") OidcUserInfo userInfo) {
}
}

View File

@@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import java.util.Map;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
/**
* This mixin class is used to serialize/deserialize {@link OidcUserInfo}.
@@ -41,4 +43,5 @@ abstract class OidcUserInfoMixin {
@JsonCreator
OidcUserInfoMixin(@JsonProperty("claims") Map<String, Object> claims) {
}
}

View File

@@ -13,17 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.util.StdConverter;
import org.springframework.security.oauth2.core.AuthenticationMethod;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.findStringValue;
/**
* {@code StdConverter} implementations.
*
@@ -33,60 +33,76 @@ import static org.springframework.security.oauth2.client.jackson2.JsonNodeUtils.
abstract class StdConverters {
static final class AccessTokenTypeConverter extends StdConverter<JsonNode, OAuth2AccessToken.TokenType> {
@Override
public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) {
String value = findStringValue(jsonNode, "value");
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) {
return OAuth2AccessToken.TokenType.BEARER;
}
return null;
}
}
static final class ClientAuthenticationMethodConverter extends StdConverter<JsonNode, ClientAuthenticationMethod> {
@Override
public ClientAuthenticationMethod convert(JsonNode jsonNode) {
String value = findStringValue(jsonNode, "value");
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (ClientAuthenticationMethod.BASIC.getValue().equalsIgnoreCase(value)) {
return ClientAuthenticationMethod.BASIC;
} else if (ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) {
}
if (ClientAuthenticationMethod.POST.getValue().equalsIgnoreCase(value)) {
return ClientAuthenticationMethod.POST;
} else if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) {
}
if (ClientAuthenticationMethod.NONE.getValue().equalsIgnoreCase(value)) {
return ClientAuthenticationMethod.NONE;
}
return null;
}
}
static final class AuthorizationGrantTypeConverter extends StdConverter<JsonNode, AuthorizationGrantType> {
@Override
public AuthorizationGrantType convert(JsonNode jsonNode) {
String value = findStringValue(jsonNode, "value");
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.AUTHORIZATION_CODE;
} else if (AuthorizationGrantType.IMPLICIT.getValue().equalsIgnoreCase(value)) {
}
if (AuthorizationGrantType.IMPLICIT.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.IMPLICIT;
} else if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) {
}
if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.CLIENT_CREDENTIALS;
} else if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) {
}
if (AuthorizationGrantType.PASSWORD.getValue().equalsIgnoreCase(value)) {
return AuthorizationGrantType.PASSWORD;
}
return null;
}
}
static final class AuthenticationMethodConverter extends StdConverter<JsonNode, AuthenticationMethod> {
@Override
public AuthenticationMethod convert(JsonNode jsonNode) {
String value = findStringValue(jsonNode, "value");
String value = JsonNodeUtils.findStringValue(jsonNode, "value");
if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) {
return AuthenticationMethod.HEADER;
} else if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) {
}
if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) {
return AuthenticationMethod.FORM;
} else if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) {
}
if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) {
return AuthenticationMethod.QUERY;
}
return null;
}
}
}

View File

@@ -13,19 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A {@code JsonDeserializer} for {@link Collections#unmodifiableMap(Map)}.
*
@@ -49,4 +50,5 @@ final class UnmodifiableMapDeserializer extends JsonDeserializer<Map<?, ?>> {
}
return Collections.unmodifiableMap(result);
}
}

View File

@@ -13,18 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.jackson2;
import java.util.Collections;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.util.Collections;
import java.util.Map;
/**
* This mixin class is used to serialize/deserialize {@link Collections#unmodifiableMap(Map)}.
* It also registers a custom deserializer {@link UnmodifiableMapDeserializer}.
* This mixin class is used to serialize/deserialize
* {@link Collections#unmodifiableMap(Map)}. It also registers a custom deserializer
* {@link UnmodifiableMapDeserializer}.
*
* @author Joe Grandja
* @since 5.3
@@ -39,4 +41,5 @@ abstract class UnmodifiableMapMixin {
@JsonCreator
UnmodifiableMapMixin(Map<?, ?> map) {
}
}

View File

@@ -13,18 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.util.function.Function;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import java.util.function.Function;
/**
*
* @author Joe Grandja
* @since 5.2
*/
@@ -32,7 +32,8 @@ class DefaultOidcIdTokenValidatorFactory implements Function<ClientRegistration,
@Override
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
return new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(), new OidcIdTokenValidator(clientRegistration));
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
new OidcIdTokenValidator(clientRegistration));
}
}

View File

@@ -13,8 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -43,26 +51,19 @@ import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.util.Assert;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
/**
* An implementation of an {@link AuthenticationProvider}
* for the OpenID Connect Core 1.0 Authorization Code Grant Flow.
* An implementation of an {@link AuthenticationProvider} for the OpenID Connect Core 1.0
* Authorization Code Grant Flow.
* <p>
* This {@link AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
* This {@link AuthenticationProvider} is responsible for authenticating an Authorization
* Code credential with the Authorization Server's Token Endpoint and if valid, exchanging
* it for an Access Token credential.
* <p>
* It will also obtain the user attributes of the End-User (Resource Owner)
* from the UserInfo Endpoint using an {@link OAuth2UserService},
* which will create a {@code Principal} in the form of an {@link OidcUser}.
* The {@code OidcUser} is then associated to the {@link OAuth2LoginAuthenticationToken}
* to complete the authentication.
* It will also obtain the user attributes of the End-User (Resource Owner) from the
* UserInfo Endpoint using an {@link OAuth2UserService}, which will create a
* {@code Principal} in the form of an {@link OidcUser}. The {@code OidcUser} is then
* associated to the {@link OAuth2LoginAuthenticationToken} to complete the
* authentication.
*
* @author Joe Grandja
* @author Mark Heckler
@@ -72,29 +73,43 @@ import java.util.Map;
* @see OidcUserService
* @see OidcUser
* @see OidcIdTokenDecoderFactory
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth">Section 3.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest">Section 3.1.3.1 Token Request</a>
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse">Section 3.1.3.3 Token Response</a>
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth">Section 3.1
* Authorization Code Grant Flow</a>
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest">Section 3.1.3.1
* Token Request</a>
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse">Section 3.1.3.3
* Token Response</a>
*/
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private final OAuth2UserService<OidcUserRequest, OidcUser> userService;
private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);
/**
* Constructs an {@code OidcAuthorizationCodeAuthenticationProvider} using the provided parameters.
*
* @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint
* @param userService the service used for obtaining the user attributes of the End-User from the UserInfo Endpoint
* Constructs an {@code OidcAuthorizationCodeAuthenticationProvider} using the
* provided parameters.
* @param accessTokenResponseClient the client used for requesting the access token
* credential from the Token Endpoint
* @param userService the service used for obtaining the user attributes of the
* End-User from the UserInfo Endpoint
*/
public OidcAuthorizationCodeAuthenticationProvider(
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OidcUserRequest, OidcUser> userService) {
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
OAuth2UserService<OidcUserRequest, OidcUser> userService) {
Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
Assert.notNull(userService, "userService cannot be null");
this.accessTokenResponseClient = accessTokenResponseClient;
@@ -103,97 +118,95 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication =
(OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request -
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (!authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)) {
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains(OidcScopes.OPENID)) {
// This is NOT an OpenID Connect Authentication Request so return null
// and let OAuth2LoginAuthenticationProvider handle it instead
return null;
}
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationRequest();
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationResponse();
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest();
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthenticationException(
authorizationResponse.getError(), authorizationResponse.getError().toString());
throw new OAuth2AuthenticationException(authorizationResponse.getError(),
authorizationResponse.getError().toString());
}
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessTokenResponse accessTokenResponse;
try {
accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);
ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) {
OAuth2Error invalidIdTokenError = new OAuth2Error(
INVALID_ID_TOKEN_ERROR_CODE,
"Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(),
null);
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE,
"Missing (required) ID Token in Token Response for Client Registration: "
+ clientRegistration.getRegistrationId(),
null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
}
OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
// Validate nonce
String requestNonce = authorizationRequest.getAttribute(OidcParameterNames.NONCE);
if (requestNonce != null) {
String nonceHash;
try {
nonceHash = createHash(requestNonce);
} catch (NoSuchAlgorithmException e) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String nonceHashClaim = idToken.getNonce();
if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(
clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oidcUser.getAuthorities());
validateNonce(authorizationRequest, idToken);
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
accessTokenResponse.getAccessToken(), idToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oidcUser.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
oidcUser,
mappedAuthorities,
accessTokenResponse.getAccessToken(),
accessTokenResponse.getRefreshToken());
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), oidcUser, mappedAuthorities,
accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
private OAuth2AccessTokenResponse getResponse(OAuth2LoginAuthenticationToken authorizationCodeAuthentication) {
try {
return this.accessTokenResponseClient.getTokenResponse(
new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange()));
}
catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private void validateNonce(OAuth2AuthorizationRequest authorizationRequest, OidcIdToken idToken) {
String requestNonce = authorizationRequest.getAttribute(OidcParameterNames.NONCE);
if (requestNonce == null) {
return;
}
String nonceHash = getNonceHash(requestNonce);
String nonceHashClaim = idToken.getNonce();
if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
private String getNonceHash(String requestNonce) {
try {
return createHash(requestNonce);
}
catch (NoSuchAlgorithmException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
/**
* Sets the {@link JwtDecoderFactory} used for {@link OidcIdToken} signature verification.
* The factory returns a {@link JwtDecoder} associated to the provided {@link ClientRegistration}.
*
* Sets the {@link JwtDecoderFactory} used for {@link OidcIdToken} signature
* verification. The factory returns a {@link JwtDecoder} associated to the provided
* {@link ClientRegistration}.
* @param jwtDecoderFactory the {@link JwtDecoderFactory} used for {@link OidcIdToken}
* signature verification
* @since 5.2
* @param jwtDecoderFactory the {@link JwtDecoderFactory} used for {@link OidcIdToken} signature verification
*/
public final void setJwtDecoderFactory(JwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
@@ -201,10 +214,11 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
}
/**
* Sets the {@link GrantedAuthoritiesMapper} used for mapping {@link OidcUser#getAuthorities()}}
* to a new set of authorities which will be associated to the {@link OAuth2LoginAuthenticationToken}.
*
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities
* Sets the {@link GrantedAuthoritiesMapper} used for mapping
* {@link OidcUser#getAuthorities()}} to a new set of authorities which will be
* associated to the {@link OAuth2LoginAuthenticationToken}.
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
* user's authorities
*/
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
@@ -216,17 +230,24 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
}
private OidcIdToken createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) {
private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
OAuth2AccessTokenResponse accessTokenResponse) {
JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
Jwt jwt;
Jwt jwt = getJwt(accessTokenResponse, jwtDecoder);
OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
jwt.getClaims());
return idToken;
}
private Jwt getJwt(OAuth2AccessTokenResponse accessTokenResponse, JwtDecoder jwtDecoder) {
try {
jwt = jwtDecoder.decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
} catch (JwtException ex) {
Map<String, Object> parameters = accessTokenResponse.getAdditionalParameters();
return jwtDecoder.decode((String) parameters.get(OidcParameterNames.ID_TOKEN));
}
catch (JwtException ex) {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
}
OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
return idToken;
}
static String createHash(String nonce) throws NoSuchAlgorithmException {
@@ -234,4 +255,5 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@@ -13,8 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@@ -41,28 +51,22 @@ import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collection;
import java.util.Map;
/**
* An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login,
* which leverages the OAuth 2.0 Authorization Code Grant Flow.
* An implementation of an
* {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth
* 2.0 Login, which leverages the OAuth 2.0 Authorization Code Grant Flow.
* <p>
* This {@link org.springframework.security.authentication.AuthenticationProvider} is responsible for authenticating
* an Authorization Code credential with the Authorization Server's Token Endpoint
* and if valid, exchanging it for an Access Token credential.
* This {@link org.springframework.security.authentication.AuthenticationProvider} is
* responsible for authenticating an Authorization Code credential with the Authorization
* Server's Token Endpoint and if valid, exchanging it for an Access Token credential.
* <p>
* It will also obtain the user attributes of the End-User (Resource Owner)
* from the UserInfo Endpoint using an {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService},
* which will create a {@code Principal} in the form of an {@link OAuth2User}.
* The {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken}
* to complete the authentication.
* It will also obtain the user attributes of the End-User (Resource Owner) from the
* UserInfo Endpoint using an
* {@link org.springframework.security.oauth2.client.userinfo.OAuth2UserService}, which
* will create a {@code Principal} in the form of an {@link OAuth2User}. The
* {@code OAuth2User} is then associated to the {@link OAuth2LoginAuthenticationToken} to
* complete the authentication.
*
* @author Rob Winch
* @author Mark Heckler
@@ -72,22 +76,28 @@ import java.util.Map;
* @see ReactiveOAuth2UserService
* @see OAuth2User
* @see ReactiveOidcIdTokenDecoderFactory
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section
* 4.1 Authorization Code Grant Flow</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token
* Request</a>
* @see <a target="_blank" href=
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token
* Response</a>
*/
public class OidcAuthorizationCodeReactiveAuthenticationManager implements
ReactiveAuthenticationManager {
public class OidcAuthorizationCodeReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
private static final String INVALID_NONCE_ERROR_CODE = "invalid_nonce";
private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
private final ReactiveOAuth2UserService<OidcUserRequest, OidcUser> userService;
private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
private GrantedAuthoritiesMapper authoritiesMapper = ((authorities) -> authorities);
private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
@@ -104,53 +114,51 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.defer(() -> {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (!authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest().getScopes().contains("openid")) {
// Section 3.1.2.1 Authentication Request -
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
// scope REQUIRED. OpenID Connect requests MUST contain the "openid" scope
// value.
if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains("openid")) {
// This is an OpenID Connect Authentication Request so return empty
// and let OAuth2LoginReactiveAuthenticationManager handle it instead
return Mono.empty();
}
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationRequest();
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest();
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
return Mono.error(new OAuth2AuthenticationException(
authorizationResponse.getError(), authorizationResponse.getError().toString()));
return Mono.error(new OAuth2AuthenticationException(authorizationResponse.getError(),
authorizationResponse.getError().toString()));
}
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
return Mono.error(new OAuth2AuthenticationException(
oauth2Error, oauth2Error.toString()));
return Mono.error(new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()));
}
OAuth2AuthorizationCodeGrantRequest authzRequest = new OAuth2AuthorizationCodeGrantRequest(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange());
return this.accessTokenResponseClient.getTokenResponse(authzRequest)
.flatMap(accessTokenResponse -> authenticationResult(authorizationCodeAuthentication, accessTokenResponse))
.onErrorMap(OAuth2AuthorizationException.class, e -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()))
.onErrorMap(JwtException.class, e -> {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, e.getMessage(), null);
return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), e);
return this.accessTokenResponseClient.getTokenResponse(authzRequest).flatMap(
(accessTokenResponse) -> authenticationResult(authorizationCodeAuthentication, accessTokenResponse))
.onErrorMap(OAuth2AuthorizationException.class,
(e) -> new OAuth2AuthenticationException(e.getError(), e.getError().toString()))
.onErrorMap(JwtException.class, (e) -> {
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, e.getMessage(),
null);
return new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(),
e);
});
});
}
/**
* Sets the {@link ReactiveJwtDecoderFactory} used for {@link OidcIdToken} signature verification.
* The factory returns a {@link ReactiveJwtDecoder} associated to the provided {@link ClientRegistration}.
*
* Sets the {@link ReactiveJwtDecoderFactory} used for {@link OidcIdToken} signature
* verification. The factory returns a {@link ReactiveJwtDecoder} associated to the
* provided {@link ClientRegistration}.
* @param jwtDecoderFactory the {@link ReactiveJwtDecoderFactory} used for
* {@link OidcIdToken} signature verification
* @since 5.2
* @param jwtDecoderFactory the {@link ReactiveJwtDecoderFactory} used for {@link OidcIdToken} signature verification
*/
public final void setJwtDecoderFactory(ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory) {
Assert.notNull(jwtDecoderFactory, "jwtDecoderFactory cannot be null");
@@ -158,66 +166,64 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
}
/**
* Sets the {@link GrantedAuthoritiesMapper} used for mapping {@link OidcUser#getAuthorities()}
* to a new set of authorities which will be associated to the {@link OAuth2LoginAuthenticationToken}.
*
* Sets the {@link GrantedAuthoritiesMapper} used for mapping
* {@link OidcUser#getAuthorities()} to a new set of authorities which will be
* associated to the {@link OAuth2LoginAuthenticationToken}.
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the
* user's authorities
* @since 5.4
* @param authoritiesMapper the {@link GrantedAuthoritiesMapper} used for mapping the user's authorities
*/
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
this.authoritiesMapper = authoritiesMapper;
}
private Mono<OAuth2LoginAuthenticationToken> authenticationResult(OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OAuth2AccessTokenResponse accessTokenResponse) {
private Mono<OAuth2LoginAuthenticationToken> authenticationResult(
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication,
OAuth2AccessTokenResponse accessTokenResponse) {
OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) {
OAuth2Error invalidIdTokenError = new OAuth2Error(
INVALID_ID_TOKEN_ERROR_CODE,
"Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(),
OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE,
"Missing (required) ID Token in Token Response for Client Registration: "
+ clientRegistration.getRegistrationId(),
null);
return Mono.error(new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString()));
}
// @formatter:off
return createOidcToken(clientRegistration, accessTokenResponse)
.doOnNext(idToken -> validateNonce(authorizationCodeAuthentication, idToken))
.map(idToken -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters))
.doOnNext((idToken) -> validateNonce(authorizationCodeAuthentication, idToken))
.map((idToken) -> new OidcUserRequest(clientRegistration, accessToken, idToken, additionalParameters))
.flatMap(this.userService::loadUser)
.map(oauth2User -> {
Collection<? extends GrantedAuthority> mappedAuthorities =
this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
return new OAuth2LoginAuthenticationToken(
authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(),
oauth2User,
mappedAuthorities,
accessToken,
accessTokenResponse.getRefreshToken());
.map((oauth2User) -> {
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
return new OAuth2LoginAuthenticationToken(authorizationCodeAuthentication.getClientRegistration(),
authorizationCodeAuthentication.getAuthorizationExchange(), oauth2User, mappedAuthorities,
accessToken, accessTokenResponse.getRefreshToken());
});
// @formatter:on
}
private Mono<OidcIdToken> createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) {
private Mono<OidcIdToken> createOidcToken(ClientRegistration clientRegistration,
OAuth2AccessTokenResponse accessTokenResponse) {
ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
String rawIdToken = (String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN);
// @formatter:off
return jwtDecoder.decode(rawIdToken)
.map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()));
.map((jwt) ->
new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims())
);
// @formatter:on
}
private static Mono<OidcIdToken> validateNonce(OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OidcIdToken idToken) {
String requestNonce = authorizationCodeAuthentication.getAuthorizationExchange()
.getAuthorizationRequest().getAttribute(OidcParameterNames.NONCE);
private static Mono<OidcIdToken> validateNonce(
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OidcIdToken idToken) {
String requestNonce = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest()
.getAttribute(OidcParameterNames.NONCE);
if (requestNonce != null) {
String nonceHash;
try {
nonceHash = createHash(requestNonce);
} catch (NoSuchAlgorithmException e) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
String nonceHash = getNonceHash(requestNonce);
String nonceHashClaim = idToken.getNonce();
if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
@@ -228,9 +234,20 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
return Mono.just(idToken);
}
private static String getNonceHash(String requestNonce) {
try {
return createHash(requestNonce);
}
catch (NoSuchAlgorithmException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_NONCE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
static String createHash(String nonce) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(nonce.getBytes(StandardCharsets.US_ASCII));
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
}
}

View File

@@ -13,16 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.core.convert.TypeDescriptor;
@@ -47,13 +50,10 @@ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecretKey;
/**
* A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder}
* used for {@link OidcIdToken} signature verification.
* The provided {@link JwtDecoder} is associated to a specific {@link ClientRegistration}.
* A {@link JwtDecoderFactory factory} that provides a {@link JwtDecoder} used for
* {@link OidcIdToken} signature verification. The provided {@link JwtDecoder} is
* associated to a specific {@link ClientRegistration}.
*
* @author Joe Grandja
* @author Rafael Dominguez
@@ -64,26 +64,36 @@ import static org.springframework.security.oauth2.jwt.NimbusJwtDecoder.withSecre
* @see OidcIdToken
*/
public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
{
put(MacAlgorithm.HS256, "HmacSHA256");
put(MacAlgorithm.HS384, "HmacSHA384");
put(MacAlgorithm.HS512, "HmacSHA512");
}
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
static {
Map<JwsAlgorithm, String> mappings = new HashMap<>();
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
};
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
new ClaimTypeConverter(createDefaultClaimTypeConverters());
private static final ClaimTypeConverter DEFAULT_CLAIM_TYPE_CONVERTER = new ClaimTypeConverter(
createDefaultClaimTypeConverters());
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = new DefaultOidcIdTokenValidatorFactory();
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = (
clientRegistration) -> SignatureAlgorithm.RS256;
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory = (
clientRegistration) -> DEFAULT_CLAIM_TYPE_CONVERTER;
/**
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcIdToken}.
*
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames claim name}
* Returns the default {@link Converter}'s used for type conversion of claim values
* for an {@link OidcIdToken}.
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames
* claim name}
*/
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
@@ -92,34 +102,34 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
claimTypeConverters.put(IdTokenClaimNames.NONCE, stringConverter);
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.AMR, collectionStringConverter);
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
return claimTypeConverters;
Map<String, Converter<Object, ?>> converters = new HashMap<>();
converters.put(IdTokenClaimNames.ISS, urlConverter);
converters.put(IdTokenClaimNames.AUD, collectionStringConverter);
converters.put(IdTokenClaimNames.NONCE, stringConverter);
converters.put(IdTokenClaimNames.EXP, instantConverter);
converters.put(IdTokenClaimNames.IAT, instantConverter);
converters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
converters.put(IdTokenClaimNames.AMR, collectionStringConverter);
converters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
converters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
converters.put(StandardClaimNames.UPDATED_AT, instantConverter);
return converters;
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
return (source) -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor,
targetDescriptor);
}
@Override
public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), (key) -> {
NimbusJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
this.claimTypeConverterFactory.apply(clientRegistration);
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter = this.claimTypeConverterFactory
.apply(clientRegistration);
if (claimTypeConverter != null) {
jwtDecoder.setClaimSetConverter(claimTypeConverter);
}
@@ -134,68 +144,66 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
//
// 6. If the ID Token is received via direct communication between the Client
// and the Token Endpoint (which it is in this flow),
// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
// the TLS server validation MAY be used to validate the issuer in place of
// checking the token signature.
// The Client MUST validate the signature of all other ID Tokens according to
// JWS [JWS]
// using the algorithm specified in the JWT alg Header Parameter.
// The Client MUST use the keys provided by the Issuer.
//
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by
// the Client
// in the id_token_signed_response_alg parameter during Registration.
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured the JwkSet URI.",
null
);
OAuth2Error oauth2Error = new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Check to ensure you have configured the JwkSet URI.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return withJwkSetUri(jwkSetUri).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
}
if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as
// HS256, HS384, or HS512,
// the octets of the UTF-8 representation of the client_secret
// corresponding to the client_id contained in the aud (audience) Claim
// are used as the key to validate the signature.
// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
// For MAC based algorithms, the behavior is unspecified if the aud is
// multi-valued or
// if an azp value is present that is different than the aud value.
String clientSecret = clientRegistration.getClientSecret();
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured the client secret.",
null
);
OAuth2Error oauth2Error = new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Check to ensure you have configured the client secret.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
SecretKeySpec secretKeySpec = new SecretKeySpec(
clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
return NimbusJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
}
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured a valid JWS Algorithm: '" +
jwsAlgorithm + "'",
null
);
OAuth2Error oauth2Error = new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
/**
* Sets the factory that provides an {@link OAuth2TokenValidator}, which is used by the {@link JwtDecoder}.
* The default composes {@link JwtTimestampValidator} and {@link OidcIdTokenValidator}.
*
* @param jwtValidatorFactory the factory that provides an {@link OAuth2TokenValidator}
* Sets the factory that provides an {@link OAuth2TokenValidator}, which is used by
* the {@link JwtDecoder}. The default composes {@link JwtTimestampValidator} and
* {@link OidcIdTokenValidator}.
* @param jwtValidatorFactory the factory that provides an
* {@link OAuth2TokenValidator}
*/
public void setJwtValidatorFactory(Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
@@ -204,11 +212,11 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
/**
* Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* used for the signature or MAC on the {@link OidcIdToken ID Token}.
* The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
*
* @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* for a specific {@link ClientRegistration client}
* used for the signature or MAC on the {@link OidcIdToken ID Token}. The default
* resolves to {@link SignatureAlgorithm#RS256 RS256} for all
* {@link ClientRegistration clients}.
* @param jwsAlgorithmResolver the resolver that provides the expected
* {@link JwsAlgorithm JWS algorithm} for a specific {@link ClientRegistration client}
*/
public void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
@@ -216,14 +224,17 @@ public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<Client
}
/**
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcIdToken}.
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
*
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
* of claim values for a specific {@link ClientRegistration client}
* Sets the factory that provides a {@link Converter} used for type conversion of
* claim values for an {@link OidcIdToken}. The default is {@link ClaimTypeConverter}
* for all {@link ClientRegistration clients}.
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used
* for type conversion of claim values for a specific {@link ClientRegistration
* client}
*/
public void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
public void setClaimTypeConverterFactory(
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
this.claimTypeConverterFactory = claimTypeConverterFactory;
}
}

View File

@@ -13,8 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.net.URL;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -26,30 +36,27 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.net.URL;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* An {@link OAuth2TokenValidator} responsible for
* validating the claims in an {@link OidcIdToken ID Token}.
* An {@link OAuth2TokenValidator} responsible for validating the claims in an
* {@link OidcIdToken ID Token}.
*
* @author Rob Winch
* @author Joe Grandja
* @since 5.1
* @see OAuth2TokenValidator
* @see Jwt
* @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">ID Token Validation</a>
* @see <a target="_blank" href=
* "https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation">ID Token
* Validation</a>
*/
public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
private static final Duration DEFAULT_CLOCK_SKEW = Duration.ofSeconds(60);
private final ClientRegistration clientRegistration;
private Duration clockSkew = DEFAULT_CLOCK_SKEW;
private Clock clock = Clock.systemUTC();
public OidcIdTokenValidator(ClientRegistration clientRegistration) {
@@ -59,75 +66,68 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
@Override
public OAuth2TokenValidatorResult validate(Jwt idToken) {
// 3.1.3.7 ID Token Validation
// 3.1.3.7 ID Token Validation
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
Map<String, Object> invalidClaims = validateRequiredClaims(idToken);
if (!invalidClaims.isEmpty()) {
return OAuth2TokenValidatorResult.failure(invalidIdToken(invalidClaims));
}
// 2. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
// 2. The Issuer Identifier for the OpenID Provider (which is typically obtained
// during Discovery)
// MUST exactly match the value of the iss (issuer) Claim.
String metadataIssuer = this.clientRegistration.getProviderDetails().getIssuerUri();
if (metadataIssuer != null && !Objects.equals(metadataIssuer, idToken.getIssuer().toExternalForm())) {
invalidClaims.put(IdTokenClaimNames.ISS, idToken.getIssuer());
}
// 3. The Client MUST validate that the aud (audience) Claim contains its client_id value
// 3. The Client MUST validate that the aud (audience) Claim contains its
// client_id value
// registered at the Issuer identified by the iss (issuer) Claim as an audience.
// The aud (audience) Claim MAY contain an array with more than one element.
// The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience,
// The ID Token MUST be rejected if the ID Token does not list the Client as a
// valid audience,
// or if it contains additional audiences not trusted by the Client.
if (!idToken.getAudience().contains(this.clientRegistration.getClientId())) {
invalidClaims.put(IdTokenClaimNames.AUD, idToken.getAudience());
}
// 4. If the ID Token contains multiple audiences,
// the Client SHOULD verify that an azp Claim is present.
String authorizedParty = idToken.getClaimAsString(IdTokenClaimNames.AZP);
if (idToken.getAudience().size() > 1 && authorizedParty == null) {
invalidClaims.put(IdTokenClaimNames.AZP, authorizedParty);
}
// 5. If an azp (authorized party) Claim is present,
// the Client SHOULD verify that its client_id is the Claim Value.
if (authorizedParty != null && !authorizedParty.equals(this.clientRegistration.getClientId())) {
invalidClaims.put(IdTokenClaimNames.AZP, authorizedParty);
}
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the
// Client
// in the id_token_signed_response_alg parameter during Registration.
// TODO Depends on gh-4413
// 9. The current time MUST be before the time represented by the exp Claim.
Instant now = Instant.now(this.clock);
if (now.minus(this.clockSkew).isAfter(idToken.getExpiresAt())) {
invalidClaims.put(IdTokenClaimNames.EXP, idToken.getExpiresAt());
}
// 10. The iat Claim can be used to reject tokens that were issued too far away from the current time,
// 10. The iat Claim can be used to reject tokens that were issued too far away
// from the current time,
// limiting the amount of time that nonces need to be stored to prevent attacks.
// The acceptable range is Client specific.
if (now.plus(this.clockSkew).isBefore(idToken.getIssuedAt())) {
invalidClaims.put(IdTokenClaimNames.IAT, idToken.getIssuedAt());
}
if (!invalidClaims.isEmpty()) {
return OAuth2TokenValidatorResult.failure(invalidIdToken(invalidClaims));
}
return OAuth2TokenValidatorResult.success();
}
/**
* Sets the maximum acceptable clock skew. The default is 60 seconds.
* The clock skew is used when validating the {@link JwtClaimNames#EXP exp}
* and {@link JwtClaimNames#IAT iat} claims.
*
* @since 5.2
* Sets the maximum acceptable clock skew. The default is 60 seconds. The clock skew
* is used when validating the {@link JwtClaimNames#EXP exp} and
* {@link JwtClaimNames#IAT iat} claims.
* @param clockSkew the maximum acceptable clock skew
* @since 5.2
*/
public void setClockSkew(Duration clockSkew) {
Assert.notNull(clockSkew, "clockSkew cannot be null");
@@ -136,12 +136,10 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
}
/**
* Sets the {@link Clock} used in {@link Instant#now(Clock)}
* when validating the {@link JwtClaimNames#EXP exp}
* and {@link JwtClaimNames#IAT iat} claims.
*
* @since 5.3
* Sets the {@link Clock} used in {@link Instant#now(Clock)} when validating the
* {@link JwtClaimNames#EXP exp} and {@link JwtClaimNames#IAT iat} claims.
* @param clock the clock
* @since 5.3
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
@@ -149,14 +147,12 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
}
private static OAuth2Error invalidIdToken(Map<String, Object> invalidClaims) {
return new OAuth2Error("invalid_id_token",
"The ID Token contains invalid claims: " + invalidClaims,
return new OAuth2Error("invalid_id_token", "The ID Token contains invalid claims: " + invalidClaims,
"https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation");
}
private static Map<String, Object> validateRequiredClaims(Jwt idToken) {
Map<String, Object> requiredClaims = new HashMap<>();
URL issuer = idToken.getIssuer();
if (issuer == null) {
requiredClaims.put(IdTokenClaimNames.ISS, issuer);
@@ -177,7 +173,7 @@ public final class OidcIdTokenValidator implements OAuth2TokenValidator<Jwt> {
if (issuedAt == null) {
requiredClaims.put(IdTokenClaimNames.IAT, issuedAt);
}
return requiredClaims;
}
}

View File

@@ -13,16 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.authentication;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.core.convert.TypeDescriptor;
@@ -47,13 +50,10 @@ import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withJwkSetUri;
import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.withSecretKey;
/**
* A {@link ReactiveJwtDecoderFactory factory} that provides a {@link ReactiveJwtDecoder}
* used for {@link OidcIdToken} signature verification.
* The provided {@link ReactiveJwtDecoder} is associated to a specific {@link ClientRegistration}.
* used for {@link OidcIdToken} signature verification. The provided
* {@link ReactiveJwtDecoder} is associated to a specific {@link ClientRegistration}.
*
* @author Joe Grandja
* @author Rafael Dominguez
@@ -64,26 +64,36 @@ import static org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.w
* @see OidcIdToken
*/
public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
private static Map<JwsAlgorithm, String> jcaAlgorithmMappings = new HashMap<JwsAlgorithm, String>() {
{
put(MacAlgorithm.HS256, "HmacSHA256");
put(MacAlgorithm.HS384, "HmacSHA384");
put(MacAlgorithm.HS512, "HmacSHA512");
}
};
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
new ClaimTypeConverter(createDefaultClaimTypeConverters());
private static final Map<JwsAlgorithm, String> JCA_ALGORITHM_MAPPINGS;
static {
Map<JwsAlgorithm, String> mappings = new HashMap<JwsAlgorithm, String>();
mappings.put(MacAlgorithm.HS256, "HmacSHA256");
mappings.put(MacAlgorithm.HS384, "HmacSHA384");
mappings.put(MacAlgorithm.HS512, "HmacSHA512");
JCA_ALGORITHM_MAPPINGS = Collections.unmodifiableMap(mappings);
}
private static final ClaimTypeConverter DEFAULT_CLAIM_TYPE_CONVERTER = new ClaimTypeConverter(
createDefaultClaimTypeConverters());
private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = new DefaultOidcIdTokenValidatorFactory();
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = clientRegistration -> SignatureAlgorithm.RS256;
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
private Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver = (
clientRegistration) -> SignatureAlgorithm.RS256;
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory = (
clientRegistration) -> DEFAULT_CLAIM_TYPE_CONVERTER;
/**
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcIdToken}.
*
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames claim name}
* Returns the default {@link Converter}'s used for type conversion of claim values
* for an {@link OidcIdToken}.
* @return a {@link Map} of {@link Converter}'s keyed by {@link IdTokenClaimNames
* claim name}
*/
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
@@ -92,34 +102,34 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
Converter<Object, ?> stringConverter = getConverter(TypeDescriptor.valueOf(String.class));
Converter<Object, ?> collectionStringConverter = getConverter(
TypeDescriptor.collection(Collection.class, TypeDescriptor.valueOf(String.class)));
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
claimTypeConverters.put(IdTokenClaimNames.ISS, urlConverter);
claimTypeConverters.put(IdTokenClaimNames.AUD, collectionStringConverter);
claimTypeConverters.put(IdTokenClaimNames.NONCE, stringConverter);
claimTypeConverters.put(IdTokenClaimNames.EXP, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.IAT, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
claimTypeConverters.put(IdTokenClaimNames.AMR, collectionStringConverter);
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
claimTypeConverters.put(StandardClaimNames.UPDATED_AT, instantConverter);
return claimTypeConverters;
Map<String, Converter<Object, ?>> converters = new HashMap<>();
converters.put(IdTokenClaimNames.ISS, urlConverter);
converters.put(IdTokenClaimNames.AUD, collectionStringConverter);
converters.put(IdTokenClaimNames.NONCE, stringConverter);
converters.put(IdTokenClaimNames.EXP, instantConverter);
converters.put(IdTokenClaimNames.IAT, instantConverter);
converters.put(IdTokenClaimNames.AUTH_TIME, instantConverter);
converters.put(IdTokenClaimNames.AMR, collectionStringConverter);
converters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
converters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
converters.put(StandardClaimNames.UPDATED_AT, instantConverter);
return converters;
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
return (source) -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor,
targetDescriptor);
}
@Override
public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), (key) -> {
NimbusReactiveJwtDecoder jwtDecoder = buildDecoder(clientRegistration);
jwtDecoder.setJwtValidator(this.jwtValidatorFactory.apply(clientRegistration));
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
this.claimTypeConverterFactory.apply(clientRegistration);
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter = this.claimTypeConverterFactory
.apply(clientRegistration);
if (claimTypeConverter != null) {
jwtDecoder.setClaimSetConverter(claimTypeConverter);
}
@@ -134,68 +144,68 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
//
// 6. If the ID Token is received via direct communication between the Client
// and the Token Endpoint (which it is in this flow),
// the TLS server validation MAY be used to validate the issuer in place of checking the token signature.
// The Client MUST validate the signature of all other ID Tokens according to JWS [JWS]
// the TLS server validation MAY be used to validate the issuer in place of
// checking the token signature.
// The Client MUST validate the signature of all other ID Tokens according to
// JWS [JWS]
// using the algorithm specified in the JWT alg Header Parameter.
// The Client MUST use the keys provided by the Issuer.
//
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by the Client
// 7. The alg value SHOULD be the default of RS256 or the algorithm sent by
// the Client
// in the id_token_signed_response_alg parameter during Registration.
String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
if (!StringUtils.hasText(jwkSetUri)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured the JwkSet URI.",
null
);
OAuth2Error oauth2Error = new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Check to ensure you have configured the JwkSet URI.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return withJwkSetUri(jwkSetUri).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm).build();
} else if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).jwsAlgorithm((SignatureAlgorithm) jwsAlgorithm)
.build();
}
if (jwsAlgorithm != null && MacAlgorithm.class.isAssignableFrom(jwsAlgorithm.getClass())) {
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
//
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as HS256, HS384, or HS512,
// 8. If the JWT alg Header Parameter uses a MAC based algorithm such as
// HS256, HS384, or HS512,
// the octets of the UTF-8 representation of the client_secret
// corresponding to the client_id contained in the aud (audience) Claim
// are used as the key to validate the signature.
// For MAC based algorithms, the behavior is unspecified if the aud is multi-valued or
// For MAC based algorithms, the behavior is unspecified if the aud is
// multi-valued or
// if an azp value is present that is different than the aud value.
String clientSecret = clientRegistration.getClientSecret();
if (!StringUtils.hasText(clientSecret)) {
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured the client secret.",
null
);
OAuth2Error oauth2Error = new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Check to ensure you have configured the client secret.",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
SecretKeySpec secretKeySpec = new SecretKeySpec(
clientSecret.getBytes(StandardCharsets.UTF_8), jcaAlgorithmMappings.get(jwsAlgorithm));
return withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm).build();
SecretKeySpec secretKeySpec = new SecretKeySpec(clientSecret.getBytes(StandardCharsets.UTF_8),
JCA_ALGORITHM_MAPPINGS.get(jwsAlgorithm));
return NimbusReactiveJwtDecoder.withSecretKey(secretKeySpec).macAlgorithm((MacAlgorithm) jwsAlgorithm)
.build();
}
OAuth2Error oauth2Error = new OAuth2Error(
MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '" +
clientRegistration.getRegistrationId() +
"'. Check to ensure you have configured a valid JWS Algorithm: '" +
jwsAlgorithm + "'",
null
);
OAuth2Error oauth2Error = new OAuth2Error(MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
"Failed to find a Signature Verifier for Client Registration: '"
+ clientRegistration.getRegistrationId()
+ "'. Check to ensure you have configured a valid JWS Algorithm: '" + jwsAlgorithm + "'",
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
/**
* Sets the factory that provides an {@link OAuth2TokenValidator}, which is used by the {@link ReactiveJwtDecoder}.
* The default composes {@link JwtTimestampValidator} and {@link OidcIdTokenValidator}.
*
* @param jwtValidatorFactory the factory that provides an {@link OAuth2TokenValidator}
* Sets the factory that provides an {@link OAuth2TokenValidator}, which is used by
* the {@link ReactiveJwtDecoder}. The default composes {@link JwtTimestampValidator}
* and {@link OidcIdTokenValidator}.
* @param jwtValidatorFactory the factory that provides an
* {@link OAuth2TokenValidator}
*/
public void setJwtValidatorFactory(Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null");
@@ -204,11 +214,11 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
/**
* Sets the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* used for the signature or MAC on the {@link OidcIdToken ID Token}.
* The default resolves to {@link SignatureAlgorithm#RS256 RS256} for all {@link ClientRegistration clients}.
*
* @param jwsAlgorithmResolver the resolver that provides the expected {@link JwsAlgorithm JWS algorithm}
* for a specific {@link ClientRegistration client}
* used for the signature or MAC on the {@link OidcIdToken ID Token}. The default
* resolves to {@link SignatureAlgorithm#RS256 RS256} for all
* {@link ClientRegistration clients}.
* @param jwsAlgorithmResolver the resolver that provides the expected
* {@link JwsAlgorithm JWS algorithm} for a specific {@link ClientRegistration client}
*/
public void setJwsAlgorithmResolver(Function<ClientRegistration, JwsAlgorithm> jwsAlgorithmResolver) {
Assert.notNull(jwsAlgorithmResolver, "jwsAlgorithmResolver cannot be null");
@@ -216,14 +226,17 @@ public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecod
}
/**
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcIdToken}.
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
*
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
* of claim values for a specific {@link ClientRegistration client}
* Sets the factory that provides a {@link Converter} used for type conversion of
* claim values for an {@link OidcIdToken}. The default is {@link ClaimTypeConverter}
* for all {@link ClientRegistration clients}.
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used
* for type conversion of claim values for a specific {@link ClientRegistration
* client}
*/
public void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
public void setClaimTypeConverterFactory(
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
this.claimTypeConverterFactory = claimTypeConverterFactory;
}
}

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support classes and interfaces for authenticating and authorizing a client
* with an OpenID Connect 1.0 Provider using a specific authorization grant flow.
* Support classes and interfaces for authenticating and authorizing a client with an
* OpenID Connect 1.0 Provider using a specific authorization grant flow.
*/
package org.springframework.security.oauth2.client.oidc.authentication;

View File

@@ -13,8 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.userinfo;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
@@ -36,17 +46,10 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
/**
* An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect 1.0 Provider's.
* An implementation of an {@link ReactiveOAuth2UserService} that supports OpenID Connect
* 1.0 Provider's.
*
* @author Rob Winch
* @since 5.1
@@ -56,29 +59,28 @@ import java.util.function.Function;
* @see DefaultOidcUser
* @see OidcUserInfo
*/
public class OidcReactiveOAuth2UserService implements
ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
public class OidcReactiveOAuth2UserService implements ReactiveOAuth2UserService<OidcUserRequest, OidcUser> {
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
new ClaimTypeConverter(createDefaultClaimTypeConverters());
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER = new ClaimTypeConverter(
createDefaultClaimTypeConverters());
private ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultReactiveOAuth2UserService();
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory = (
clientRegistration) -> DEFAULT_CLAIM_TYPE_CONVERTER;
/**
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcUserInfo}.
* Returns the default {@link Converter}'s used for type conversion of claim values
* for an {@link OidcUserInfo}.
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames
* claim name}
* @since 5.2
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames claim name}
*/
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
@@ -88,57 +90,63 @@ public class OidcReactiveOAuth2UserService implements
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
return (source) -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor,
targetDescriptor);
}
@Override
public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
// @formatter:off
return getUserInfo(userRequest)
.map(userInfo -> new OidcUserAuthority(userRequest.getIdToken(), userInfo))
.defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), null))
.map(authority -> {
OidcUserInfo userInfo = authority.getUserInfo();
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
OAuth2AccessToken token = userRequest.getAccessToken();
for (String scope : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
}
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
if (StringUtils.hasText(userNameAttributeName)) {
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
} else {
.map((userInfo) ->
new OidcUserAuthority(userRequest.getIdToken(), userInfo)
)
.defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), null))
.map((authority) -> {
OidcUserInfo userInfo = authority.getUserInfo();
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
OAuth2AccessToken token = userRequest.getAccessToken();
for (String scope : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
}
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
if (StringUtils.hasText(userNameAttributeName)) {
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo,
userNameAttributeName);
}
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
}
});
});
// @formatter:on
}
private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) {
if (!OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest)) {
return Mono.empty();
}
return this.oauth2UserService.loadUser(userRequest)
.map(OAuth2User::getAttributes)
.map(claims -> convertClaims(claims, userRequest.getClientRegistration()))
.map(OidcUserInfo::new)
.doOnNext(userInfo -> {
String subject = userInfo.getSubject();
if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
});
// @formatter:off
return this.oauth2UserService
.loadUser(userRequest)
.map(OAuth2User::getAttributes)
.map((claims) -> convertClaims(claims, userRequest.getClientRegistration()))
.map(OidcUserInfo::new)
.doOnNext((userInfo) -> {
String subject = userInfo.getSubject();
if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
});
// @formatter:on
}
private Map<String, Object> convertClaims(Map<String, Object> claims, ClientRegistration clientRegistration) {
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
this.claimTypeConverterFactory.apply(clientRegistration);
return claimTypeConverter != null ?
claimTypeConverter.convert(claims) :
DEFAULT_CLAIM_TYPE_CONVERTER.convert(claims);
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter = this.claimTypeConverterFactory
.apply(clientRegistration);
return (claimTypeConverter != null) ? claimTypeConverter.convert(claims)
: DEFAULT_CLAIM_TYPE_CONVERTER.convert(claims);
}
public void setOauth2UserService(ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
@@ -147,15 +155,18 @@ public class OidcReactiveOAuth2UserService implements
}
/**
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcUserInfo}.
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
*
* Sets the factory that provides a {@link Converter} used for type conversion of
* claim values for an {@link OidcUserInfo}. The default is {@link ClaimTypeConverter}
* for all {@link ClientRegistration clients}.
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used
* for type conversion of claim values for a specific {@link ClientRegistration
* client}
* @since 5.2
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
* of claim values for a specific {@link ClientRegistration client}
*/
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
public final void setClaimTypeConverterFactory(
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
this.claimTypeConverterFactory = claimTypeConverterFactory;
}
}

View File

@@ -13,20 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.userinfo;
import java.util.Collections;
import java.util.Map;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.Map;
/**
* Represents a request the {@link OidcUserService} uses
* when initiating a request to the UserInfo Endpoint.
* Represents a request the {@link OidcUserService} uses when initiating a request to the
* UserInfo Endpoint.
*
* @author Joe Grandja
* @since 5.0
@@ -36,33 +37,29 @@ import java.util.Map;
* @see OidcUserService
*/
public class OidcUserRequest extends OAuth2UserRequest {
private final OidcIdToken idToken;
/**
* Constructs an {@code OidcUserRequest} using the provided parameters.
*
* @param clientRegistration the client registration
* @param accessToken the access token credential
* @param idToken the ID Token
*/
public OidcUserRequest(ClientRegistration clientRegistration,
OAuth2AccessToken accessToken, OidcIdToken idToken) {
public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, OidcIdToken idToken) {
this(clientRegistration, accessToken, idToken, Collections.emptyMap());
}
/**
* Constructs an {@code OidcUserRequest} using the provided parameters.
*
* @since 5.1
* @param clientRegistration the client registration
* @param accessToken the access token credential
* @param idToken the ID Token
* @param additionalParameters the additional parameters, may be empty
* @since 5.1
*/
public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken,
OidcIdToken idToken, Map<String, Object> additionalParameters) {
public OidcUserRequest(ClientRegistration clientRegistration, OAuth2AccessToken accessToken, OidcIdToken idToken,
Map<String, Object> additionalParameters) {
super(clientRegistration, accessToken, additionalParameters);
Assert.notNull(idToken, "idToken cannot be null");
this.idToken = idToken;
@@ -70,10 +67,10 @@ public class OidcUserRequest extends OAuth2UserRequest {
/**
* Returns the {@link OidcIdToken ID Token} containing claims about the user.
*
* @return the {@link OidcIdToken} containing claims about the user.
*/
public OidcIdToken getIdToken() {
return this.idToken;
}
}

View File

@@ -30,13 +30,14 @@ import org.springframework.util.StringUtils;
final class OidcUserRequestUtils {
/**
* Determines if an {@link OidcUserRequest} should attempt to retrieve the user info endpoint. Will return true if
* all of the following are true:
* Determines if an {@link OidcUserRequest} should attempt to retrieve the user info
* endpoint. Will return true if all of the following are true:
*
* <ul>
* <li>The user info endpoint is defined on the ClientRegistration</li>
* <li>The Client Registration uses the {@link AuthorizationGrantType#AUTHORIZATION_CODE} and scopes in the
* access token are defined in the {@link ClientRegistration}</li>
* <li>The user info endpoint is defined on the ClientRegistration</li>
* <li>The Client Registration uses the
* {@link AuthorizationGrantType#AUTHORIZATION_CODE} and scopes in the access token
* are defined in the {@link ClientRegistration}</li>
* </ul>
* @param userRequest
* @return
@@ -44,27 +45,28 @@ final class OidcUserRequestUtils {
static boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
// Auto-disabled if UserInfo Endpoint URI is not provided
ClientRegistration clientRegistration = userRequest.getClientRegistration();
if (StringUtils.isEmpty(clientRegistration.getProviderDetails()
.getUserInfoEndpoint().getUri())) {
if (StringUtils.isEmpty(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())) {
return false;
}
// The Claims requested by the profile, email, address, and phone scope values
// are returned from the UserInfo Endpoint (as described in Section 5.3.2),
// when a response_type value is used that results in an Access Token being issued.
// However, when no Access Token is issued, which is the case for the response_type=id_token,
// when a response_type value is used that results in an Access Token being
// issued.
// However, when no Access Token is issued, which is the case for the
// response_type=id_token,
// the resulting Claims are returned in the ID Token.
// The Authorization Code Grant Flow, which is response_type=code, results in an Access Token being issued.
// The Authorization Code Grant Flow, which is response_type=code, results in an
// Access Token being issued.
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
// Return true if there is at least one match between the authorized scope(s) and UserInfo scope(s)
return CollectionUtils
.containsAny(userRequest.getAccessToken().getScopes(), userRequest.getClientRegistration().getScopes());
// Return true if there is at least one match between the authorized scope(s)
// and UserInfo scope(s)
return CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(),
userRequest.getClientRegistration().getScopes());
}
return false;
}
private OidcUserRequestUtils() {}
private OidcUserRequestUtils() {
}
}

View File

@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.oidc.userinfo;
import java.time.Instant;
@@ -29,6 +30,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
@@ -50,7 +52,8 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0 Provider's.
* An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0
* Provider's.
*
* @author Joe Grandja
* @since 5.0
@@ -61,25 +64,30 @@ import org.springframework.util.StringUtils;
* @see OidcUserInfo
*/
public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER =
new ClaimTypeConverter(createDefaultClaimTypeConverters());
private Set<String> accessibleScopes = new HashSet<>(Arrays.asList(
OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE));
private static final Converter<Map<String, Object>, Map<String, Object>> DEFAULT_CLAIM_TYPE_CONVERTER = new ClaimTypeConverter(
createDefaultClaimTypeConverters());
private Set<String> accessibleScopes = new HashSet<>(
Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE));
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = new DefaultOAuth2UserService();
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory =
clientRegistration -> DEFAULT_CLAIM_TYPE_CONVERTER;
private Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory = (
clientRegistration) -> DEFAULT_CLAIM_TYPE_CONVERTER;
/**
* Returns the default {@link Converter}'s used for type conversion of claim values for an {@link OidcUserInfo}.
* Returns the default {@link Converter}'s used for type conversion of claim values
* for an {@link OidcUserInfo}.
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames
* claim name}
* @since 5.2
* @return a {@link Map} of {@link Converter}'s keyed by {@link StandardClaimNames claim name}
*/
public static Map<String, Converter<Object, ?>> createDefaultClaimTypeConverters() {
Converter<Object, ?> booleanConverter = getConverter(TypeDescriptor.valueOf(Boolean.class));
Converter<Object, ?> instantConverter = getConverter(TypeDescriptor.valueOf(Instant.class));
Map<String, Converter<Object, ?>> claimTypeConverters = new HashMap<>();
claimTypeConverters.put(StandardClaimNames.EMAIL_VERIFIED, booleanConverter);
claimTypeConverters.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, booleanConverter);
@@ -88,8 +96,9 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
}
private static Converter<Object, ?> getConverter(TypeDescriptor targetDescriptor) {
final TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
return source -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor, targetDescriptor);
TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(Object.class);
return (source) -> ClaimConversionService.getSharedInstance().convert(source, sourceDescriptor,
targetDescriptor);
}
@Override
@@ -98,26 +107,16 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
OidcUserInfo userInfo = null;
if (this.shouldRetrieveUserInfo(userRequest)) {
OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest);
Map<String, Object> claims;
Converter<Map<String, Object>, Map<String, Object>> claimTypeConverter =
this.claimTypeConverterFactory.apply(userRequest.getClientRegistration());
if (claimTypeConverter != null) {
claims = claimTypeConverter.convert(oauth2User.getAttributes());
} else {
claims = DEFAULT_CLAIM_TYPE_CONVERTER.convert(oauth2User.getAttributes());
}
Map<String, Object> claims = getClaims(userRequest, oauth2User);
userInfo = new OidcUserInfo(claims);
// https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
// 1) The sub (subject) Claim MUST always be returned in the UserInfo Response
if (userInfo.getSubject() == null) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// 2) Due to the possibility of token substitution attacks (see Section 16.11),
// 2) Due to the possibility of token substitution attacks (see Section
// 16.11),
// the UserInfo Response is not guaranteed to be about the End-User
// identified by the sub (subject) element of the ID Token.
// The sub Claim in the UserInfo Response MUST be verified to exactly match
@@ -128,57 +127,63 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
}
Set<GrantedAuthority> authorities = new LinkedHashSet<>();
authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo));
OAuth2AccessToken token = userRequest.getAccessToken();
for (String authority : token.getScopes()) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return getUser(userRequest, userInfo, authorities);
}
OidcUser user;
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
if (StringUtils.hasText(userNameAttributeName)) {
user = new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
} else {
user = new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
private Map<String, Object> getClaims(OidcUserRequest userRequest, OAuth2User oauth2User) {
Converter<Map<String, Object>, Map<String, Object>> converter = this.claimTypeConverterFactory
.apply(userRequest.getClientRegistration());
if (converter != null) {
return converter.convert(oauth2User.getAttributes());
}
return DEFAULT_CLAIM_TYPE_CONVERTER.convert(oauth2User.getAttributes());
}
return user;
private OidcUser getUser(OidcUserRequest userRequest, OidcUserInfo userInfo, Set<GrantedAuthority> authorities) {
ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
String userNameAttributeName = providerDetails.getUserInfoEndpoint().getUserNameAttributeName();
if (StringUtils.hasText(userNameAttributeName)) {
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName);
}
return new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo);
}
private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
// Auto-disabled if UserInfo Endpoint URI is not provided
if (StringUtils.isEmpty(userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUri())) {
ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
if (StringUtils.isEmpty(providerDetails.getUserInfoEndpoint().getUri())) {
return false;
}
// The Claims requested by the profile, email, address, and phone scope values
// are returned from the UserInfo Endpoint (as described in Section 5.3.2),
// when a response_type value is used that results in an Access Token being issued.
// However, when no Access Token is issued, which is the case for the response_type=id_token,
// when a response_type value is used that results in an Access Token being
// issued.
// However, when no Access Token is issued, which is the case for the
// response_type=id_token,
// the resulting Claims are returned in the ID Token.
// The Authorization Code Grant Flow, which is response_type=code, results in an Access Token being issued.
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(
userRequest.getClientRegistration().getAuthorizationGrantType())) {
// Return true if there is at least one match between the authorized scope(s) and accessible scope(s)
return this.accessibleScopes.isEmpty() ||
CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.accessibleScopes);
// The Authorization Code Grant Flow, which is response_type=code, results in an
// Access Token being issued.
if (AuthorizationGrantType.AUTHORIZATION_CODE
.equals(userRequest.getClientRegistration().getAuthorizationGrantType())) {
// Return true if there is at least one match between the authorized scope(s)
// and accessible scope(s)
return this.accessibleScopes.isEmpty()
|| CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.accessibleScopes);
}
return false;
}
/**
* Sets the {@link OAuth2UserService} used when requesting the user info resource.
*
* @param oauth2UserService the {@link OAuth2UserService} used when requesting the
* user info resource.
* @since 5.1
* @param oauth2UserService the {@link OAuth2UserService} used when requesting the user info resource.
*/
public final void setOauth2UserService(OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService) {
Assert.notNull(oauth2UserService, "oauth2UserService cannot be null");
@@ -186,30 +191,34 @@ public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcU
}
/**
* Sets the factory that provides a {@link Converter} used for type conversion of claim values for an {@link OidcUserInfo}.
* The default is {@link ClaimTypeConverter} for all {@link ClientRegistration clients}.
*
* Sets the factory that provides a {@link Converter} used for type conversion of
* claim values for an {@link OidcUserInfo}. The default is {@link ClaimTypeConverter}
* for all {@link ClientRegistration clients}.
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used
* for type conversion of claim values for a specific {@link ClientRegistration
* client}
* @since 5.2
* @param claimTypeConverterFactory the factory that provides a {@link Converter} used for type conversion
* of claim values for a specific {@link ClientRegistration client}
*/
public final void setClaimTypeConverterFactory(Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
public final void setClaimTypeConverterFactory(
Function<ClientRegistration, Converter<Map<String, Object>, Map<String, Object>>> claimTypeConverterFactory) {
Assert.notNull(claimTypeConverterFactory, "claimTypeConverterFactory cannot be null");
this.claimTypeConverterFactory = claimTypeConverterFactory;
}
/**
* Sets the scope(s) that allow access to the user info resource.
* The default is {@link OidcScopes#PROFILE profile}, {@link OidcScopes#EMAIL email}, {@link OidcScopes#ADDRESS address} and {@link OidcScopes#PHONE phone}.
* The scope(s) are checked against the "granted" scope(s) associated to the {@link OidcUserRequest#getAccessToken() access token}
* to determine if the user info resource is accessible or not.
* If there is at least one match, the user info resource will be requested, otherwise it will not.
*
* @since 5.2
* Sets the scope(s) that allow access to the user info resource. The default is
* {@link OidcScopes#PROFILE profile}, {@link OidcScopes#EMAIL email},
* {@link OidcScopes#ADDRESS address} and {@link OidcScopes#PHONE phone}. The scope(s)
* are checked against the "granted" scope(s) associated to the
* {@link OidcUserRequest#getAccessToken() access token} to determine if the user info
* resource is accessible or not. If there is at least one match, the user info
* resource will be requested, otherwise it will not.
* @param accessibleScopes the scope(s) that allow access to the user info resource
* @since 5.2
*/
public final void setAccessibleScopes(Set<String> accessibleScopes) {
Assert.notNull(accessibleScopes, "accessibleScopes cannot be null");
this.accessibleScopes = accessibleScopes;
}
}

Some files were not shown because too many files have changed in this diff Show More