diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtEncodingContextUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtEncodingContextUtils.java deleted file mode 100644 index d07fcfa..0000000 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtEncodingContextUtils.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.Set; - -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.IdTokenClaimNames; -import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; -import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; -import org.springframework.security.oauth2.jwt.JoseHeader; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; -import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * @author Joe Grandja - * @since 0.1.0 - */ -final class JwtEncodingContextUtils { - private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); - - private JwtEncodingContextUtils() { - } - - static JwtEncodingContext.Builder accessTokenContext(RegisteredClient registeredClient, OAuth2Authorization authorization) { - // @formatter:off - return accessTokenContext(registeredClient, authorization, - authorization.getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME)); - // @formatter:on - } - - static JwtEncodingContext.Builder accessTokenContext(RegisteredClient registeredClient, OAuth2Authorization authorization, - Set authorizedScopes) { - // @formatter:off - return accessTokenContext(registeredClient, authorization.getPrincipalName(), authorizedScopes) - .authorization(authorization); - // @formatter:on - } - - static JwtEncodingContext.Builder accessTokenContext(RegisteredClient registeredClient, - String principalName, Set authorizedScopes) { - - JoseHeader.Builder headersBuilder = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256); - - String issuer = "http://auth-server:9000"; // TODO Allow configuration for issuer claim - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().accessTokenTimeToLive()); - - // @formatter:off - JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder() - .issuer(issuer) - .subject(principalName) - .audience(Collections.singletonList(registeredClient.getClientId())) - .issuedAt(issuedAt) - .expiresAt(expiresAt) - .notBefore(issuedAt); - if (!CollectionUtils.isEmpty(authorizedScopes)) { - claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes); - } - // @formatter:on - - // @formatter:off - return JwtEncodingContext.with(headersBuilder, claimsBuilder) - .registeredClient(registeredClient) - .tokenType(OAuth2TokenType.ACCESS_TOKEN); - // @formatter:on - } - - static JwtEncodingContext.Builder idTokenContext(RegisteredClient registeredClient, OAuth2Authorization authorization) { - JoseHeader.Builder headersBuilder = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256); - - String issuer = "http://auth-server:9000"; // TODO Allow configuration for issuer claim - Instant issuedAt = Instant.now(); - Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); // TODO Allow configuration for id token time-to-live - OAuth2AuthorizationRequest authorizationRequest = authorization.getAttribute( - OAuth2AuthorizationRequest.class.getName()); - String nonce = (String) authorizationRequest.getAdditionalParameters().get(OidcParameterNames.NONCE); - - // @formatter:off - JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder() - .issuer(issuer) - .subject(authorization.getPrincipalName()) - .audience(Collections.singletonList(registeredClient.getClientId())) - .issuedAt(issuedAt) - .expiresAt(expiresAt) - .claim(IdTokenClaimNames.AZP, registeredClient.getClientId()); - if (StringUtils.hasText(nonce)) { - claimsBuilder.claim(IdTokenClaimNames.NONCE, nonce); - } - // TODO Add 'auth_time' claim - // @formatter:on - - // @formatter:off - return JwtEncodingContext.with(headersBuilder, claimsBuilder) - .registeredClient(registeredClient) - .authorization(authorization) - .tokenType(ID_TOKEN_TOKEN_TYPE); - // @formatter:on - } - -} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtUtils.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtUtils.java new file mode 100644 index 0000000..f8d8dc5 --- /dev/null +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/JwtUtils.java @@ -0,0 +1,101 @@ +/* + * 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.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +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.server.authorization.client.RegisteredClient; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * Utility methods used by the {@link AuthenticationProvider}'s when issuing {@link Jwt}'s. + * + * @author Joe Grandja + * @since 0.1.0 + */ +final class JwtUtils { + + private JwtUtils() { + } + + static JoseHeader.Builder headers() { + return JoseHeader.withAlgorithm(SignatureAlgorithm.RS256); + } + + static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient, + String issuer, String subject, Set authorizedScopes) { + + Instant issuedAt = Instant.now(); + Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().accessTokenTimeToLive()); + + // @formatter:off + JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder(); + if (StringUtils.hasText(issuer)) { + claimsBuilder.issuer(issuer); + } + claimsBuilder + .subject(subject) + .audience(Collections.singletonList(registeredClient.getClientId())) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .notBefore(issuedAt); + if (!CollectionUtils.isEmpty(authorizedScopes)) { + claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes); + } + // @formatter:on + + return claimsBuilder; + } + + static JwtClaimsSet.Builder idTokenClaims(RegisteredClient registeredClient, + String issuer, String subject, String nonce) { + + Instant issuedAt = Instant.now(); + // TODO Allow configuration for ID Token time-to-live + Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES); + + // @formatter:off + JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder(); + if (StringUtils.hasText(issuer)) { + claimsBuilder.issuer(issuer); + } + claimsBuilder + .subject(subject) + .audience(Collections.singletonList(registeredClient.getClientId())) + .issuedAt(issuedAt) + .expiresAt(expiresAt) + .claim(IdTokenClaimNames.AZP, registeredClient.getClientId()); + if (StringUtils.hasText(nonce)) { + claimsBuilder.claim(IdTokenClaimNames.NONCE, nonce); + } + // TODO Add 'auth_time' claim + // @formatter:on + + return claimsBuilder; + } + +} diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java index 10dfe8f..3af5574 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java @@ -19,7 +19,9 @@ import java.security.Principal; import java.util.Collections; import java.util.HashMap; 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; @@ -42,6 +44,7 @@ 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.token.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2AuthorizationCode; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; @@ -66,10 +69,14 @@ import static org.springframework.security.oauth2.server.authorization.authentic * @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 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. @@ -89,6 +96,11 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica this.jwtCustomizer = jwtCustomizer; } + @Autowired(required = false) + protected void setProviderSettings(ProviderSettings providerSettings) { + this.providerSettings = providerSettings; + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = @@ -127,13 +139,25 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica 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(), authorizedScopes); + // @formatter:off - JwtEncodingContext context = JwtEncodingContextUtils.accessTokenContext(registeredClient, authorization) + JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder) + .registeredClient(registeredClient) .principal(authorization.getAttribute(Principal.class.getName())) + .authorization(authorization) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrant(authorizationCodeAuthentication) .build(); // @formatter:on + this.jwtCustomizer.customize(context); JoseHeader headers = context.getHeaders().build(); @@ -152,13 +176,23 @@ public class OAuth2AuthorizationCodeAuthenticationProvider implements Authentica 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 = JwtEncodingContextUtils.idTokenContext(registeredClient, authorization) + context = JwtEncodingContext.with(headersBuilder, claimsBuilder) + .registeredClient(registeredClient) .principal(authorization.getAttribute(Principal.class.getName())) + .authorization(authorization) + .tokenType(ID_TOKEN_TOKEN_TYPE) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrant(authorizationCodeAuthentication) .build(); // @formatter:on + this.jwtCustomizer.customize(context); headers = context.getHeaders().build(); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java index 038f1dd..34639f2 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2ClientCredentialsAuthenticationProvider.java @@ -19,6 +19,7 @@ import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; +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; @@ -27,6 +28,7 @@ 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.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jwt.Jwt; @@ -35,6 +37,7 @@ 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.token.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.util.Assert; @@ -61,6 +64,7 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica private final OAuth2AuthorizationService authorizationService; private final JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer = (context) -> {}; + private ProviderSettings providerSettings; /** * Constructs an {@code OAuth2ClientCredentialsAuthenticationProvider} using the provided parameters. @@ -81,6 +85,11 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica this.jwtCustomizer = jwtCustomizer; } + @Autowired(required = false) + protected void setProviderSettings(ProviderSettings providerSettings) { + this.providerSettings = providerSettings; + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = @@ -105,13 +114,22 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica scopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes()); } + String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null; + + JoseHeader.Builder headersBuilder = JwtUtils.headers(); + JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims( + registeredClient, issuer, clientPrincipal.getName(), scopes); + // @formatter:off - JwtEncodingContext context = JwtEncodingContextUtils.accessTokenContext(registeredClient, clientPrincipal.getName(), scopes) + JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder) + .registeredClient(registeredClient) .principal(clientPrincipal) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizationGrant(clientCredentialsAuthentication) .build(); // @formatter:on + this.jwtCustomizer.customize(context); JoseHeader headers = context.getHeaders().build(); diff --git a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java index 8e3de17..b01b2c1 100644 --- a/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java +++ b/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java @@ -21,6 +21,7 @@ import java.time.Instant; import java.util.Base64; 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; @@ -33,6 +34,7 @@ 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.OAuth2RefreshToken2; +import org.springframework.security.oauth2.core.OAuth2TokenType; import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; import org.springframework.security.oauth2.jwt.JoseHeader; import org.springframework.security.oauth2.jwt.Jwt; @@ -40,8 +42,8 @@ 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.core.OAuth2TokenType; 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.config.TokenSettings; import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; @@ -69,6 +71,7 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP private final OAuth2AuthorizationService authorizationService; private final JwtEncoder jwtEncoder; private OAuth2TokenCustomizer jwtCustomizer = (context) -> {}; + private ProviderSettings providerSettings; /** * Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters. @@ -89,6 +92,11 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP this.jwtCustomizer = jwtCustomizer; } + @Autowired(required = false) + protected void setProviderSettings(ProviderSettings providerSettings) { + this.providerSettings = providerSettings; + } + @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2RefreshTokenAuthenticationToken refreshTokenAuthentication = @@ -137,13 +145,23 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); } + String issuer = this.providerSettings != null ? this.providerSettings.issuer() : null; + + JoseHeader.Builder headersBuilder = JwtUtils.headers(); + JwtClaimsSet.Builder claimsBuilder = JwtUtils.accessTokenClaims( + registeredClient, issuer, authorization.getPrincipalName(), scopes); + // @formatter:off - JwtEncodingContext context = JwtEncodingContextUtils.accessTokenContext(registeredClient, authorization, scopes) + JwtEncodingContext context = JwtEncodingContext.with(headersBuilder, claimsBuilder) + .registeredClient(registeredClient) .principal(authorization.getAttribute(Principal.class.getName())) + .authorization(authorization) + .tokenType(OAuth2TokenType.ACCESS_TOKEN) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrant(refreshTokenAuthentication) .build(); // @formatter:on + this.jwtCustomizer.customize(context); JoseHeader headers = context.getHeaders().build();