Set iss claim in Jwt using configured issuer

Closes gh-223
This commit is contained in:
Joe Grandja 2021-02-10 04:07:27 -05:00
parent afd5491ced
commit c9afc3e061
5 changed files with 177 additions and 129 deletions

View File

@ -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<String> authorizedScopes) {
// @formatter:off
return accessTokenContext(registeredClient, authorization.getPrincipalName(), authorizedScopes)
.authorization(authorization);
// @formatter:on
}
static JwtEncodingContext.Builder accessTokenContext(RegisteredClient registeredClient,
String principalName, Set<String> 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
}
}

View File

@ -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<String> 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;
}
}

View File

@ -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 <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
*/
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<JwtEncodingContext> 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<String> 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();

View File

@ -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<JwtEncodingContext> 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();

View File

@ -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<JwtEncodingContext> 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();