Polish gh-128

This commit is contained in:
Joe Grandja 2020-10-29 20:45:11 -04:00
parent 78d4bd0bad
commit a2167a5091
14 changed files with 176 additions and 165 deletions

View File

@ -144,16 +144,18 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
jwtEncoder); jwtEncoder);
builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider)); builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
new OAuth2RefreshTokenAuthenticationProvider(
getAuthorizationService(builder),
jwtEncoder);
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider = OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
new OAuth2ClientCredentialsAuthenticationProvider( new OAuth2ClientCredentialsAuthenticationProvider(
getAuthorizationService(builder), getAuthorizationService(builder),
jwtEncoder); jwtEncoder);
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider)); builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
new OAuth2RefreshTokenAuthenticationProvider(getAuthorizationService(builder), jwtEncoder);
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider = OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
new OAuth2TokenRevocationAuthenticationProvider( new OAuth2TokenRevocationAuthenticationProvider(
getAuthorizationService(builder)); getAuthorizationService(builder));

View File

@ -66,16 +66,12 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
} else if (TokenType.AUTHORIZATION_CODE.equals(tokenType)) { } else if (TokenType.AUTHORIZATION_CODE.equals(tokenType)) {
OAuth2AuthorizationCode authorizationCode = authorization.getTokens().getToken(OAuth2AuthorizationCode.class); OAuth2AuthorizationCode authorizationCode = authorization.getTokens().getToken(OAuth2AuthorizationCode.class);
return authorizationCode != null && authorizationCode.getTokenValue().equals(token); return authorizationCode != null && authorizationCode.getTokenValue().equals(token);
} } else if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
return authorization.getTokens().getAccessToken() != null && return authorization.getTokens().getAccessToken() != null &&
authorization.getTokens().getAccessToken().getTokenValue().equals(token); authorization.getTokens().getAccessToken().getTokenValue().equals(token);
} } else if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
return authorization.getTokens().getRefreshToken() != null && return authorization.getTokens().getRefreshToken() != null &&
authorization.getTokens().getRefreshToken().getTokenValue().equals(token); authorization.getTokens().getRefreshToken().getTokenValue().equals(token);
} }
return false; return false;

View File

@ -18,24 +18,25 @@ package org.springframework.security.oauth2.server.authorization.authentication;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.Version;
import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.server.authorization.Version;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.util.Collections; import java.util.Collections;
/** /**
* An {@link Authentication} implementation used when issuing an OAuth 2.0 Access Token. * An {@link Authentication} implementation used when issuing an
* OAuth 2.0 Access Token and (optional) Refresh Token.
* *
* @author Joe Grandja * @author Joe Grandja
* @author Madhu Bhat * @author Madhu Bhat
* @since 0.0.1 * @since 0.0.1
* @see AbstractAuthenticationToken * @see AbstractAuthenticationToken
* @see OAuth2AuthorizationCodeAuthenticationProvider
* @see RegisteredClient * @see RegisteredClient
* @see OAuth2AccessToken * @see OAuth2AccessToken
* @see OAuth2RefreshToken
* @see OAuth2ClientAuthenticationToken * @see OAuth2ClientAuthenticationToken
*/ */
public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken { public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken {
@ -65,8 +66,8 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
* @param accessToken the access token * @param accessToken the access token
* @param refreshToken the refresh token * @param refreshToken the refresh token
*/ */
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
Authentication clientPrincipal, OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) { OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
super(Collections.emptyList()); super(Collections.emptyList());
Assert.notNull(registeredClient, "registeredClient cannot be null"); Assert.notNull(registeredClient, "registeredClient cannot be null");
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
@ -105,14 +106,13 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
return this.accessToken; return this.accessToken;
} }
/** /**
* Returns the {@link OAuth2RefreshToken} if provided * Returns the {@link OAuth2RefreshToken refresh token}.
* *
* @return the {@link OAuth2RefreshToken} * @return the {@link OAuth2RefreshToken} or {@code null} if not available
*/ */
@Nullable @Nullable
public OAuth2RefreshToken getRefreshToken() { public OAuth2RefreshToken getRefreshToken() {
return refreshToken; return this.refreshToken;
} }
} }

View File

@ -97,9 +97,9 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes); jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes);
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient) OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
.principalName(clientPrincipal.getName()) .principalName(clientPrincipal.getName())
.tokens(OAuth2Tokens.builder().accessToken(accessToken).build()) .tokens(OAuth2Tokens.builder().accessToken(accessToken).build())
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
.build(); .build();
this.authorizationService.save(authorization); this.authorizationService.save(authorization);

View File

@ -13,12 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.security.oauth2.server.authorization.authentication; package org.springframework.security.oauth2.server.authorization.authentication;
import java.time.Instant;
import java.util.Set;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
@ -38,6 +34,9 @@ import org.springframework.security.oauth2.server.authorization.config.TokenSett
import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens; import org.springframework.security.oauth2.server.authorization.token.OAuth2Tokens;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.time.Instant;
import java.util.Set;
/** /**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant. * An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
* *
@ -47,19 +46,23 @@ import org.springframework.util.Assert;
* @see OAuth2AccessTokenAuthenticationToken * @see OAuth2AccessTokenAuthenticationToken
* @see OAuth2AuthorizationService * @see OAuth2AuthorizationService
* @see JwtEncoder * @see JwtEncoder
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token</a> * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.5">Section 1.5 Refresh Token Grant</a>
* @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 OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider { public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
private final OAuth2AuthorizationService authorizationService; private final OAuth2AuthorizationService authorizationService;
private final JwtEncoder jwtEncoder; private final JwtEncoder jwtEncoder;
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService, JwtEncoder jwtEncoder) { /**
* Constructs an {@code OAuth2RefreshTokenAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param jwtEncoder the jwt encoder
*/
public OAuth2RefreshTokenAuthenticationProvider(OAuth2AuthorizationService authorizationService,
JwtEncoder jwtEncoder) {
Assert.notNull(authorizationService, "authorizationService cannot be null"); Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(jwtEncoder, "jwtEncoder cannot be null"); Assert.notNull(jwtEncoder, "jwtEncoder cannot be null");
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.jwtEncoder = jwtEncoder; this.jwtEncoder = jwtEncoder;
} }
@ -73,43 +76,45 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(refreshTokenAuthentication.getPrincipal().getClass())) { if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(refreshTokenAuthentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) refreshTokenAuthentication.getPrincipal(); clientPrincipal = (OAuth2ClientAuthenticationToken) refreshTokenAuthentication.getPrincipal();
} }
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) { if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT)); throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
} }
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
OAuth2Authorization authorization = this.authorizationService.findByToken(refreshTokenAuthentication.getRefreshToken(), TokenType.REFRESH_TOKEN); OAuth2Authorization authorization = this.authorizationService.findByToken(
refreshTokenAuthentication.getRefreshToken(), TokenType.REFRESH_TOKEN);
if (authorization == null) { if (authorization == null) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
} }
Instant refreshTokenExpiration = authorization.getTokens().getRefreshToken().getExpiresAt(); if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
if (refreshTokenExpiration.isBefore(Instant.now())) { throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
// as per https://tools.ietf.org/html/rfc6749#section-5.2 }
// invalid_grant: The provided authorization grant (e.g., authorization
// code, resource owner credentials) or refresh token is invalid, expired, revoked [...]. Instant refreshTokenExpiresAt = authorization.getTokens().getRefreshToken().getExpiresAt();
if (refreshTokenExpiresAt.isBefore(Instant.now())) {
// As per https://tools.ietf.org/html/rfc6749#section-5.2
// invalid_grant: The provided authorization grant (e.g., authorization code,
// resource owner credentials) or refresh token is invalid, expired, revoked [...].
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
} }
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); // As per https://tools.ietf.org/html/rfc6749#section-6
// https://tools.ietf.org/html/rfc6749#section-6
// The requested scope MUST NOT include any scope not originally granted by the resource owner, // The requested scope MUST NOT include any scope not originally granted by the resource owner,
// and if omitted is treated as equal to the scope originally granted by the resource owner. // and if omitted is treated as equal to the scope originally granted by the resource owner.
Set<String> refreshTokenScopes = refreshTokenAuthentication.getScopes(); Set<String> scopes = refreshTokenAuthentication.getScopes();
Set<String> authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES); Set<String> authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES);
if (!authorizedScopes.containsAll(refreshTokenScopes)) { if (!authorizedScopes.containsAll(scopes)) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE)); throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
} }
if (scopes.isEmpty()) {
if (refreshTokenScopes.isEmpty()) { scopes = authorizedScopes;
refreshTokenScopes = authorizedScopes;
} }
Jwt jwt = OAuth2TokenIssuerUtil Jwt jwt = OAuth2TokenIssuerUtil
.issueJwtAccessToken(this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), refreshTokenScopes); .issueJwtAccessToken(this.jwtEncoder, authorization.getPrincipalName(), registeredClient.getClientId(), scopes);
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), refreshTokenScopes); jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes);
TokenSettings tokenSettings = registeredClient.getTokenSettings(); TokenSettings tokenSettings = registeredClient.getTokenSettings();
OAuth2RefreshToken refreshToken; OAuth2RefreshToken refreshToken;
@ -120,13 +125,13 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
} }
authorization = OAuth2Authorization.from(authorization) authorization = OAuth2Authorization.from(authorization)
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt) .tokens(OAuth2Tokens.builder().accessToken(accessToken).refreshToken(refreshToken).build())
.tokens(OAuth2Tokens.builder().accessToken(accessToken).refreshToken(refreshToken).build()) .attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
.build(); .build();
this.authorizationService.save(authorization); this.authorizationService.save(authorization);
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken); return new OAuth2AccessTokenAuthenticationToken(
registeredClient, clientPrincipal, accessToken, refreshToken);
} }
@Override @Override

View File

@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.security.oauth2.server.authorization.authentication; package org.springframework.security.oauth2.server.authorization.authentication;
import java.util.Collections;
import java.util.Set;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.server.authorization.Version;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.util.Collections;
import java.util.Set;
/** /**
* An {@link Authentication} implementation used for the OAuth 2.0 Refresh Token Grant. * An {@link Authentication} implementation used for the OAuth 2.0 Refresh Token Grant.
* *
@ -33,42 +33,37 @@ import org.springframework.util.Assert;
* @see OAuth2ClientAuthenticationToken * @see OAuth2ClientAuthenticationToken
*/ */
public class OAuth2RefreshTokenAuthenticationToken extends AbstractAuthenticationToken { public class OAuth2RefreshTokenAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final Authentication clientPrincipal;
private final String refreshToken; private final String refreshToken;
private final Authentication clientPrincipal;
private final Set<String> scopes; private final Set<String> scopes;
/** /**
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters. * Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters.
* *
* @param refreshToken refresh token value * @param refreshToken the refresh token
* @param clientPrincipal the authenticated client principal * @param clientPrincipal the authenticated client principal
*/ */
public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal) { public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal) {
this(clientPrincipal, refreshToken, Collections.emptySet()); this(refreshToken, clientPrincipal, Collections.emptySet());
} }
/** /**
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters. * Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters.
* *
* @param refreshToken the refresh token
* @param clientPrincipal the authenticated client principal * @param clientPrincipal the authenticated client principal
* @param refreshToken refresh token value * @param scopes the requested scope(s)
* @param requestedScopes scopes requested by refresh token
*/ */
public OAuth2RefreshTokenAuthenticationToken(Authentication clientPrincipal, String refreshToken, Set<String> requestedScopes) { public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal,
Set<String> scopes) {
super(Collections.emptySet()); super(Collections.emptySet());
Assert.hasText(refreshToken, "refreshToken cannot be empty");
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null"); Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
Assert.hasText(refreshToken, "refreshToken cannot be null or empty"); Assert.notNull(scopes, "scopes cannot be null");
this.clientPrincipal = clientPrincipal;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
this.scopes = requestedScopes; this.clientPrincipal = clientPrincipal;
} this.scopes = scopes;
@Override
public Object getCredentials() {
return "";
} }
@Override @Override
@ -76,6 +71,16 @@ public class OAuth2RefreshTokenAuthenticationToken extends AbstractAuthenticatio
return this.clientPrincipal; return this.clientPrincipal;
} }
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the refresh token.
*
* @return the refresh token
*/
public String getRefreshToken() { public String getRefreshToken() {
return this.refreshToken; return this.refreshToken;
} }

View File

@ -13,19 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.security.oauth2.server.authorization.authentication; package org.springframework.security.oauth2.server.authorization.authentication;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Collections;
import java.util.Set;
import org.springframework.security.crypto.keygen.Base64StringKeyGenerator; import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
import org.springframework.security.crypto.keygen.StringKeyGenerator; import org.springframework.security.crypto.keygen.StringKeyGenerator;
import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken;
@ -36,13 +25,23 @@ import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.JwtEncoder;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Collections;
import java.util.Set;
/** /**
* @author Alexey Nesterov * @author Alexey Nesterov
* @since 0.0.3 * @since 0.0.3
*/ */
class OAuth2TokenIssuerUtil { class OAuth2TokenIssuerUtil {
private static final StringKeyGenerator CODE_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96); private static final StringKeyGenerator TOKEN_GENERATOR = new Base64StringKeyGenerator(Base64.getUrlEncoder().withoutPadding(), 96);
static Jwt issueJwtAccessToken(JwtEncoder jwtEncoder, String subject, String audience, Set<String> scopes) { static Jwt issueJwtAccessToken(JwtEncoder jwtEncoder, String subject, String audience, Set<String> scopes) {
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build(); JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
@ -71,8 +70,8 @@ class OAuth2TokenIssuerUtil {
static OAuth2RefreshToken issueRefreshToken(Duration refreshTokenTimeToLive) { static OAuth2RefreshToken issueRefreshToken(Duration refreshTokenTimeToLive) {
Instant issuedAt = Instant.now(); Instant issuedAt = Instant.now();
Instant refreshTokenExpiresAt = issuedAt.plus(refreshTokenTimeToLive); Instant expiresAt = issuedAt.plus(refreshTokenTimeToLive);
return new OAuth2RefreshToken(CODE_GENERATOR.generateKey(), issuedAt, refreshTokenExpiresAt); return new OAuth2RefreshToken(TOKEN_GENERATOR.generateKey(), issuedAt, expiresAt);
} }
} }

View File

@ -15,12 +15,12 @@
*/ */
package org.springframework.security.oauth2.server.authorization.config; package org.springframework.security.oauth2.server.authorization.config;
import org.springframework.util.Assert;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.util.Assert;
/** /**
* A facility for token configuration settings. * A facility for token configuration settings.
* *
@ -61,33 +61,31 @@ public class TokenSettings extends Settings {
} }
/** /**
* Set the time-to-live for an access token. * Set the time-to-live for an access token. Must be greater than {@code Duration.ZERO}.
* *
* @param accessTokenTimeToLive the time-to-live for an access token * @param accessTokenTimeToLive the time-to-live for an access token
* @return the {@link TokenSettings} * @return the {@link TokenSettings}
*/ */
public TokenSettings accessTokenTimeToLive(Duration accessTokenTimeToLive) { public TokenSettings accessTokenTimeToLive(Duration accessTokenTimeToLive) {
Assert.notNull(accessTokenTimeToLive, "accessTokenTimeToLive cannot be null");
Assert.isTrue(accessTokenTimeToLive.getSeconds() > 0, "accessTokenTimeToLive must be greater than Duration.ZERO");
setting(ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive); setting(ACCESS_TOKEN_TIME_TO_LIVE, accessTokenTimeToLive);
return this; return this;
} }
/** /**
* Returns {@code true} if refresh tokens support is enabled. * Returns {@code true} if refresh tokens are enabled. The default is {@code true}.
* This include generation of refresh token as a part of Authorization Code Grant flow and support of Refresh Token
* Grant flow. The default is {@code true}.
* *
* @return {@code true} if the client support refresh token, {@code false} otherwise * @return {@code true} if refresh tokens are enabled, {@code false} otherwise
*/ */
public boolean enableRefreshTokens() { public boolean enableRefreshTokens() {
return setting(ENABLE_REFRESH_TOKENS); return setting(ENABLE_REFRESH_TOKENS);
} }
/** /**
* Set to {@code true} to enable refresh tokens support. * Set to {@code true} to enable refresh tokens.
* This include generation of refresh token as a part of Authorization Code Grant flow and support of Refresh Token
* Grant flow.
* *
* @param enableRefreshTokens {@code true} to enable refresh token grant support, {@code false} otherwise * @param enableRefreshTokens {@code true} to enable refresh tokens, {@code false} otherwise
* @return the {@link TokenSettings} * @return the {@link TokenSettings}
*/ */
public TokenSettings enableRefreshTokens(boolean enableRefreshTokens) { public TokenSettings enableRefreshTokens(boolean enableRefreshTokens) {
@ -96,18 +94,19 @@ public class TokenSettings extends Settings {
} }
/** /**
* Returns {@code true} if existing refresh token is re-used when a new access token is requested via Refresh Token grant, * Returns {@code true} if refresh tokens are reused when returning the access token response,
* or {@code false} if a new refresh token is generated. * or {@code false} if a new refresh token is issued. The default is {@code true}.
* The default is {@code false}.
*/ */
public boolean reuseRefreshTokens() { public boolean reuseRefreshTokens() {
return setting(REUSE_REFRESH_TOKENS); return setting(REUSE_REFRESH_TOKENS);
} }
/** /**
* Set to {@code true} to re-use existing refresh token when new access token is requested via Refresh Token grant, * Set to {@code true} if refresh tokens are reused when returning the access token response,
* or to {@code false} to generate a new refresh token. * or {@code false} if a new refresh token is issued.
* @param reuseRefreshTokens {@code true} to re-use existing refresh token, {@code false} to generate a new one *
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue new refresh tokens
* @return the {@link TokenSettings}
*/ */
public TokenSettings reuseRefreshTokens(boolean reuseRefreshTokens) { public TokenSettings reuseRefreshTokens(boolean reuseRefreshTokens) {
setting(REUSE_REFRESH_TOKENS, reuseRefreshTokens); setting(REUSE_REFRESH_TOKENS, reuseRefreshTokens);
@ -115,21 +114,23 @@ public class TokenSettings extends Settings {
} }
/** /**
* Returns refresh token time-to-live. The default is 60 minutes. Always greater than {@code Duration.ZERO}. * Returns the time-to-live for a refresh token. The default is 60 minutes.
* @return refresh token time-to-live *
* @return the time-to-live for a refresh token
*/ */
public Duration refreshTokenTimeToLive() { public Duration refreshTokenTimeToLive() {
return setting(REFRESH_TOKEN_TIME_TO_LIVE); return setting(REFRESH_TOKEN_TIME_TO_LIVE);
} }
/** /**
* Sets refresh token time-to-live. * Set the time-to-live for a refresh token. Must be greater than {@code Duration.ZERO}.
* @param refreshTokenTimeToLive refresh token time-to-live. Has to be greater than {@code Duration.ZERO}. *
* @param refreshTokenTimeToLive the time-to-live for a refresh token
* @return the {@link TokenSettings}
*/ */
public TokenSettings refreshTokenTimeToLive(Duration refreshTokenTimeToLive) { public TokenSettings refreshTokenTimeToLive(Duration refreshTokenTimeToLive) {
Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null"); Assert.notNull(refreshTokenTimeToLive, "refreshTokenTimeToLive cannot be null");
Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0, "refreshTokenTimeToLive has to be greater than Duration.ZERO"); Assert.isTrue(refreshTokenTimeToLive.getSeconds() > 0, "refreshTokenTimeToLive must be greater than Duration.ZERO");
setting(REFRESH_TOKEN_TIME_TO_LIVE, refreshTokenTimeToLive); setting(REFRESH_TOKEN_TIME_TO_LIVE, refreshTokenTimeToLive);
return this; return this;
} }
@ -138,7 +139,7 @@ public class TokenSettings extends Settings {
Map<String, Object> settings = new HashMap<>(); Map<String, Object> settings = new HashMap<>();
settings.put(ACCESS_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(5)); settings.put(ACCESS_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(5));
settings.put(ENABLE_REFRESH_TOKENS, true); settings.put(ENABLE_REFRESH_TOKENS, true);
settings.put(REUSE_REFRESH_TOKENS, false); settings.put(REUSE_REFRESH_TOKENS, true);
settings.put(REFRESH_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(60)); settings.put(REFRESH_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(60));
return settings; return settings;
} }

View File

@ -130,8 +130,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name()); this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>(); Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>();
converters.put(AuthorizationGrantType.AUTHORIZATION_CODE, new AuthorizationCodeAuthenticationConverter()); converters.put(AuthorizationGrantType.AUTHORIZATION_CODE, new AuthorizationCodeAuthenticationConverter());
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
converters.put(AuthorizationGrantType.REFRESH_TOKEN, new RefreshTokenAuthenticationConverter()); converters.put(AuthorizationGrantType.REFRESH_TOKEN, new RefreshTokenAuthenticationConverter());
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
this.authorizationGrantAuthenticationConverter = new DelegatingAuthorizationGrantAuthenticationConverter(converters); this.authorizationGrantAuthenticationConverter = new DelegatingAuthorizationGrantAuthenticationConverter(converters);
} }
@ -165,7 +165,9 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
} }
} }
private void sendAccessTokenResponse(HttpServletResponse response, OAuth2AccessToken accessToken, OAuth2RefreshToken refreshToken) throws IOException { private void sendAccessTokenResponse(HttpServletResponse response, OAuth2AccessToken accessToken,
OAuth2RefreshToken refreshToken) throws IOException {
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.Builder builder =
OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType()) .tokenType(accessToken.getTokenType())
@ -235,6 +237,43 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
} }
} }
private static class RefreshTokenAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// refresh_token (REQUIRED)
String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
if (!StringUtils.hasText(refreshToken) ||
parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REFRESH_TOKEN);
}
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
}
if (StringUtils.hasText(scope)) {
Set<String> requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
return new OAuth2RefreshTokenAuthenticationToken(refreshToken, clientPrincipal, requestedScopes);
}
return new OAuth2RefreshTokenAuthenticationToken(refreshToken, clientPrincipal);
}
}
private static class ClientCredentialsAuthenticationConverter implements Converter<HttpServletRequest, Authentication> { private static class ClientCredentialsAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
@Override @Override
@ -264,41 +303,4 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
return new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal); return new OAuth2ClientCredentialsAuthenticationToken(clientPrincipal);
} }
} }
private static class RefreshTokenAuthenticationConverter implements Converter<HttpServletRequest, Authentication> {
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// refresh token (REQUIRED)
String refreshToken = parameters.getFirst(OAuth2ParameterNames.REFRESH_TOKEN);
if (StringUtils.hasText(refreshToken) &&
parameters.get(OAuth2ParameterNames.REFRESH_TOKEN).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.REFRESH_TOKEN);
}
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.SCOPE);
}
if (StringUtils.hasText(scope)) {
Set<String> requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
return new OAuth2RefreshTokenAuthenticationToken(clientPrincipal, refreshToken, requestedScopes);
}
return new OAuth2RefreshTokenAuthenticationToken(refreshToken, clientPrincipal);
}
}
} }

View File

@ -162,7 +162,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN)) when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization); .thenReturn(this.authorization);
RegisteredClient clientWithReuseTokensTrue = TestRegisteredClients.registeredClient() RegisteredClient clientWithReuseTokensTrue = TestRegisteredClients.registeredClient2()
.tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(true)) .tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(true))
.build(); .build();
@ -183,7 +183,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN)) when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization); .thenReturn(this.authorization);
RegisteredClient clientWithReuseTokensFalse = TestRegisteredClients.registeredClient() RegisteredClient clientWithReuseTokensFalse = TestRegisteredClients.registeredClient2()
.tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(false)) .tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(false))
.build(); .build();
@ -208,7 +208,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
requestedScopes.add("openid"); requestedScopes.add("openid");
OAuth2RefreshTokenAuthenticationToken tokenWithScopes OAuth2RefreshTokenAuthenticationToken tokenWithScopes
= new OAuth2RefreshTokenAuthenticationToken(this.clientPrincipal, REFRESH_TOKEN_VALUE, requestedScopes); = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, this.clientPrincipal, requestedScopes);
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN)) when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization); .thenReturn(this.authorization);
@ -227,7 +227,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
requestedScopes.add("another-scope"); requestedScopes.add("another-scope");
OAuth2RefreshTokenAuthenticationToken tokenWithScopes OAuth2RefreshTokenAuthenticationToken tokenWithScopes
= new OAuth2RefreshTokenAuthenticationToken(this.clientPrincipal, REFRESH_TOKEN_VALUE, requestedScopes); = new OAuth2RefreshTokenAuthenticationToken(REFRESH_TOKEN_VALUE, this.clientPrincipal, requestedScopes);
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN)) when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
.thenReturn(this.authorization); .thenReturn(this.authorization);

View File

@ -34,7 +34,7 @@ public class OAuth2RefreshTokenAuthenticationTokenTests {
@Test @Test
public void constructorWhenClientPrincipalNullThrowException() { public void constructorWhenClientPrincipalNullThrowException() {
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", null)) assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("test", null))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("clientPrincipal cannot be null"); .hasMessage("clientPrincipal cannot be null");
} }
@ -43,18 +43,18 @@ public class OAuth2RefreshTokenAuthenticationTokenTests {
public void constructorWhenRefreshTokenNullOrEmptyThrowException() { public void constructorWhenRefreshTokenNullOrEmptyThrowException() {
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken(null, mock(OAuth2ClientAuthenticationToken.class))) assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken(null, mock(OAuth2ClientAuthenticationToken.class)))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("refreshToken cannot be null or empty"); .hasMessage("refreshToken cannot be empty");
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", mock(OAuth2ClientAuthenticationToken.class))) assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", mock(OAuth2ClientAuthenticationToken.class)))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.hasMessage("refreshToken cannot be null or empty"); .hasMessage("refreshToken cannot be empty");
} }
@Test @Test
public void constructorWhenGettingScopesThenReturnRequestedScopes() { public void constructorWhenGettingScopesThenReturnRequestedScopes() {
Set<String> expectedScopes = new HashSet<>(Arrays.asList("scope-a", "scope-b")); Set<String> expectedScopes = new HashSet<>(Arrays.asList("scope-a", "scope-b"));
OAuth2RefreshTokenAuthenticationToken token OAuth2RefreshTokenAuthenticationToken token
= new OAuth2RefreshTokenAuthenticationToken(mock(OAuth2ClientAuthenticationToken.class), "test", expectedScopes); = new OAuth2RefreshTokenAuthenticationToken("test", mock(OAuth2ClientAuthenticationToken.class), expectedScopes);
assertThat(token.getScopes()).containsAll(expectedScopes); assertThat(token.getScopes()).containsAll(expectedScopes);
} }

View File

@ -35,7 +35,7 @@ public class TokenSettingsTests {
assertThat(tokenSettings.settings()).hasSize(4); assertThat(tokenSettings.settings()).hasSize(4);
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5)); assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
assertThat(tokenSettings.enableRefreshTokens()).isTrue(); assertThat(tokenSettings.enableRefreshTokens()).isTrue();
assertThat(tokenSettings.reuseRefreshTokens()).isEqualTo(false); assertThat(tokenSettings.reuseRefreshTokens()).isEqualTo(true);
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60)); assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
} }
@ -83,12 +83,12 @@ public class TokenSettingsTests {
assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ZERO)) assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ZERO))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage) .extracting(Throwable::getMessage)
.isEqualTo("refreshTokenTimeToLive has to be greater than Duration.ZERO"); .isEqualTo("refreshTokenTimeToLive must be greater than Duration.ZERO");
assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ofSeconds(-10))) assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ofSeconds(-10)))
.isInstanceOf(IllegalArgumentException.class) .isInstanceOf(IllegalArgumentException.class)
.extracting(Throwable::getMessage) .extracting(Throwable::getMessage)
.isEqualTo("refreshTokenTimeToLive has to be greater than Duration.ZERO"); .isEqualTo("refreshTokenTimeToLive must be greater than Duration.ZERO");
} }
@Test @Test
@ -101,7 +101,7 @@ public class TokenSettingsTests {
assertThat(tokenSettings.settings()).hasSize(6); assertThat(tokenSettings.settings()).hasSize(6);
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive); assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive);
assertThat(tokenSettings.enableRefreshTokens()).isTrue(); assertThat(tokenSettings.enableRefreshTokens()).isTrue();
assertThat(tokenSettings.reuseRefreshTokens()).isFalse(); assertThat(tokenSettings.reuseRefreshTokens()).isTrue();
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60)); assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
assertThat(tokenSettings.<String>setting("name1")).isEqualTo("value1"); assertThat(tokenSettings.<String>setting("name1")).isEqualTo("value1");
assertThat(tokenSettings.<String>setting("name2")).isEqualTo("value2"); assertThat(tokenSettings.<String>setting("name2")).isEqualTo("value2");

View File

@ -1,7 +1,8 @@
ext['spring-security.version'] = '5.5.+' // TODO remove once Spring Boot upgraded to Spring Security 5.5
apply plugin: 'io.spring.convention.spring-sample-boot' apply plugin: 'io.spring.convention.spring-sample-boot'
// TODO Remove once Spring Boot upgrades to Spring Security 5.5
ext['spring-security.version'] = '5.5.+'
dependencies { dependencies {
compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.springframework.boot:spring-boot-starter-security' compile 'org.springframework.boot:spring-boot-starter-security'

View File

@ -50,8 +50,8 @@ public class WebClientConfig {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder() OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode() .authorizationCode()
.clientCredentials()
.refreshToken() .refreshToken()
.clientCredentials()
.build(); .build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository); clientRegistrationRepository, authorizedClientRepository);