Polish gh-128
This commit is contained in:
parent
78d4bd0bad
commit
a2167a5091
@ -144,16 +144,18 @@ public final class OAuth2AuthorizationServerConfigurer<B extends HttpSecurityBui
|
||||
jwtEncoder);
|
||||
builder.authenticationProvider(postProcess(authorizationCodeAuthenticationProvider));
|
||||
|
||||
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
|
||||
new OAuth2RefreshTokenAuthenticationProvider(
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
|
||||
|
||||
OAuth2ClientCredentialsAuthenticationProvider clientCredentialsAuthenticationProvider =
|
||||
new OAuth2ClientCredentialsAuthenticationProvider(
|
||||
getAuthorizationService(builder),
|
||||
jwtEncoder);
|
||||
builder.authenticationProvider(postProcess(clientCredentialsAuthenticationProvider));
|
||||
|
||||
OAuth2RefreshTokenAuthenticationProvider refreshTokenAuthenticationProvider =
|
||||
new OAuth2RefreshTokenAuthenticationProvider(getAuthorizationService(builder), jwtEncoder);
|
||||
builder.authenticationProvider(postProcess(refreshTokenAuthenticationProvider));
|
||||
|
||||
OAuth2TokenRevocationAuthenticationProvider tokenRevocationAuthenticationProvider =
|
||||
new OAuth2TokenRevocationAuthenticationProvider(
|
||||
getAuthorizationService(builder));
|
||||
|
@ -66,14 +66,10 @@ public final class InMemoryOAuth2AuthorizationService implements OAuth2Authoriza
|
||||
} else if (TokenType.AUTHORIZATION_CODE.equals(tokenType)) {
|
||||
OAuth2AuthorizationCode authorizationCode = authorization.getTokens().getToken(OAuth2AuthorizationCode.class);
|
||||
return authorizationCode != null && authorizationCode.getTokenValue().equals(token);
|
||||
}
|
||||
|
||||
if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
|
||||
} else if (TokenType.ACCESS_TOKEN.equals(tokenType)) {
|
||||
return authorization.getTokens().getAccessToken() != null &&
|
||||
authorization.getTokens().getAccessToken().getTokenValue().equals(token);
|
||||
}
|
||||
|
||||
if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
|
||||
} else if (TokenType.REFRESH_TOKEN.equals(tokenType)) {
|
||||
return authorization.getTokens().getRefreshToken() != null &&
|
||||
authorization.getTokens().getRefreshToken().getTokenValue().equals(token);
|
||||
}
|
||||
|
@ -18,24 +18,25 @@ package org.springframework.security.oauth2.server.authorization.authentication;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken;
|
||||
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.OAuth2RefreshToken;
|
||||
import org.springframework.security.oauth2.server.authorization.Version;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
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 Madhu Bhat
|
||||
* @since 0.0.1
|
||||
* @see AbstractAuthenticationToken
|
||||
* @see OAuth2AuthorizationCodeAuthenticationProvider
|
||||
* @see RegisteredClient
|
||||
* @see OAuth2AccessToken
|
||||
* @see OAuth2RefreshToken
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
*/
|
||||
public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthenticationToken {
|
||||
@ -65,8 +66,8 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
|
||||
* @param accessToken the access token
|
||||
* @param refreshToken the refresh token
|
||||
*/
|
||||
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient,
|
||||
Authentication clientPrincipal, OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
|
||||
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
|
||||
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken) {
|
||||
super(Collections.emptyList());
|
||||
Assert.notNull(registeredClient, "registeredClient cannot be null");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
@ -105,14 +106,13 @@ public class OAuth2AccessTokenAuthenticationToken extends AbstractAuthentication
|
||||
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
|
||||
public OAuth2RefreshToken getRefreshToken() {
|
||||
return refreshToken;
|
||||
return this.refreshToken;
|
||||
}
|
||||
}
|
||||
|
@ -97,9 +97,9 @@ public class OAuth2ClientCredentialsAuthenticationProvider implements Authentica
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes);
|
||||
|
||||
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
|
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
|
||||
.principalName(clientPrincipal.getName())
|
||||
.tokens(OAuth2Tokens.builder().accessToken(accessToken).build())
|
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
|
||||
.build();
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
|
@ -13,12 +13,8 @@
|
||||
* 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.util.Set;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.util.Assert;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Refresh Token Grant.
|
||||
*
|
||||
@ -47,19 +46,23 @@ import org.springframework.util.Assert;
|
||||
* @see OAuth2AccessTokenAuthenticationToken
|
||||
* @see OAuth2AuthorizationService
|
||||
* @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>
|
||||
*/
|
||||
|
||||
public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final OAuth2AuthorizationService authorizationService;
|
||||
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(jwtEncoder, "jwtEncoder cannot be null");
|
||||
|
||||
this.authorizationService = authorizationService;
|
||||
this.jwtEncoder = jwtEncoder;
|
||||
}
|
||||
@ -73,43 +76,45 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
|
||||
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(refreshTokenAuthentication.getPrincipal().getClass())) {
|
||||
clientPrincipal = (OAuth2ClientAuthenticationToken) refreshTokenAuthentication.getPrincipal();
|
||||
}
|
||||
|
||||
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
|
||||
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) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
|
||||
}
|
||||
|
||||
Instant refreshTokenExpiration = authorization.getTokens().getRefreshToken().getExpiresAt();
|
||||
if (refreshTokenExpiration.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 [...].
|
||||
if (!registeredClient.getId().equals(authorization.getRegisteredClientId())) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
|
||||
|
||||
// https://tools.ietf.org/html/rfc6749#section-6
|
||||
// As per https://tools.ietf.org/html/rfc6749#section-6
|
||||
// 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.
|
||||
Set<String> refreshTokenScopes = refreshTokenAuthentication.getScopes();
|
||||
Set<String> scopes = refreshTokenAuthentication.getScopes();
|
||||
Set<String> authorizedScopes = authorization.getAttribute(OAuth2AuthorizationAttributeNames.AUTHORIZED_SCOPES);
|
||||
if (!authorizedScopes.containsAll(refreshTokenScopes)) {
|
||||
if (!authorizedScopes.containsAll(scopes)) {
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
|
||||
}
|
||||
|
||||
if (refreshTokenScopes.isEmpty()) {
|
||||
refreshTokenScopes = authorizedScopes;
|
||||
if (scopes.isEmpty()) {
|
||||
scopes = authorizedScopes;
|
||||
}
|
||||
|
||||
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,
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), refreshTokenScopes);
|
||||
jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), scopes);
|
||||
|
||||
TokenSettings tokenSettings = registeredClient.getTokenSettings();
|
||||
OAuth2RefreshToken refreshToken;
|
||||
@ -120,13 +125,13 @@ public class OAuth2RefreshTokenAuthenticationProvider implements AuthenticationP
|
||||
}
|
||||
|
||||
authorization = OAuth2Authorization.from(authorization)
|
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
|
||||
.tokens(OAuth2Tokens.builder().accessToken(accessToken).refreshToken(refreshToken).build())
|
||||
.attribute(OAuth2AuthorizationAttributeNames.ACCESS_TOKEN_ATTRIBUTES, jwt)
|
||||
.build();
|
||||
|
||||
this.authorizationService.save(authorization);
|
||||
|
||||
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken);
|
||||
return new OAuth2AccessTokenAuthenticationToken(
|
||||
registeredClient, clientPrincipal, accessToken, refreshToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -13,16 +13,16 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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.core.Authentication;
|
||||
import org.springframework.security.oauth2.server.authorization.Version;
|
||||
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.
|
||||
*
|
||||
@ -33,42 +33,37 @@ import org.springframework.util.Assert;
|
||||
* @see OAuth2ClientAuthenticationToken
|
||||
*/
|
||||
public class OAuth2RefreshTokenAuthenticationToken extends AbstractAuthenticationToken {
|
||||
|
||||
private final Authentication clientPrincipal;
|
||||
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
|
||||
private final String refreshToken;
|
||||
private final Authentication clientPrincipal;
|
||||
private final Set<String> scopes;
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param refreshToken refresh token value
|
||||
* @param refreshToken the refresh token
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
*/
|
||||
public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal) {
|
||||
this(clientPrincipal, refreshToken, Collections.emptySet());
|
||||
this(refreshToken, clientPrincipal, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@code OAuth2RefreshTokenAuthenticationToken} using the provided parameters.
|
||||
*
|
||||
* @param refreshToken the refresh token
|
||||
* @param clientPrincipal the authenticated client principal
|
||||
* @param refreshToken refresh token value
|
||||
* @param requestedScopes scopes requested by refresh token
|
||||
* @param scopes the requested scope(s)
|
||||
*/
|
||||
public OAuth2RefreshTokenAuthenticationToken(Authentication clientPrincipal, String refreshToken, Set<String> requestedScopes) {
|
||||
public OAuth2RefreshTokenAuthenticationToken(String refreshToken, Authentication clientPrincipal,
|
||||
Set<String> scopes) {
|
||||
super(Collections.emptySet());
|
||||
|
||||
Assert.hasText(refreshToken, "refreshToken cannot be empty");
|
||||
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
|
||||
Assert.hasText(refreshToken, "refreshToken cannot be null or empty");
|
||||
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
Assert.notNull(scopes, "scopes cannot be null");
|
||||
this.refreshToken = refreshToken;
|
||||
this.scopes = requestedScopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
this.clientPrincipal = clientPrincipal;
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -76,6 +71,16 @@ public class OAuth2RefreshTokenAuthenticationToken extends AbstractAuthenticatio
|
||||
return this.clientPrincipal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the refresh token.
|
||||
*
|
||||
* @return the refresh token
|
||||
*/
|
||||
public String getRefreshToken() {
|
||||
return this.refreshToken;
|
||||
}
|
||||
|
@ -13,19 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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.StringKeyGenerator;
|
||||
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.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
|
||||
* @since 0.0.3
|
||||
*/
|
||||
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) {
|
||||
JoseHeader joseHeader = JoseHeader.withAlgorithm(SignatureAlgorithm.RS256).build();
|
||||
@ -71,8 +70,8 @@ class OAuth2TokenIssuerUtil {
|
||||
|
||||
static OAuth2RefreshToken issueRefreshToken(Duration refreshTokenTimeToLive) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,12 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.config;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return the {@link TokenSettings}
|
||||
*/
|
||||
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);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if refresh tokens support is enabled.
|
||||
* 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}.
|
||||
* Returns {@code true} if refresh tokens are enabled. 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() {
|
||||
return setting(ENABLE_REFRESH_TOKENS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to {@code true} to enable refresh tokens support.
|
||||
* This include generation of refresh token as a part of Authorization Code Grant flow and support of Refresh Token
|
||||
* Grant flow.
|
||||
* Set to {@code true} to enable refresh tokens.
|
||||
*
|
||||
* @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}
|
||||
*/
|
||||
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,
|
||||
* or {@code false} if a new refresh token is generated.
|
||||
* The default is {@code false}.
|
||||
* Returns {@code true} if refresh tokens are reused when returning the access token response,
|
||||
* or {@code false} if a new refresh token is issued. The default is {@code true}.
|
||||
*/
|
||||
public boolean reuseRefreshTokens() {
|
||||
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,
|
||||
* or to {@code false} to generate a new refresh token.
|
||||
* @param reuseRefreshTokens {@code true} to re-use existing refresh token, {@code false} to generate a new one
|
||||
* Set to {@code true} if refresh tokens are reused when returning the access token response,
|
||||
* or {@code false} if a new refresh token is issued.
|
||||
*
|
||||
* @param reuseRefreshTokens {@code true} to reuse refresh tokens, {@code false} to issue new refresh tokens
|
||||
* @return the {@link TokenSettings}
|
||||
*/
|
||||
public TokenSettings reuseRefreshTokens(boolean 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}.
|
||||
* @return refresh token time-to-live
|
||||
* Returns the time-to-live for a refresh token. The default is 60 minutes.
|
||||
*
|
||||
* @return the time-to-live for a refresh token
|
||||
*/
|
||||
public Duration refreshTokenTimeToLive() {
|
||||
return setting(REFRESH_TOKEN_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets refresh token time-to-live.
|
||||
* @param refreshTokenTimeToLive refresh token time-to-live. Has to be greater than {@code Duration.ZERO}.
|
||||
* Set the time-to-live for a refresh token. Must 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) {
|
||||
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);
|
||||
return this;
|
||||
}
|
||||
@ -138,7 +139,7 @@ public class TokenSettings extends Settings {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put(ACCESS_TOKEN_TIME_TO_LIVE, Duration.ofMinutes(5));
|
||||
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));
|
||||
return settings;
|
||||
}
|
||||
|
@ -130,8 +130,8 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
||||
this.tokenEndpointMatcher = new AntPathRequestMatcher(tokenEndpointUri, HttpMethod.POST.name());
|
||||
Map<AuthorizationGrantType, Converter<HttpServletRequest, Authentication>> converters = new HashMap<>();
|
||||
converters.put(AuthorizationGrantType.AUTHORIZATION_CODE, new AuthorizationCodeAuthenticationConverter());
|
||||
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
|
||||
converters.put(AuthorizationGrantType.REFRESH_TOKEN, new RefreshTokenAuthenticationConverter());
|
||||
converters.put(AuthorizationGrantType.CLIENT_CREDENTIALS, new ClientCredentialsAuthenticationConverter());
|
||||
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.withToken(accessToken.getTokenValue())
|
||||
.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> {
|
||||
|
||||
@Override
|
||||
@ -264,41 +303,4 @@ public class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
|
||||
.thenReturn(this.authorization);
|
||||
|
||||
RegisteredClient clientWithReuseTokensTrue = TestRegisteredClients.registeredClient()
|
||||
RegisteredClient clientWithReuseTokensTrue = TestRegisteredClients.registeredClient2()
|
||||
.tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(true))
|
||||
.build();
|
||||
|
||||
@ -183,7 +183,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
when(this.authorizationService.findByToken(REFRESH_TOKEN_VALUE, TokenType.REFRESH_TOKEN))
|
||||
.thenReturn(this.authorization);
|
||||
|
||||
RegisteredClient clientWithReuseTokensFalse = TestRegisteredClients.registeredClient()
|
||||
RegisteredClient clientWithReuseTokensFalse = TestRegisteredClients.registeredClient2()
|
||||
.tokenSettings(tokenSettings -> tokenSettings.reuseRefreshTokens(false))
|
||||
.build();
|
||||
|
||||
@ -208,7 +208,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
requestedScopes.add("openid");
|
||||
|
||||
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))
|
||||
.thenReturn(this.authorization);
|
||||
@ -227,7 +227,7 @@ public class OAuth2RefreshTokenAuthenticationProviderTests {
|
||||
requestedScopes.add("another-scope");
|
||||
|
||||
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))
|
||||
.thenReturn(this.authorization);
|
||||
|
@ -34,7 +34,7 @@ public class OAuth2RefreshTokenAuthenticationTokenTests {
|
||||
|
||||
@Test
|
||||
public void constructorWhenClientPrincipalNullThrowException() {
|
||||
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", null))
|
||||
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("test", null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("clientPrincipal cannot be null");
|
||||
}
|
||||
@ -43,18 +43,18 @@ public class OAuth2RefreshTokenAuthenticationTokenTests {
|
||||
public void constructorWhenRefreshTokenNullOrEmptyThrowException() {
|
||||
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken(null, mock(OAuth2ClientAuthenticationToken.class)))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("refreshToken cannot be null or empty");
|
||||
.hasMessage("refreshToken cannot be empty");
|
||||
|
||||
assertThatThrownBy(() -> new OAuth2RefreshTokenAuthenticationToken("", mock(OAuth2ClientAuthenticationToken.class)))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("refreshToken cannot be null or empty");
|
||||
.hasMessage("refreshToken cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenGettingScopesThenReturnRequestedScopes() {
|
||||
Set<String> expectedScopes = new HashSet<>(Arrays.asList("scope-a", "scope-b"));
|
||||
OAuth2RefreshTokenAuthenticationToken token
|
||||
= new OAuth2RefreshTokenAuthenticationToken(mock(OAuth2ClientAuthenticationToken.class), "test", expectedScopes);
|
||||
= new OAuth2RefreshTokenAuthenticationToken("test", mock(OAuth2ClientAuthenticationToken.class), expectedScopes);
|
||||
|
||||
assertThat(token.getScopes()).containsAll(expectedScopes);
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ public class TokenSettingsTests {
|
||||
assertThat(tokenSettings.settings()).hasSize(4);
|
||||
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(Duration.ofMinutes(5));
|
||||
assertThat(tokenSettings.enableRefreshTokens()).isTrue();
|
||||
assertThat(tokenSettings.reuseRefreshTokens()).isEqualTo(false);
|
||||
assertThat(tokenSettings.reuseRefreshTokens()).isEqualTo(true);
|
||||
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
|
||||
}
|
||||
|
||||
@ -83,12 +83,12 @@ public class TokenSettingsTests {
|
||||
assertThatThrownBy(() -> new TokenSettings().refreshTokenTimeToLive(Duration.ZERO))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.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)))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.extracting(Throwable::getMessage)
|
||||
.isEqualTo("refreshTokenTimeToLive has to be greater than Duration.ZERO");
|
||||
.isEqualTo("refreshTokenTimeToLive must be greater than Duration.ZERO");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -101,7 +101,7 @@ public class TokenSettingsTests {
|
||||
assertThat(tokenSettings.settings()).hasSize(6);
|
||||
assertThat(tokenSettings.accessTokenTimeToLive()).isEqualTo(accessTokenTimeToLive);
|
||||
assertThat(tokenSettings.enableRefreshTokens()).isTrue();
|
||||
assertThat(tokenSettings.reuseRefreshTokens()).isFalse();
|
||||
assertThat(tokenSettings.reuseRefreshTokens()).isTrue();
|
||||
assertThat(tokenSettings.refreshTokenTimeToLive()).isEqualTo(Duration.ofMinutes(60));
|
||||
assertThat(tokenSettings.<String>setting("name1")).isEqualTo("value1");
|
||||
assertThat(tokenSettings.<String>setting("name2")).isEqualTo("value2");
|
||||
|
@ -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'
|
||||
|
||||
// TODO Remove once Spring Boot upgrades to Spring Security 5.5
|
||||
ext['spring-security.version'] = '5.5.+'
|
||||
|
||||
dependencies {
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.springframework.boot:spring-boot-starter-security'
|
||||
|
@ -50,8 +50,8 @@ public class WebClientConfig {
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider =
|
||||
OAuth2AuthorizedClientProviderBuilder.builder()
|
||||
.authorizationCode()
|
||||
.clientCredentials()
|
||||
.refreshToken()
|
||||
.clientCredentials()
|
||||
.build();
|
||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
|
Loading…
Reference in New Issue
Block a user