oidc-op
oidc-op copied to clipboard
Token Exchange support
So, we are in the process of adding Token Exchange support on oidc-op as described in RFC-8693 and we need feedback regarding the implementation.
More specifically, we consider the following scenario regarding the exchanging of Access Tokens with Refresh Tokens:
- A
USER_AaccessesCLIENT_Aand retrieves an Access TokenAT1with a set of scopes that includes theoffline_scope. CLIENT_AsendsAT1toCLIENT_B.- Then
CLIENT_BexchangesAT1with a new Refresh TokenRT1with the same scope set, but sets theaudienceparameter of the request to beCLIENT_CandCLIENT_D. - Finally
CLIENT_B,CLIENT_CorCLIENT_Dmay useRT1to get Access TokenAT2with the same or fewer scopes (and optionally with a different audience) to access protected resource X. Equivalently,AT2will be owned by the client that issued the new Token Exchange request and every client (if any) that will be stated in the audience parameter will be allowed to use it.
Some observations on the aforementioned scenario:
- During step 1, the initial access token
AT1belongs to a sessionUSER_A;;CLIENT_Ain terms of oidc-op. - On the contrary, at step 3 the exchanged refresh token
RT1should be mapped in a different client in order forCLIENT_Bto be able to use it. This in terms of oidc-op is interpreted as a new sessionUSER_A;;CLIENT_Bwhere the token should be assigned. - In step 4, only the owner and the corresponding audience of token
RT1are allowed to use it. Currently, oidc-op retrieves the session thatRT1is mapped to and checks if theclient_idstated in the request matches the client of the session. This check should be modified in order to include a check upon theaudienceof the used token. - In RFC-8693 there is no strict definition of what the
audience(or evenresource) parameter should represent. For now, we intend to map theaudienceparameter with oidc-opclient_id.
Some potential conflicts in case of multiple audiences:
- What happens if we decide to support revocation of token upon usage? The first client, out of the set of the legitimate clients that are allowed to use the token, restricts the others from using it.
The way I see it a token exchange request is an exchange between a token with a set of token_type/scope/resource/audience to a different token with another set of token_type/scope/resource/audience. The task of the library is to :
- Allow or disallow the request.
- Produce the new token if needed.
I see 2 big problems with this:
- How will we manage sessions, E.g.:
- If client_1 gives an access token to client_2 and client_2 exchanges it who will the new token belong to?
- If client_1 exchanges an access token (A) for a new token (B) and then the grant that created token (A) gets revoked, what happens to (B) and its children-tokens.
- How will we decide if a mapping between a set of
token_type/scope/resource/audienceto another one is allowed? It is a complex problem that is hard to exhaust with simple configurations.
For (1):
- When a token (A) is exchanged for a token (B). Then a new session should be created. The new session must be somehow connected to the original session only for auditing reasons. IMHO the revocation of the parent session should not revoke the children.
- If client_1 gives token (A) to client_2 and then client_2 uses token (A) to produce token (B), then token (B) must belong to client_2. This means that the new session/grant must be under client_2, have the claims to scopes mapping defined for client_2 and client_1 must not be able to revoke it.
For (2):
- Token exchange should define a global and a per client policy.
- Each policy will define different behaviors based on the provided token type (and the requested token type?).
- We need to allow the user to provide a callable that will decide whether a mapping is allowed.
- We should also provide some out of the box behavior for each of
scope/resource/audience, e.g.:- Allow subset of the originally requested.
- Allow all.
- Allow none.
- Allow subset of a pre-configured set.
Other than that we should add a set of validations based on the subject_token_type. E.g.:
- if the
subject_token_typeis refresh_token then we should not allow an audience other than the client_id to be requested, since according to the spec only the owner of the refresh token must have it.
@rohe @peppelinux any thoughts on this approach?
Hi, happy to see this PR indeed
@nsklikas on your thoughts
-
it depends by STS policy. I'm quite reluctant to share a token between two clients. I'd see token exchange like a way that a Client has to obtain a new access token usable to a RS, without requiring a new authentication (auth code). More like a SSO mechanism
-
the new token has the power, once exchanged it works with its powers. that's how I used to think Token exchange, just personal approach.
1.1 I'd store only the event of exchange, at STS side, as a log. Yes, the revocation of the parent won't revocke the children 1.2 don't share token between different clients, and also the STS MUST be protected with a client authentication and this MUST match with the client_id/aud of the submitted token. That's how I'd do my implementation.
@nsklikas My 2c
- The entity that gets a token from an token exchange point owns the token
- If the original token gets revoked the all the tokens flowing from it MUST be removed/revoked. Provided the original token and all coming from it is minted by the same client. If you move to another client all bets are off! For that reason I question whether a client should be allowed to mint tokens based on tokens from another client.
- No suggestion for how to deal with mapping.
- Presently creating a new token from another token does not lead to a new session being created. Note that session here is based on an authentication session. Meaning that if the same entity uses the exchange to get a new token based on an old one it has it's still within the same authentication session.
- I can't see every allowing the minting of a refresh token based on an access token.
- Yes, I think a callable is the way to deal with mapping.
- The defaults sounds OK
it depends by STS policy. I'm quite reluctant to share a token between two clients. I'd see token exchange like a way that a Client has to obtain a new access token usable to a RS, without requiring a new authentication (auth code). More like a SSO mechanism
@peppelinux But wouldn't that be like using a refresh token? I understand your doubts about sharing tokens between clients, but I think that sometimes it is okay. If the user has given his consent, giving an access token to a trusted client may be okay (although I still don't like this approach). Perhaps this should be configurable as well.
If the original token gets revoked the all the tokens flowing from it MUST be removed/revoked. Provided the original token and all coming from it is minted by the same client. If you move to another client all bets are off! For that reason I question whether a client should be allowed to mint tokens based on tokens from another client.
@rohe Sure, I agree if the token comes from the same client it should be revoked as well.
@peppelinux something went wrong and you edited my comment instead of writing an answer. I will paste it here for now:
Yes, I seen the british ehalthy system that adopts this kind of sharing between clients. Regarding refresh token, no because of this use case:
The RP needs to exchange an acquired access_token (from ISS1) to a third-party RS. This RS have two way to handle this request:
acts like a RP to reauthenticate the user again (a kind of proxy, AA to RP side, RP to OP side) expose a STS endpoint that validate the access_token issued by ISS1 and exchange it with an access_token issued by itselfregarding point 2, we have some singolatirites. 2.1 We use an access_token issued for another scopes, as a auth mechanism to release another access_token. But it's resonable to have STS for that! 2.2 the RP could exchange a token by itsself, without any use interaction. Yes it MUST have the consent of the user but we now that token exchange is a machine-to-machine flow
Do you think to give the access token to the user-agent and have it submitted by the user? Yes, we can but also the RP could do something similar, with a procedura user-agent, isn't so?
Do you think to give the access token to the user-agent and have it submitted by the user?
No but issuing a refresh token requires the user 's consent, likewise giving an access token to another client and exchanging it for a refresh token must have the consent of the user
Also it's not clear to me what an STS is, is there some oauth2 spec describing it?
STS is here https://datatracker.ietf.org/doc/html/rfc8693
what's the specs you're considering for this token endpoint?
Ok, I'm blind. Thanks. With:
expose a STS endpoint that validate the access_token issued by ISS1 and exchange it with an access_token issued by itself
You mean that the RS should expose an STS endpoint? I'm not really sure what you mean.
The STS is only an endpoint, it can be hosted anywhere
In #165 Token Exchange support is introduced based on the discussion here. I'm considering the following format for the token exchange related configurations.
"token": {
"path": "token",
"class": "oidcop.oidc.token.Token",
"kwargs": {
"token_exchange": {
"subject_token_types_supported": [
"urn:ietf:params:oauth:token-type:access_token",
"urn:ietf:params:oauth:token-type:refresh_token",
"urn:ietf:params:oauth:token-type:id_token"
],
"requested_token_types_supported": [
"urn:ietf:params:oauth:token-type:access_token",
"urn:ietf:params:oauth:token-type:refresh_token",
"urn:ietf:params:oauth:token-type:id_token"
],
"policy": {
"urn:ietf:params:oauth:token-type:refresh_token": {
"callable": "/path/to/callable",
"kwargs": {
"audience": ["https://example.com"],
"resource": [],
"scopes": ["abc", "def"],
}
},
"": {
"callable": "/path/to/callable",
"kwargs": {
"audience": ["https://example.com"],
"resource": [],
"scopes": ["abc", "def"],
"requested_token_types_supported": [
"urn:ietf:params:oauth:token-type:access_token",
"urn:ietf:params:oauth:token-type:refresh_token",
"urn:ietf:params:oauth:token-type:id_token"
],
}
}
}
}
},
Any configuration under the token_exchange refers to the general configurations regarding the behavior of token exchange handler, i.e the supported subject token types.
Under the policy key exists any subject token specific policy, that is handled by a callable that accepts a set of arguments. If no specific subject token policy is defined then the default callable defined under "" is used.
Any comments?
So it will be
{
"token": {
"path": "token",
"class": "oidcop.oidc.token.Token",
"kwargs": {
"token_exchange": {
"subject_token_types_supported": [] # A list of supported subject_token_types, if not defined then all token_types are allowed
"requested_token_types_supported": [] # A list of supported requested_token_types, if not defined then all token_types are allowed
"policy": {
"token_type": # If {token_type} is not in subject_token_types_supported, then this is ignored
"token_type_2": {
"callable": # A string path to a callable or a callable object
"kwargs": # A dict with extra params that will be passed to the callable in addition to request, token, context, etc
"": {} # The default policy which we will fall back to in case a token_type is supported, but not defined in the policy dict
}