/* * Copyright 2020-2021 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.security.oauth2.server.authorization.authentication; import java.security.Principal; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2ErrorCodes; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; import org.springframework.security.oauth2.server.authorization.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import static org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient; /** * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Authorization Code Grant. * * @author Joe Grandja * @author Daniel Garnier-Moiroux * @since 0.0.1 * @see OAuth2AuthorizationCodeAuthenticationToken * @see OAuth2AccessTokenAuthenticationToken * @see OAuth2AuthorizationService * @see JwtEncoder * @see OAuth2TokenCustomizer * @see JwtEncodingContext * @see Section 4.1 Authorization Code Grant * @see Section 4.1.3 Access Token Request */ public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider { private static final OAuth2TokenType AUTHORIZATION_CODE_TOKEN_TYPE = new OAuth2TokenType(OAuth2ParameterNames.CODE); private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); private final OAuth2AuthorizationService authorizationService; private final JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer = (context) -> {}; private ProviderSettings providerSettings; /** * Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters. * * @param authorizationService the authorization service * @param jwtEncoder the jwt encoder */ public OAuth2AuthorizationCodeAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) { Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); this.authorizationService = authorizationService; this.jwtEncoder = jwtEncoder; } public final void setJwtCustomizer(OAuth2TokenCustomizer jwtCustomizer) { Assert.notNull(jwtCustomizer, "jwtCustomizer cannot be null"); this.jwtCustomizer = jwtCustomizer; } @Autowired(required = false) protected void setProviderSettings(ProviderSettings providerSettings) { this.providerSettings = providerSettings; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken) authentication; OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(authorizationCodeAuthentication); RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); OAuth2Authorization authorization = this.authorizationService.findByToken( authorizationCodeAuthentication.getCode(), AUTHORIZATION_CODE_TOKEN_TYPE); if (authorization == null) { throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); } OAuth2Authorization.Token authorizationCode = authorization.getToken(OAuth2AuthorizationCode.class); OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( OAuth2AuthorizationRequest.class.getName()); if (!registeredClient.getClientId().equals(authorizationRequest.getClientId())) { if (!authorizationCode.isInvalidated()) { // Invalidate the authorization code given that a different client is attempting to use it authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken()); this.authorizationService.save(authorization); } throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); } if (StringUtils.hasText(authorizationRequest.getRedirectUri()) && !authorizationRequest.getRedirectUri().equals(authorizationCodeAuthentication.getRedirectUri())) { throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); } if (authorizationCode.isInvalidated()) { throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); } String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null; Set authorizedScopes = authorization.getAttribute( OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME); JoseHeader.Builder headersBuilder = JwtUtils.headers(); JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims( registeredClient, issuer, authorization.getPrincipalName(), excludeOpenidIfNecessary(authorizedScopes)); // @formatter:off JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder) .registeredClient(registeredClient) .principal(authorization.getAttribute(Principal.class.getName())) .authorization(authorization) .authorizedScopes(authorizedScopes) .tokenType(OAuth2TokenType.ACCESS_TOKEN) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrant(authorizationCodeAuthentication) .build(); // @formatter:on this.jwtCustomizer.customize(context); JoseHeader headers = context.getHeaders().build(); JwtClaimsSet claims = context.getClaims().build(); Jwt jwtAccessToken = this.jwtEncoder.encode(headers, claims); OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, jwtAccessToken.getTokenValue(), jwtAccessToken.getIssuedAt(), jwtAccessToken.getExpiresAt(), excludeOpenidIfNecessary(authorizedScopes)); OAuth2RefreshToken refreshToken = null; if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) { refreshToken = OAuth2RefreshTokenAuthenticationProvider.generateRefreshToken( registeredClient.getTokenSettings().refreshTokenTimeToLive()); } Jwt jwtIdToken = null; if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) { String nonce = (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE); headersBuilder = JwtUtils.headers(); claimsBuilder = JwtUtils.idTokenClaims( registeredClient, issuer, authorization.getPrincipalName(), nonce); // @formatter:off context = JwtEncodingContext.with(headersBuilder, claimsBuilder) .registeredClient(registeredClient) .principal(authorization.getAttribute(Principal.class.getName())) .authorization(authorization) .authorizedScopes(authorizedScopes) .tokenType(ID_TOKEN_TOKEN_TYPE) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrant(authorizationCodeAuthentication) .build(); // @formatter:on this.jwtCustomizer.customize(context); headers = context.getHeaders().build(); claims = context.getClaims().build(); jwtIdToken = this.jwtEncoder.encode(headers, claims); } OidcIdToken idToken; if (jwtIdToken != null) { idToken = new OidcIdToken(jwtIdToken.getTokenValue(), jwtIdToken.getIssuedAt(), jwtIdToken.getExpiresAt(), jwtIdToken.getClaims()); } else { idToken = null; } // @formatter:off OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.from(authorization) .token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, jwtAccessToken.getClaims()) ); if (refreshToken != null) { authorizationBuilder.refreshToken(refreshToken); } if (idToken != null) { authorizationBuilder .token(idToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims())); } authorization = authorizationBuilder.build(); // @formatter:on // Invalidate the authorization code as it can only be used once authorization = OAuth2AuthenticationProviderUtils.invalidate(authorization, authorizationCode.getToken()); this.authorizationService.save(authorization); Map additionalParameters = Collections.emptyMap(); if (idToken != null) { additionalParameters = new HashMap<>(); additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()); } return new OAuth2AccessTokenAuthenticationToken( registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } private static Set excludeOpenidIfNecessary(Set scopes) { if (!scopes.contains(OidcScopes.OPENID)) { return scopes; } scopes = new HashSet<>(scopes); scopes.remove(OidcScopes.OPENID); return scopes; } @Override public boolean supports(Class authentication) { return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication); } }