Skip to content

Implement Token Revocation Endpoint #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2020 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;

import org.springframework.util.Assert;

/**
* An {@link OAuth2TokenRevocationService} that revokes tokens.
*
* @author Vivek Babu
* @see OAuth2AuthorizationService
* @since 0.0.1
*/
public final class DefaultOAuth2TokenRevocationService implements OAuth2TokenRevocationService {

private OAuth2AuthorizationService authorizationService;

/**
* Constructs an {@code DefaultOAuth2TokenRevocationService}.
*/
public DefaultOAuth2TokenRevocationService(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}

@Override
public void revoke(String token, TokenType tokenType) {
final OAuth2Authorization authorization = this.authorizationService.findByTokenAndTokenType(token, tokenType);
if (authorization != null) {
final OAuth2Authorization revokedAuthorization = OAuth2Authorization.from(authorization)
.revoked(true).build();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OAuth2Authorization.revoked does not indicate which token is revoked so this member will not work.

Let me draw out a scenario:

  1. Client receives code in the Authorization Response
  2. the code is leaked to a malicious client
  3. the malicious client attempts to obtain an access token using the code and the Authorization Server must detect this and revoke the code
  4. any further attempts of using the code should be rejected. As well, the "real" client will not be able to obtain the access token since the code was revoked. Instead the client will have to restart the flow.

So we do need some kind of construct in OAuth2Authorization that maps a token (code, access token or refresh token) to some extra metadata. One attribute would be a revoked flag. We'll also likely want to know the time it was revoked. We may store additional attributes related to the token, eg. the code can only be used once.

Give this some further thought and the type of construct we'll need so it can support storing metadata/attributes related to a specific token.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@babuv2 I've been giving this some further thought and I'd like to propose the following class as a holder of token metadata. The name maps nicely as referenced in the Token Introspection RFC:

This specification defines a protocol that allows authorized
protected resources to query the authorization server to determine
the set of metadata for a given token that was presented to them by
an OAuth 2.0 client. This metadata includes whether or not the token
is currently active (or if it has expired or otherwise been revoked)...

public class OAuth2TokenMetadata<T extends AbstractOAuth2Token> implements Serializable {
	private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
	private final T token;
	private boolean revoked;
	private Instant revokedAt;

	public OAuth2TokenMetadata(T token) {
	}

	public T getToken() {
	}

	public boolean isExpired() {
	}

	public boolean isRevoked() {
	}

	public Instant getRevokedAt() {
	}

	public boolean isActive() {
	}
}

How about we start with this and see how it shapes up. Sounds good?

this.authorizationService.save(revokedAuthorization);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
*
* @author Joe Grandja
* @author Krisztian Toth
* @author Vivek Babu
* @since 0.0.1
* @see RegisteredClient
* @see OAuth2AccessToken
Expand All @@ -43,6 +44,7 @@ public class OAuth2Authorization implements Serializable {
private String principalName;
private OAuth2AccessToken accessToken;
private Map<String, Object> attributes;
private boolean revoked;

protected OAuth2Authorization() {
}
Expand Down Expand Up @@ -74,6 +76,15 @@ public OAuth2AccessToken getAccessToken() {
return this.accessToken;
}

/**
* Returns whether the authorization has been revoked.
*
* @return the status of the authorization, revoked or not
*/
public boolean isRevoked() {
return this.revoked;
}

/**
* Returns the attribute(s) associated to the authorization.
*
Expand Down Expand Up @@ -108,7 +119,8 @@ public boolean equals(Object obj) {
return Objects.equals(this.registeredClientId, that.registeredClientId) &&
Objects.equals(this.principalName, that.principalName) &&
Objects.equals(this.accessToken, that.accessToken) &&
Objects.equals(this.attributes, that.attributes);
Objects.equals(this.attributes, that.attributes) &&
Objects.equals(this.revoked, that.revoked);
}

@Override
Expand Down Expand Up @@ -138,7 +150,8 @@ public static Builder from(OAuth2Authorization authorization) {
return new Builder(authorization.getRegisteredClientId())
.principalName(authorization.getPrincipalName())
.accessToken(authorization.getAccessToken())
.attributes(attrs -> attrs.putAll(authorization.getAttributes()));
.attributes(attrs -> attrs.putAll(authorization.getAttributes()))
.revoked(authorization.isRevoked());
}

/**
Expand All @@ -150,6 +163,7 @@ public static class Builder implements Serializable {
private String principalName;
private OAuth2AccessToken accessToken;
private Map<String, Object> attributes = new HashMap<>();
private boolean revoked;

protected Builder(String registeredClientId) {
this.registeredClientId = registeredClientId;
Expand Down Expand Up @@ -203,6 +217,16 @@ public Builder attributes(Consumer<Map<String, Object>> attributesConsumer) {
return this;
}

/**
* Sets the authorization as revoked.
*
* @return the {@link Builder}
*/
public Builder revoked(boolean revoked) {
this.revoked = revoked;
return this;
}

/**
* Builds a new {@link OAuth2Authorization}.
*
Expand All @@ -217,6 +241,7 @@ public OAuth2Authorization build() {
authorization.principalName = this.principalName;
authorization.accessToken = this.accessToken;
authorization.attributes = Collections.unmodifiableMap(this.attributes);
authorization.revoked = this.revoked;
return authorization;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2020 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;

/**
* Implementations of this interface are responsible for the revocation of
* OAuth2 tokens.
*
* @author Vivek Babu
* @since 0.0.1
*/
public interface OAuth2TokenRevocationService {

/**
* Revokes the given token.
*
* @param token the token to be revoked
* @param tokenType the type of token to be revoked
*/
void revoke(String token, TokenType tokenType);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2020 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 org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
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.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenRevocationService;
import org.springframework.security.oauth2.server.authorization.TokenType;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.util.Assert;

/**
* An {@link AuthenticationProvider} implementation for the OAuth 2.0 Token Revocation.
*
* @author Vivek Babu
* @since 0.0.1
* @see OAuth2TokenRevocationAuthenticationToken
* @see OAuth2AuthorizationService
* @see OAuth2TokenRevocationService
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7009#section-2.1">Section 2.1 Revocation Request</a>
*/
public class OAuth2TokenRevocationAuthenticationProvider implements AuthenticationProvider {

private OAuth2AuthorizationService authorizationService;
private OAuth2TokenRevocationService tokenRevocationService;

/**
* Constructs an {@code OAuth2TokenRevocationAuthenticationProvider} using the provided parameters.
*
* @param authorizationService the authorization service
* @param tokenRevocationService the token revocation service
*/
public OAuth2TokenRevocationAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenRevocationService tokenRevocationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenRevocationService, "tokenRevocationService cannot be null");
this.authorizationService = authorizationService;
this.tokenRevocationService = tokenRevocationService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2TokenRevocationAuthenticationToken tokenRevocationAuthenticationToken =
(OAuth2TokenRevocationAuthenticationToken) authentication;

OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(tokenRevocationAuthenticationToken.getPrincipal()
.getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) tokenRevocationAuthenticationToken.getPrincipal();
}
if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT));
}

final RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
final String tokenTypeHint = tokenRevocationAuthenticationToken.getTokenTypeHint();
final String token = tokenRevocationAuthenticationToken.getToken();
final OAuth2Authorization authorization = authorizationService.findByTokenAndTokenType(token,
TokenType.ACCESS_TOKEN);

OAuth2TokenRevocationAuthenticationToken successfulAuthentication =
new OAuth2TokenRevocationAuthenticationToken(token, registeredClient, tokenTypeHint);

if (authorization == null) {
return successfulAuthentication;
}

if (!registeredClient.getClientId().equals(authorization.getRegisteredClientId())) {
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT));
}

tokenRevocationService.revoke(token, TokenType.ACCESS_TOKEN);
return successfulAuthentication;
}

@Override
public boolean supports(Class<?> authentication) {
return OAuth2TokenRevocationAuthenticationToken.class.isAssignableFrom(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright 2020 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 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.server.authorization.client.RegisteredClient;
import org.springframework.util.Assert;

import java.util.Collections;

/**
* An {@link Authentication} implementation used for OAuth 2.0 Client Authentication.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in javadoc

*
* @author Vivek Babu
* @since 0.0.1
* @see AbstractAuthenticationToken
* @see RegisteredClient
* @see OAuth2TokenRevocationAuthenticationProvider
*/
public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = Version.SERIAL_VERSION_UID;
private final String tokenTypeHint;
private Authentication clientPrincipal;
private String token;
private RegisteredClient registeredClient;

public OAuth2TokenRevocationAuthenticationToken(String token,
Authentication clientPrincipal, @Nullable String tokenTypeHint) {
super(Collections.emptyList());
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
Assert.hasText(token, "token cannot be empty");
this.token = token;
this.clientPrincipal = clientPrincipal;
this.tokenTypeHint = tokenTypeHint;
}

public OAuth2TokenRevocationAuthenticationToken(String token,
RegisteredClient registeredClient, @Nullable String tokenTypeHint) {
super(Collections.emptyList());
Assert.notNull(registeredClient, "registeredClient cannot be null");
Assert.hasText(token, "token cannot be empty");
this.token = token;
this.registeredClient = registeredClient;
this.tokenTypeHint = tokenTypeHint;
setAuthenticated(true);
}

@Override
public Object getPrincipal() {
return this.clientPrincipal != null ? this.clientPrincipal : this.registeredClient
.getClientId();
}

@Override
public Object getCredentials() {
return "";
}

/**
* Returns the token.
*
* @return the token
*/
public String getToken() {
return this.token;
}

/**
* Returns the token type hint.
*
* @return the token type hint
*/
public String getTokenTypeHint() {
return tokenTypeHint;
}

/**
* Returns the {@link RegisteredClient registered client}.
*
* @return the {@link RegisteredClient}
*/
public @Nullable
RegisteredClient getRegisteredClient() {
return this.registeredClient;
}
}
Loading