Skip to content

Create OAuth2 two-legged flow with user identifier associated with the access token #277

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

Open
AxelNennker opened this issue Mar 12, 2025 · 18 comments · May be fixed by #290
Open

Create OAuth2 two-legged flow with user identifier associated with the access token #277

AxelNennker opened this issue Mar 12, 2025 · 18 comments · May be fixed by #290
Labels
enhancement New feature or request fall25

Comments

@AxelNennker
Copy link
Collaborator

AxelNennker commented Mar 12, 2025

Problem description
There is a need for an access token that is created by a two-legged flow (OAuth2 client credentials) but that access token is associated with a user identifier (API target).

As stated in #268 the current CIBA description is a two-legged flow, although CIBA as standardized as a three-legged flow.

Basically the CIBA flow as described solves this issue, but it is easy that that two-legged CIBA can too easily be misunderstood as a three-legged flow which it is not.

Possible evolution

I think there are two ways forward.

  • Describe the existing two-legged CIBA in the ICM profile and make it clear that although personal data is involved a two-legged flow is used.
  • Create a new OAuth2 grant-type in the profile that includes a user identifier and creates an access token associated with that user identifier.
    This would have the advantage, that we do not have to talk about polling etc because the new flow would create the access token in one call.
@AxelNennker AxelNennker added the enhancement New feature or request label Mar 12, 2025
@AxelNennker AxelNennker changed the title Create OAuth2 two-legged flow with user identifier in access token Create OAuth2 two-legged flow with user identifier associated with the access token Mar 12, 2025
@subha5h
Copy link

subha5h commented Mar 12, 2025

@AxelNennker, on the second option:

Create a new OAuth2 grant-type in the profile that includes a user identifier

Instead of creating a new OAuth2 grant-type, couldn't we re-use OAuth2 Client Credential grant-type to additionally include the target-device/target-user identifier within the scope parameter.

The scope parameter has been already enhanced in current grant-types to include dpv:purpose. So, including the target-device identifier would still be OAuth2 compliant and be much simpler than coming up with new grant-type.

As long as the Authorization Server is able to understand this, I believe the OAuth2 framework would not restrict this enhancement of scope parameter in the Client Credential grant-type.

So, the access-token request could look something like this,

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

scope=login_hint:tel:+353876158815 dpv:FraudPreventionAndDetection sim-swap:retrieve-date sim-swap:check location-verification:verify 
grant_type=client_credentials

and like CIBA, login_hint could also be ipport or operatortoken as well.

@AxelNennker
Copy link
Collaborator Author

AxelNennker commented Mar 12, 2025

@subha5h thanks for the proposal. Putting the user identifier into the OAuth2 client credentials flow scope parameter was exactly my though as well.

Maybe others want to keep the two-legged CIBA flow because it is already implemented by some.

I think going the OAuth2 way is cleaner and that is my preferred solution. Also because there is one less network request.

I would just go for tel:+353876158815 without the login_hint. Assuming we never create an API "tel" (nor dpv).

@sfnuser
Copy link
Contributor

sfnuser commented Mar 12, 2025

I would propose using the refresh_token if the main issue is to avoid the slow authentication leg every time when access_token expires in the CIBA flow. Isn't this a standard way? This way the end user session is always maintained in the OP server and when they opt out or modify consent, it becomes easier to revoke the session and associated tokens.

@AxelNennker
Copy link
Collaborator Author

I would propose using the refresh_token if the main issue is to avoid the slow authentication leg every time when access_token expires in the CIBA flow. Isn't this a standard way? This way the end user session is always maintained in the OP server and when they opt out or modify consent, it becomes easier to revoke the session and associated tokens.

Yes, refreshtokens are the standard way to get a new access token without new user authentication.

Users do not have to re-authenticate every time an access token expires. The API Consumer can use the current refreshtoken to get a new valid access token and SHOULD get a new refreshtoken.

Aside from that, law can require that the user authenticates from time to time. German Banking regulation, for example, requires a new strong authentication every 90 day. I think that is based on PSD2 requirements.
CAMARA does not define token lifetimes because that local market requirements and law requirements have their say on that.
I think most off-the-shelf OAuth2 and OpenId Connect allow configuring token-lifetimes and refreshtoken-idle lifetimes.

Everybody, please also read https://datatracker.ietf.org/doc/html/rfc9700#name-refresh-token-protection on refresh tokens.

Even if the API Consumer does not use their current refresh token, that does not mean that the user has to authenticate.
In OIDC Authorization Code Flow the User can have a valid session with the API Provider - cookies are usually used to detect a valid session. That is why in OIDC core the API Consumer can request a new login using "prompt=login" or max_age=0 if the API Consumer has e.g. security reasons to request a new login. No API Consumer is requesting a new user-authentication for fun.

In CIBA there are usually no cookies, but then id_token_hint or login_hint_token can be used as a replacement.
CIBA requests are usually send from the backend. If somebody is sending CIBA requests from a mobile phone, I would like to read their security evaluation.

@sfnuser
Copy link
Contributor

sfnuser commented Mar 13, 2025

Aside from that, law can require that the user authenticates from time to time. German Banking regulation, for example, requires a new strong authentication every 90 day. I think that is based on PSD2 requirements. CAMARA does not define token lifetimes because that local market requirements and law requirements have their say on that. I think most off-the-shelf OAuth2 and OpenId Connect allow configuring token-lifetimes and refreshtoken-idle lifetimes.

Everybody, please also read https://datatracker.ietf.org/doc/html/rfc9700#name-refresh-token-protection on refresh tokens.

I agree. These are off-the-shelf features from most of the OP providers. Sender Constrained Refresh token enforcement is straight forward with mTLS as CIBA requests are usually sent from backend.

Even if the API Consumer does not use their current refresh token, that does not mean that the user has to authenticate. In OIDC Authorization Code Flow the User can have a valid session with the API Provider - cookies are usually used to detect a valid session. That is why in OIDC core the API Consumer can request a new login using "prompt=login" or max_age=0 if the API Consumer has e.g. security reasons to request a new login. No API Consumer is requesting a new user-authentication for fun.

In CIBA there are usually no cookies, but then id_token_hint or login_hint_token can be used as a replacement.

Are we proposing this as a work around to avoid the authentication leg during access_token expiry? Won't we run into the same issue again - what happens when id_token expires if its used as id_token_hint? In the case of login_hint_token, what’s the proposed issuance process, lifetime, and revocation mechanism? Aren't we replicating refresh_token functionality with less standardization? Also these are hints and the standard does not mention that the authentication leg can be skipped when we use them.

While I agree on short term disruption for some providers on their interpretation, but long-term, we should prioritize standards compliance and interoperability to avoid fragmentation and vendor lock-in.

@AxelNennker
Copy link
Collaborator Author

Are we proposing this as a work around to avoid the authentication leg during access_token expiry? Won't we run into the same issue again - what happens when id_token expires if its used as id_token_hint? In the case of login_hint_token, what’s the proposed issuance process, lifetime, and revocation mechanism? Aren't we replicating refresh_token functionality with less standardization? Also these are hints and the standard does not mention that the authentication leg can be skipped when we use them.

While I agree on short term disruption for some providers on their interpretation, but long-term, we should prioritize standards compliance and interoperability to avoid fragmentation and vendor lock-in.

This issue proposes the creation of a flow that creates an access token that is associated with a user identifier, and thus the access token is restricted to that user identifer.

Usually in OAuth2 client_credential flows the client acts on its own behalf and is not restricted.

If we had an OAuth2 client-credentials-based flow, where the access token is associated with an user identifier, then it would be clear to the API Consumer and the API Provider that this flow is two-legged.
CIBA, in contrast, is by definition a three-legged flow.
The "A" in CIBA stands for (user) Authentication.
In CIBA the user authentication is initiated by the backend and the user authentication is done by sending a message to the user's authentication device.

In the proposed flow there would be no id_token. If the API Consumer had an id_token, then they could use CIBA - if CAMARA would allow id_token_hint. In CIBA with id_token_hint the API Provider can decide whether they accept expired id_tokens, that is a policy decision.

In the proposed flow there would be no room for interpretation whether the flow is two-legged or three-legged.
The proposed flow is two-legged, a classic OAuth2 client_credentials flow with an access token restricted to one resource owner.

@jpengar
Copy link
Collaborator

jpengar commented Mar 14, 2025

Basically the CIBA flow as described solves this issue, but it is easy that that two-legged CIBA can too easily be misunderstood as a three-legged flow which it is not.

Calling it 2-legged CIBA is an opinion, and it is arguable. As discussed over the past few weeks, some WG participants have pointed out that the existing documented flow does not strictly make CIBA a two-legged flow, since the user rights to opt-in (user is explicitly authenticated in this case) and opt-out (consent revocation is checked on every auth request) are still maintained, and the access token is indeed associated with the user identifier (the access token contains the three legs, client, auth server and user).

There is a need for an access token that is created by a two-legged flow (OAuth2 client credentials) but that access token is associated with a user identifier (API target).

On the other hand, I think that the proposed changes in #268 could potentially solve issue #258 by not requiring another alternative flow to be defined. The proposed changes are based on not contradicting CIBA, but also not going beyond CIBA itself.

However, if the WG still considers this alternative flow, from Telefónica's point of view we believe that the right way to do it would be to use JWTs as Authorization Grants ("urn:ietf:params:oauth:grant-type:jwt-bearer" grant_type for Oauth 2.0) that provide a JWT assertion following the OAuth Assertion Framework: Using Assertions as Authorization Grants | Client Acting on Behalf of a User.

For example:

POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]

JWT Bearer grant is the flow that actually allows user-associated tokens (three-legged tokens) to be obtained by delegating authentication to the client, which is sufficiently compromised so that the delegated authentication can be certified by the client in an assertion with its associated signature.

In contrast, extending Client Credentials to achieve similar functionality is not advisable IMHO. The Client Credentials grant type specification lacks mechanisms for assertions or for associating tokens with user identifiers. Attempting to retrofit such capabilities would represent a significant and arguably confusing extension, effectively creating a less coherent and potentially redundant alternative to the well-established JWT Bearer grant. This approach seems less aligned with standard OAuth 2.0 practices and could introduce unnecessary complexity.

In summary, we recommend that the changes proposed in PR #268 be adopted. If an alternative is still pursued, JWT Bearer grant provides a standards-based and secure approach. Extending Client Credentials for this purpose is less technically sound and conceptually misaligned. We are open to further discussion within the WG to clarify these points.

@subha5h
Copy link

subha5h commented Mar 19, 2025

existing documented flow does not strictly make CIBA a two-legged flow, since the user rights to opt-in (user is explicitly authenticated in this case) and opt-out (consent revocation is checked on every auth request) are still maintained, and the access token is indeed associated with the user identifier.

Access token documented in Camara CIBA flow might be 3-legged. I believe, the disagreement is not on the access token being 2-legged, but on "Whether the CIBA flow described in Camara today & the access token issued is complaint with Open Id CIBA specification when no authentication is done during auth flow?".

If consent capture is needed, authentication would be done during the flow, which may align with standard Open Id CIBA flow.
But,

  • If consent already exists, authentication would have been done before the CIBA flow is triggered.
  • And, if consent is not explicitly needed (opt-out), then authentication would not have been done anytime.

So, within Camara WG we may still agree to keep the CIBA flow as it is. But would it still be compliant with Open Id or "would it be a Camara variant of Open Id CIBA flow"?

the right way to do it would be to use [JWTs as Authorization Grants] (https://datatracker.ietf.org/doc/html/rfc7523#section-2.1)

  • Is this how the assertion should look like? which means Client_Id need not be sent outside of assertion?
{
    "iss": "<ASP_App_Client_Id_on_CSP>",
    "sub": "<target_device_MSISDN>",
    "aud": "<Operator_Auth_Server_Url>",
    "exp": "<Expiration_Time>"
}
  • In case of an aggregator in between, should the issuer be aggregator_Id because assertion would most probably be generated by Aggregator and then Client_Id would have to be sent separately outside of assertion?
  • Should the JWT be encrypted and not just signed? Because the "sub" claim would include MSISDN (a PII data). And the keys would have to be exchanged during the onboarding done via Operate APIs?
  • Should the purpose be included within the scope attribute? or should it be a one of custom claims within the assertion?

@AxelNennker
Copy link
Collaborator Author

AxelNennker commented Mar 20, 2025

OIDC and CAMARA are using RFC7523 already.

OIDC https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication:~:text=OAuth.JWT%5D.-,private_key_jwt,-Clients%20that%20have
CAMARA https://github.com/camaraproject/IdentityAndConsentManagement/blob/main/documentation/CAMARA-Security-Interoperability.md#client-authentication

The difference between the OIDC usage in private_key_jwt and the original RFC7523 seems to be that RFC7523 uses "sub" to identify the user while OIDC uses sub to identify the client.

It is simple to create a PR to CAMARA-Security-Interoperability.md that says something like:
OIDC requires that the sub field in private_key_jwt is set to the client_id. CAMARA RECOMMENDS that the API Consumer sets that value of the sub field to an User identifier. The API Provider then associates the resulting access token with an User identifier. If the API Consumer sets the sub field to its client_id then the resulting access token is not restricted to one user and if the CAMARA API requires an User identifer then that must be an API parameter. API Providers MUST support a user identifier that is a phone number, e.g. "tel:+34666666666".

@jpengar
Copy link
Collaborator

jpengar commented Mar 20, 2025

OIDC and CAMARA are using RFC7523 already.

OIDC https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication:~:text=OAuth.JWT%5D.-,private_key_jwt,-Clients%20that%20have CAMARA https://github.com/camaraproject/IdentityAndConsentManagement/blob/main/documentation/CAMARA-Security-Interoperability.md#client-authentication

The difference between the OIDC usage in private_key_jwt and the original RFC7523 seems to be that RFC7523 uses "sub" to identify the user while OIDC uses sub to identify the client.

It is simple to create a PR to CAMARA-Security-Interoperability.md that says something like: OIDC requires that the sub field in private_key_jwt is set to the client_id. CAMARA RECOMMENDS that the API Consumer sets that value of the sub field to an User identifier. The API Provider then associates the resulting access token with an User identifier. If the API Consumer sets the sub field to its client_id then the resulting access token is not restricted to one user and if the CAMARA API requires an User identifer then that must be an API parameter. API Providers MUST support a user identifier that is a phone number, e.g. "tel:+34666666666".

This is confusing very different things... The assertions can be used either for client authentication or, in this case, as an authorization grant. CAMARA already uses them for the former (private_key_jwt) and also for the signed request object (signed authentication requests), but these assertions are independent and can't be mixed.

@subha5h
Copy link

subha5h commented Mar 20, 2025

The assertions can be used either for client authentication or, in this case, as an authorization grant.

Couldn't the same assertion be used for both client authentication and authorization? and to do that in Camara, I assume we just need to overrule the below OIDC recommendation and use sub claim for target-device-identifier instead of client_id as suggested by @AxelNennker

The JWT MUST contain the following REQUIRED Claim Values and MAY contain the following OPTIONAL Claim Values:
iss REQUIRED. Issuer. This MUST contain the client_id of the OAuth Client.
sub REQUIRED. Subject. This MUST contain the client_id of the OAuth Client.

@AxelNennker
Copy link
Collaborator Author

This is confusing very different things... The assertions can be used either for client authentication or, in this case, as an authorization grant. CAMARA already uses them for the former (private_key_jwt) and also for the signed request object (signed authentication requests), but these assertions are independent and can't be mixed.

Yes, those are different things.

The OAuth2 Access Token Request example looks like this:

POST /token HTTP/1.1
     Host: server.example.com
     Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
     Content-Type: application/x-www-form-urlencoded

     grant_type=client_credentials

The example from RFC7523 looks like this:

POST /token.oauth2 HTTP/1.1
     Host: as.example.com
     Content-Type: application/x-www-form-urlencoded

     grant_type=authorization_code&
     code=n0esc3NRze7LTCu7iYzS6a5acc3f0ogp4&
     client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3A
     client-assertion-type%3Ajwt-bearer&
     client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.
     eyJpc3Mi[...omitted for brevity...].
     cC4hiUPo[...omitted for brevity...]

OAuth2 client authentication says:

If the client type is confidential, the client and authorization
server establish a client authentication method suitable for the
security requirements of the authorization server. The authorization
server MAY accept any form of client authentication meeting its
security requirements.

The CAMARA API Provider's Authorization Server could accept this form of client authentication example (with extra line breaks for display purposes only):

POST /token HTTP/1.1
     Host: server.example.com
     Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.
     eyJpc3Mi[...omitted for brevity...].
     cC4hiUPo[...omitted for brevity...]
     Content-Type: application/x-www-form-urlencoded

     grant_type=client_credentials

The bearer token would be the signed and encrypted JWT from RFC7523.

{
  "iss": "https://backend.api-consumer.com",
  "sub": "tel:+353876158815 ,
  "aud": "https://az.api-provider.com",
  "nbf": 1300815780,
  "exp": 1300819380,
  "iat": 1300819080
}

I think RFC7523 was created because the IEFT oauth WG did not want to touch three years young OAuth2 standard.
So, the above is mostly an intellectual exercise we could agree on in CAMARA.

I guess it is "cleaner" to use RFC7523 as a two-legged flow.
Here is some info about support by various vendors.

@jpengar
Copy link
Collaborator

jpengar commented Mar 21, 2025

Based on the RFC7523 standard and existing CAMARA specifications, the process involves two distinct JWT (JSON Web Token) assertions for client authentication and authorization grant.

Firstly, for client authentication, CAMARA mandates the use of private_key_jwt as the client assertion. This requires providing a client assertion within the authentication request. The type of this assertion is indicated by the client_assertion_type field, which should be set to urn:ietf:params:oauth:client-assertion-type:jwt-bearer, and the actual JWT assertion is provided in the client_assertion field.

Secondly, an authorization grant is also required. This is a separate JWT assertion used to grant access. It is provided in the authentication request using the grant_type parameter set to urn:ietf:params:oauth:grant-type:jwt-bearer. The JWT assertion itself is placed in the assertion field.

Therefore, if we stick to the standard, the authentication request necessitates two different JWT assertions serving distinct purposes: one for authenticating the client and the other for representing the authorization grant.

Here is how the authentication request would look:

POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.
...

Note that this follows existing standards without deviation. The only additional detail CAMARA would need to specify is how to fill the sub claim of the Authorization grant assertion (tel: and other values we decide to allow).

Client assertion payload example:

{
  "iss": "client_id",
  "sub": "client_id",
  "aud": "https://az.api-provider.com",
  "exp": 1504807731,
  "iat": 1504804131,
  "jti": "53f42eb1-b751-44b5-bada-6990e08f35ad"
}

Authorization grant assertion payload example:

{
  "iss": "client_id",
  "sub": "tel:+34666666666",
  "aud": "https://az.api-provider.com",
  "exp": 1504807731,
  "iat": 1504804131,
  "jti": "53f42eb1-b751-44b5-bada-6990e08f35ac"
}

It's worth noting that since the authorization grant assertion is also signed using the client's key (identified by the client_id in its iss claim), the client's signature is also effectively validated by this assertion. This suggests a potential redundancy in requiring a separate client assertion, so depending on the specific security requirements and trust model, it could be allowed not to send it.

@jpengar jpengar added this to the Meta-release Fall25 milestone Mar 25, 2025
@jpengar jpengar added the fall25 label Mar 25, 2025
@AxelNennker
Copy link
Collaborator Author

+1 for not sending the client assertion IF we do this.

I think semantically it is the same whether the assertion is send in an HTTP Authorization header or as an assertion post parameter. I would suggest to add the grant_typ as a "typ" to the assertion to make sure what is is.

{
  "typ": "urn:ietf:params:oauth:grant-type:jwt-bearer"
  "iss": "client_id",
  "sub": "tel:+34666666666",
  "aud": "https://az.api-provider.com",
  "exp": 1504807731,
  "iat": 1504804131,
  "jti": "53f42eb1-b751-44b5-bada-6990e08f35ac"
}

I believe that API gateways are easier to configure to validate Authorization headers, but that is just a guess on my behalf.

@garciasolero
Copy link
Contributor

@AxelNennker, I do not think it is necessary to send the claim typ, as the grant_type parameter already indicates the content of the assertion parameter in the request itself (JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants). However, if it is intended to be sent, the claim typ is usually included in the header of the JWT rather than in the payload.

@james-emerson
Copy link

james-emerson commented Apr 4, 2025

@AxelNennker thank you for raising this. From an aggregator/channel partner perspective this will ensure API adoption for backend APIs that do not require end user consent (e.g. SIM Swap).
I see this is announced in the Fall '25 release
..but we still need to define and agree the two-legged auth including JWT design as it is not solved by #268

@AxelNennker
Copy link
Collaborator Author

Please see #268 (comment)

The flow I proposed there uses the signed request object as a client assertion and the login_hint=operatortoken: as the User assertion.
The flow keeps to our architecture principle that all the access decisions (legal, Purpose, scope-based, ...) are done by the API Provider's authorization server and the API endpoint then is simple in the sense that it just checks the access token.
Redundancies mentioned above are avoided.

I think that API providers who currently have CIBA implementations already can adapt to this flow quite easily.

@HuubAppelboom
Copy link

@AxelNennker One of the problems I see with creating yet another flow with an access token tied to a specific user, is that such a flow is inherently slow, because such an access token can only be created at the time the resource call is needed to be executed.
Any client credential implementation the way it is done now (liek in Mobile Connect) is much faster because the Access Token can be retrieved well in advance.

If your main worry about why the token should be tied to a specific number is privacy or security, there may also be other options to reduce the possibility to abuse tokens, simply by restricting how many times a token can be used, and potentially how many tokens an API Consumer may have collected (to be used in the next hours or so).

In its simplest form, for a specific client_id you allow the API Consumer to have one token in stock (or in a cache), and which can be used for any next resource call. That way you keep the moment of when the token is retrieved separate from when the resource call is being executed. This way you can also address the use cases where you need sub 100ms latencies.

For API Consumers that have a lot of traffic, you can also agree that they can hold more than one token in stock if that becomes a bottle neck.

I think this method will also address some of the privacy concerns, but will give a much shorter latency at the time of a transaction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request fall25
Projects
None yet
7 participants