oidc-op icon indicating copy to clipboard operation
oidc-op copied to clipboard

Token Exchange support

Open ctriant opened this issue 3 years ago • 11 comments

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:

  1. A USER_A accesses CLIENT_A and retrieves an Access Token AT1 with a set of scopes that includes the offline_scope.
  2. CLIENT_A sends AT1 to CLIENT_B.
  3. Then CLIENT_B exchanges AT1 with a new Refresh Token RT1 with the same scope set, but sets the audience parameter of the request to be CLIENT_C and CLIENT_D.
  4. Finally CLIENT_B, CLIENT_C or CLIENT_D may use RT1 to get Access Token AT2 with the same or fewer scopes (and optionally with a different audience) to access protected resource X. Equivalently, AT2 will 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 AT1 belongs to a session USER_A;;CLIENT_A in terms of oidc-op.
  • On the contrary, at step 3 the exchanged refresh token RT1 should be mapped in a different client in order for CLIENT_B to be able to use it. This in terms of oidc-op is interpreted as a new session USER_A;;CLIENT_B where the token should be assigned.
  • In step 4, only the owner and the corresponding audience of token RT1 are allowed to use it. Currently, oidc-op retrieves the session that RT1 is mapped to and checks if the client_id stated in the request matches the client of the session. This check should be modified in order to include a check upon the audience of the used token.
  • In RFC-8693 there is no strict definition of what the audience (or even resource ) parameter should represent. For now, we intend to map the audience parameter with oidc-op client_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.

ctriant avatar Nov 26 '21 09:11 ctriant

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:

  1. 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.
  2. How will we decide if a mapping between a set of token_type/scope/resource/audience to 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_type is 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?

nsklikas avatar Dec 13 '21 11:12 nsklikas

Hi, happy to see this PR indeed

@nsklikas on your thoughts

  1. 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

  2. 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.

peppelinux avatar Dec 13 '21 11:12 peppelinux

@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

rohe avatar Dec 13 '21 13:12 rohe

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.

nsklikas avatar Dec 15 '21 09:12 nsklikas

@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 itself

regarding 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?

nsklikas avatar Dec 15 '21 14:12 nsklikas

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?

nsklikas avatar Dec 15 '21 14:12 nsklikas

STS is here https://datatracker.ietf.org/doc/html/rfc8693

what's the specs you're considering for this token endpoint?

peppelinux avatar Dec 15 '21 22:12 peppelinux

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.

nsklikas avatar Dec 16 '21 08:12 nsklikas

The STS is only an endpoint, it can be hosted anywhere

peppelinux avatar Dec 16 '21 09:12 peppelinux

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?

ctriant avatar Dec 27 '21 12:12 ctriant

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
}

nsklikas avatar Dec 28 '21 13:12 nsklikas