Implement Token Revocation Endpoint#84
Conversation
|
@babuv2 Please sign the Contributor License Agreement! Click here to manually synchronize the status of this Pull Request. See the FAQ for frequently asked questions. |
|
@babuv2 Thank you for signing the Contributor License Agreement! |
| * The default endpoint {@code URI} for token revocation request. | ||
| */ | ||
| public static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke"; | ||
| public static final String TOKEN_TYPE_HINT = "token_type_hint"; |
There was a problem hiding this comment.
Change modifier to private for TOKEN_TYPE_HINT and TOKEN
| MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request); | ||
|
|
||
| // client_id (REQUIRED) | ||
| String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); |
There was a problem hiding this comment.
Please remove client_id as it's not a valid parameter for a revocation request.
| this.credentials = ""; | ||
| } | ||
|
|
||
| public OAuth2TokenRevocationAuthenticationToken(String token, |
There was a problem hiding this comment.
Remove this constructor since client_id is not a valid parameter.
| private RegisteredClient registeredClient; | ||
|
|
||
| public OAuth2TokenRevocationAuthenticationToken(String token, | ||
| Authentication clientPrincipal, String tokenTypeHint) { |
There was a problem hiding this comment.
Add @Nullable for tokenTypeHint arg
| public class OAuth2TokenRevocationAuthenticationToken extends AbstractAuthenticationToken { | ||
| private static final long serialVersionUID = Version.SERIAL_VERSION_UID; | ||
| private final String tokenTypeHint; | ||
| private Authentication authentication; |
There was a problem hiding this comment.
Rename to clientPrincipal
| OAuth2TokenRevocationAuthenticationToken authenticationToken = (OAuth2TokenRevocationAuthenticationToken) | ||
| authentication; | ||
|
|
||
| String clientId = authenticationToken.getPrincipal().toString(); |
There was a problem hiding this comment.
We should assert on OAuth2ClientAuthenticationToken. Take a look at OAuth2AuthorizationCodeAuthenticationProvider and apply the same check for client auth.
| throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); | ||
| } | ||
|
|
||
| if (token.equals(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE))) { |
There was a problem hiding this comment.
It's not clear to me what this check is for?
There was a problem hiding this comment.
This is not needed Joe. I have removed it. As of now we ignore the hint and only look for access token
| OAuth2Authorization authorization = null; | ||
| TokenType tokenType = new TokenType(tokenTypeHint); | ||
|
|
||
| if (TokenType.REFRESH_TOKEN.equals(tokenType) || TokenType.ACCESS_TOKEN.equals(tokenType)) { |
There was a problem hiding this comment.
Apologies as I did mention to allow the capability to revoke a refresh token but at the moment we don't have refresh token grant feature. Therefore, please remove all references to refresh token. We will add this in after we have refresh token feature implemented.
For now, we can assume the revocation request is for access tokens only. Any other token type hint should return 400 for now.
There was a problem hiding this comment.
Joe the spec says that hint can be any value. if it is of value access_token or refresh_token the authorization server CAN use it to speed up its lookup. If any other value of token_type_hint is give it should not lead to any error
"An invalid token type hint value is ignored by the authorization
server and does not influence the revocation response."
| * @param token the token credential | ||
| * @return the {@link OAuth2Authorization} if found, otherwise {@code null} | ||
| */ | ||
| OAuth2Authorization findByToken(String token); |
There was a problem hiding this comment.
I'm wondering if we need to add this operation? Instead we could consider changing findByTokenAndTokenType to String token, @Nullable TokenType tokenType? I think the name would change to. Thoughts?
There was a problem hiding this comment.
Just looking up for access_token for now
| * | ||
| * @param authorization {@link OAuth2Authorization} to be invalidated | ||
| */ | ||
| void invalidate(OAuth2Authorization authorization); |
There was a problem hiding this comment.
I'm not sure we should add this operation here. We will need the ability to revoke access tokens, refresh tokens and possibly authorization codes. This operation doesn't allow us to specify which token to revoke. I wonder if we should have a revoke on OAuth2Authorization instead. Thoughts?
There was a problem hiding this comment.
@jgrandja Joe this is the only place where I have a doubt, you mean to say that have a revoke method on the authorization and then we should do
authorization.revoke() and then authorizationService.save(authorization)? Because in that case with our current inmemory implementation one more record will get inserted with revoked status. Instead I thought we can have an invalidate method on the service and let the implementation decide how to handle the revoke. In this default inmemory implementation
- we remove the authorization and then
- add it in revoked status
Other implementations may remove it from DB and handle other revocations etc.
We can discuss this further Joe
|
@jgrandja Joe, Thanks for all the inputs |
4836bdb to
686115c
Compare
|
@babuv2 Regarding our discussion around renaming |
@jgrandja Joe should I make any more changes to this PR? |
|
@babuv2 Yes. Do you recall our discussion around We also agreed that we would extract |
686115c to
dcd399e
Compare
|
@jgrandja Joe, I have added the revocation service implementation |
|
Thanks @babuv2. I will try to review today if not first thing tomorrow. |
| final OAuth2Authorization authorization = this.authorizationService.findByTokenAndTokenType(token, tokenType); | ||
| if (authorization != null) { | ||
| final OAuth2Authorization revokedAuthorization = OAuth2Authorization.from(authorization) | ||
| .revoked(true).build(); |
There was a problem hiding this comment.
OAuth2Authorization.revoked does not indicate which token is revoked so this member will not work.
Let me draw out a scenario:
- Client receives
codein the Authorization Response - the
codeis leaked to a malicious client - the malicious client attempts to obtain an access token using the
codeand the Authorization Server must detect this and revoke thecode - any further attempts of using the
codeshould be rejected. As well, the "real" client will not be able to obtain the access token since thecodewas 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.
There was a problem hiding this comment.
@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?
| private final AntPathRequestMatcher revocationEndpointMatcher; | ||
|
|
||
| private final Converter<HttpServletRequest, Authentication> tokenRevocationAuthenticationConverter = | ||
| new OAuth2TokenRevocationEndpointFilter.TokenRevocationAuthenticationConverter(); |
There was a problem hiding this comment.
Outer class reference OAuth2TokenRevocationEndpointFilter can be removed.
| * The default endpoint {@code URI} for token revocation request. | ||
| */ | ||
| public static final String DEFAULT_TOKEN_REVOCATION_ENDPOINT_URI = "/oauth2/revoke"; | ||
| private static final String TOKEN_TYPE_HINT = "token_type_hint"; |
There was a problem hiding this comment.
Please move TOKEN and TOKEN_TYPE_HINT to TokenRevocationAuthenticationConverter
| this.errorHttpResponseConverter.write(error, null, httpResponse); | ||
| } | ||
|
|
||
| private static OAuth2AuthenticationException throwError(String errorCode, String parameterName) { |
| } | ||
|
|
||
| // token_type_hint (OPTIONAL) | ||
| String tokenTypeHint = parameters.getFirst(TOKEN_TYPE_HINT); |
There was a problem hiding this comment.
We should validate that TOKEN_TYPE_HINT was not supplied with multiple values
| try { | ||
| Authentication tokenRevocationRequestAuthentication = | ||
| this.tokenRevocationAuthenticationConverter.convert(request); | ||
| this.authenticationManager.authenticate(tokenRevocationRequestAuthentication); |
There was a problem hiding this comment.
We should explicitly set status to 200 - even though this will default. However, it will ensure the response is written and therefore will commit.
| import java.util.Collections; | ||
|
|
||
| /** | ||
| * An {@link Authentication} implementation used for OAuth 2.0 Client Authentication. |
|
Thanks for the PR @babuv2 ! This is now merged along with a follow-up polish commit to complete the feature. |
Fixes gh-83