-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
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! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR @babuv2! Overall, this looks very good.
Please see review comments for requested changes.
* 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change modifier to private
for TOKEN_TYPE_HINT
and TOKEN
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request); | ||
|
||
// client_id (REQUIRED) | ||
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove client_id
as it's not a valid parameter for a revocation request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
this.credentials = ""; | ||
} | ||
|
||
public OAuth2TokenRevocationAuthenticationToken(String token, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this constructor since client_id
is not a valid parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
private RegisteredClient registeredClient; | ||
|
||
public OAuth2TokenRevocationAuthenticationToken(String token, | ||
Authentication clientPrincipal, String tokenTypeHint) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add @Nullable
for tokenTypeHint
arg
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to clientPrincipal
OAuth2TokenRevocationAuthenticationToken authenticationToken = (OAuth2TokenRevocationAuthenticationToken) | ||
authentication; | ||
|
||
String clientId = authenticationToken.getPrincipal().toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should assert on OAuth2ClientAuthenticationToken
. Take a look at OAuth2AuthorizationCodeAuthenticationProvider and apply the same check for client auth.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT)); | ||
} | ||
|
||
if (token.equals(authorization.getAttribute(OAuth2AuthorizationAttributeNames.CODE))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me what this check is for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates @babuv2. Please see review comments.
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.
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:
- Client receives
code
in the Authorization Response - the
code
is leaked to a malicious client - the malicious client attempts to obtain an access token using the
code
and the Authorization Server must detect this and revoke thecode
- 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 thecode
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.
There was a problem hiding this comment.
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?
private final AntPathRequestMatcher revocationEndpointMatcher; | ||
|
||
private final Converter<HttpServletRequest, Authentication> tokenRevocationAuthenticationConverter = | ||
new OAuth2TokenRevocationEndpointFilter.TokenRevocationAuthenticationConverter(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move TOKEN
and TOKEN_TYPE_HINT
to TokenRevocationAuthenticationConverter
this.errorHttpResponseConverter.write(error, null, httpResponse); | ||
} | ||
|
||
private static OAuth2AuthenticationException throwError(String errorCode, String parameterName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should return void
} | ||
|
||
// token_type_hint (OPTIONAL) | ||
String tokenTypeHint = parameters.getFirst(TOKEN_TYPE_HINT); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo in javadoc
Thanks for the PR @babuv2 ! This is now merged along with a follow-up polish commit to complete the feature. |
Fixes gh-83