IdentityPython / oidc-op

An implementation of an OIDC Provider (OP)
Apache License 2.0
64 stars 26 forks source link

Token Exchange support #162

Open ctriant opened 2 years ago

ctriant commented 2 years ago

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:

Some potential conflicts in case of multiple audiences:

nsklikas commented 2 years ago

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 :

I see 2 big problems with this: 1) How will we manage sessions, E.g.:

For (1):

For (2):

Other than that we should add a set of validations based on the subject_token_type. E.g.:

@rohe @peppelinux any thoughts on this approach?

peppelinux commented 2 years ago

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.

rohe commented 2 years ago

@nsklikas My 2c

nsklikas commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

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?

peppelinux commented 2 years ago

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

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

nsklikas commented 2 years ago

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.

peppelinux commented 2 years ago

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

ctriant commented 2 years ago

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?

nsklikas commented 2 years ago

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
}