Support token revocation and introspection#1473
Conversation
|
@Sephster , as mentioned #1453 (comment), it would be great to have more maintainers for this repository! @hafezdivandari is an excellent professional with extensive knowledge of OAuth2. Having more people to support the project would certainly help its growth and maintenance! 🚀 |
|
Hi @Sephster, by any chance, did you have the time to check this PR? I'd appreciate any comments or feedback. |
|
Sorry I hadn't realised it was ready for review. I'll get to it as soon as I can but am going to clear some outstanding issues prior. Thanks for your work on this |
|
@Sephster thanks so much for all your work maintaining this library! I was just wondering whether you had even a rough idea of when you might get to looking at this PR? It would be super helpful for the problem I'm currently trying to solve! 😅 |
|
Hey @Sephster, I feel the OAuth Server is a bit incomplete without this. When you have a chance, could you take a look? The PR is large, but I did my best to make it easy to review. It’s also difficult to keep it open for too long since other merged PRs may cause conflicts. Thanks. |
|
I'll be picking this up next. Thanks |
|
Thanks @Sephster! I have the implementation PR ready for Laravel Passport if you’d like to see it in action: laravel/passport#1858 |
|
I'm on holiday with the family for a few days but still reviewing and hoping to get a first response to you soon. Thanks for the patience |
|
Hi @Sephster, thank you for reviewing the PR and for the detailed feedback. I’ve addressed the requested changes and replied to each comment. Please feel free to resolve any comment where you find my response satisfactory. |
|
Hi @Sephster , would there be any chance of having this merged as a Christmas gift? |
|
That's my intention but my kids gifts are going to have to take priority 🐵 I aim to have this reviewed before Christmas though, all being well. Thanks @hafezdivandari |
|
Aiming to pick up and complete this this week @hafezdivandari - thanks for your patience |
|
Hi @Sephster , any progress? |
|
Not yet - was working on dropping PHP 8.1 support last week but aim to pick this up, at least partially, this evening all being wel |
|
We’re this close 🤏 |
|
|
||
| $claims = $token->claims(); | ||
|
|
||
| // Check if token is linked to the client |
There was a problem hiding this comment.
Hi, I'm wondering about linking a client to the token in this check, as by reading the spec I understood that the client reaching the token introspection endpoint doesn't have to be the OAuth2 client. I don't see the explicit requirement in the spec that the client to which the token has been issued must be the one asking about the access token on the introspection endpoint... In the security section of the token introspection spec it even says:
A single piece of software acting as both a client and a
protected resource MAY reuse the same credentials between the token
endpoint and the introspection endpoint, though doing so potentially
conflates the activities of the client and protected resource
portions of the software and the authorization server MAY require
separate credentials for each mode.
There was a problem hiding this comment.
Client A should not have access to a token issued for client B at the first place? And the token issued for Client A will be considered inactive for Client B, that's why we send "active": false as the response:
Note that a properly formed and authorized query for an inactive or
otherwise invalid token (or a token the protected resource is not
allowed to know about) is not considered an error response by this
specification. In these cases, the authorization server MUST instead
respond with an introspection response with the "active" field set to
"false" as described in Section 2.2.
There was a problem hiding this comment.
In the same section you mentioned right before that line:
the authorization server MUST require authentication of protected
resources that need to access the introspection endpoint and SHOULD
require protected resources to be specifically authorized to call the
introspection endpoint. The specifics of such authentication
credentials are out of scope of this specification, but commonly
these credentials could take the form of any valid client
authentication mechanism used with the token endpoint, an OAuth 2.0
access token, or other HTTP authorization or authentication
mechanism.
There was a problem hiding this comment.
The specifics of such authentication
credentials are out of scope of this specification, but commonly
these credentials could take the form of any valid client
authentication mechanism used with the token endpoint, an OAuth 2.0
access token, or other HTTP authorization or authentication
mechanism.
So, even because of this sentence above I think that the client from the introspection endpoint perspective can be whatever authentication mechanism the implementation chooses. The main thing is that the request is authenticated. So, those client credentials can be something else - not only OAuth2 client credentials from the client to which the original token was issued. The implementation of token introspection can choose whatever client credentials they want and distribute those client credentials to only specifically allowed protected resources to be allowed to use the introspection endpoint.
In my mind, the scenario could be this - we have OAuth2 Authorization Server ASA, an OAuth2 client C OCC registered with ASA, and an external protected resources B PRB, If the external PRB chooses to start accepting access tokens originally issued from ASA to any OAuth2 clients (different clients), it can use the token introspection endpoint on ASA to check the validity of that access token. Since it is external protected resource, it doesn't know the client credentials of original client to which the access token was issued. It only knows the access token it received. On the other hand, since the request to the introspection endpoint must be authenticated - protected resource could have it own client credentials for that purpose which don't necessarily need to be those to which the token was issued.
To summarize, my main concern is the fact that I can't read from the specification that the token which is checked on introspection endpoint must be the one that is issued to the client asking about it.
There was a problem hiding this comment.
I now understand your point, here is the summary:
| Endpoint | Token type | Client binding required? |
|---|---|---|
| Introspection | Access | No * |
| Introspection | Refresh | Yes |
| Revocation | Access | Yes |
| Revocation | Refresh | Yes |
* It is required right now in the current implementation, but I'm going to change this into not required in a single commit.
There was a problem hiding this comment.
Sorry @Sephster, I still don’t fully understand what specific changes you’re requesting.
I think it would be preferable to just have this endpoint used by the resource server only and not bother with Oauth2 clients at all. A simple http basic auth check should suffice but I don't know how much change would be required for this.
According to RFC 7662:
these credentials could take the form of any valid client
authentication mechanism used with the token endpoint, an OAuth 2.0
access token, or other HTTP authorization or authentication
mechanism. A single piece of software acting as both a client and a
protected resource MAY reuse the same credentials between the token
endpoint and the introspection endpoint, though doing so potentially
conflates the activities of the client and protected resource
portions of the software and the authorization server MAY require
separate credentials for each mode.
My understanding is that, the resource server essentially acts like an OAuth client when hitting the introspection endpoint. So we don't necessarily need a separate concept of "resource server", letting regular OAuth clients authenticate to the introspection endpoint is perfectly valid per the spec.
That’s why we’re using the same validateClient method as the other endpoints, which internally calls getClientCredentials and also supports Basic Auth.
There was a problem hiding this comment.
I think we remove all client checks from the introspection endpoint and replace it with some middleware the does a basic http auth check.
You're right that the spec permits a client and the resource server to be one and the same, but I think this over complicates things, as currently, a client is just something acting on behalf of the end user and the resource server isn't quite the same as all the other clients.
By having the resource server just authenticate with the endpoint using http basic auth, we are also avoiding the issue with the current implementation where a client can currently inspect any token, regardless of if it belongs to them.
For the reasons above, I think we just simplify the introspection endpoint and make it protected by http basic auth and not have client checks, so we can then use these credentials for the resource server to call it when required.
I think this is the typical way introspection end points are implemented
There was a problem hiding this comment.
But we need client binding for refresh tokens? I mean the token can be access token or refresh token, and for refresh token we need to check if it is issued to the client requesting introspection?
Also according to spec:
If the protected resource uses OAuth 2.0 client credentials to
authenticate to the introspection endpoint and its credentials are
invalid, the authorization server responds with an HTTP 401
(Unauthorized) as described in Section 5.2 of OAuth 2.0 RFC6749
However, it would be greatly appreciated if you could implement the changes you have in mind on top of this PR.
There was a problem hiding this comment.
we are also avoiding the issue with the current implementation where a client can currently inspect any token, regardless of if it belongs to them.
This is our design choice in this implementation, that we allow clients to introspect any access token, regardless of ownership. At the moment, this is allowed only for access tokens, not refresh tokens. I can revert this and enforce the same binding for both token types if needed.
I think this is the typical way introspection end points are implemented
I reviewed several examples and implementations, and they all use client authentication for the introspection endpoint, as we do here on this PR.
There was a problem hiding this comment.
thanks Hafez. I'll take a look at all this this week.
|
Any updates on this? I need this for Laravel Passport. Thanks @hafezdivandari for your hard work on this |
|
Waiting on me revisiting last review point. Had aimed to get to it last week but didn't happen so will prioritise soon. Cheers @samuelwei |
Fixes #806
Fixes #1350
Closes #1255
Closes #1135
Closes #995
Closes #925
It's much easier to implement Token Revocation RFC7009 and Token Introspection RFC7662 at the same time! These 2 RFCs have most of the logic in common:
"application/x-www-form-urlencoded" data:
tokenis required, accepts either an access token or a refresh token.token_type_hintis optional to help the server optimize the token lookup.access_tokenorrefresh_token.Authorization: Basic {client_credentials}headerclient_idandclient_secretparameters.400:invalid_requestthe requiredtokenparameter was not sent.401:invalid_clientthe request is not authorized. Client credentials were not sent or invalid.200: The token is revoked, expired, doesn't exists, or was not issued to the client making the request:activefield set tofalse.{ "active": false }200: The token is valid, active, and was issued to the client making the request:activefield set totrue.{ "active": true, ... }Implementation
Summary
Summary of file changes in order of appearance.
New
AbstractHandlerclassAbstractGrantandAbstractTokenHandlerclasses.clientRepository,accessTokenRepository, andrefreshTokenRepositoryproperties have been moved to this class from childAbstractGrantclass.setClientRepository,setAccessTokenRepository,setRefreshTokenRepository,getClientCredentials,parseParam,getRequestParameter,getBasicAuthCredentials,getQueryStringParameter,getCookieParameter,getServerParameter, andvalidateEncryptedRefreshTokenmethods have been moved to this class from childAbstractGrantclass without any change.validateClientmethod from childAbstractGranthas been moved to this class with a minor change: The call togetIdentifier()isGrantTypeInterfacespecific.getClientEntityOrFailmethod from childAbstractGranthas been moved to this class. This method is also overridden on childAbstractGrant: Call tosupportsGrantTypemethod isAbstracGrantspecific.validateEncryptedRefreshTokenis extracted fromRefreshTokenGrant::validateOldRefreshTokenmethod.Change
AuthorizationValidators\BearerTokenValidatorclassBearerTokenValidatorInterfaceinterface.validateAuthorizationmethod into a newvalidateBearerTokenmethod.validateBearerTokenmethod also accepts optional$clientIdargument. If a$clientIdis provided, it verifies whether the token was issued to the given client.New
AuthorizationValidators\BearerTokenValidatorInterfaceinterfaceBearerTokenValidatorclass.validateBearerTokenmethod.AbstractTokenHandler::setBearerTokenValidator()method.Change
Exception\OAuthServerExceptionclassunsupportedTokenTypemethod.Change
Grant\AbstractGrantclassAbstractHandlerclass.AbstractHandlerclass.getClientEntityOrFailmethod.Change
Grant\RefreshTokenGrantclassvalidateOldRefreshTokenmethod has been extracted into a new parent AbstractHandler::validateEncryptedRefreshToken method.New
Handlers\AbstractTokenHandlerclassAbstractHandlerclass.TokenHandlerInterfaceinterface.setPublicKeyandsetBearerTokenValidatormethods that to allow injecting a customized token validator.validateToken,validateAccessTokenandvalidateRefreshTokenmethods that have been used byTokenIntrospectionHandlerandTokenRevocationHandlerclasses.New
Handlers\TokenHandlerInterfaceinterfaceAbstractTokenHandlerclass.setTokenRevocationHandlerandsetTokenIntrospectionHandlermethods of theTokenServerclass.New
Handlers\TokenIntrospectionHandlerclassAbstractTokenHandlerclass.respondToRequestmethod that responds to "Token Introspection RFC7662" request.setResponseTypemethod that allows injecting a customized response type.New
Handlers\TokenRevocationHandlerclassAbstractTokenHandlerclass.respondToRequestmethod that responds to "Token Revocation RFC7009" request.New
ResponseTypes\IntrospectionResponseclassIntrospectionResponseTypeInterfaceinterface.generateHttpResponsemethod to generate "Token Revocation RFC7009" response.New
ResponseTypes\IntrospectionResponseTypeInterfaceclassIntrospectionResponseclass.TokenIntrospectionHandler::setResponseTypemethod that allows injecting a customized response type.New
TokenServerclassrespondToTokenRevocationRequestandrespondToTokenIntrospectionRequestmethods to respond to the respective request.setTokenRevocationHandlerandsetTokenIntrospectionHandlermethods that allows injecting customized handlers.