← Back to list

Why FAPI mandates `code id_token` even when Client does not need an id_token?

because it is suggested as a alternative to JARM with minimum impact to the Client and Authorization Server.

Arjun Balla · 2025-01-02 21:30 · 1 claps · 8.2 min read
#fapi #oauth #oidc #jarm #jar
Open on Medium ↗
Wiki topics: LIT · Literature & Writing

Why FAPI mandates code id_token as response_type (Detached Signature s_hash and c_hash) even when Client does not need an id_token?

Prerequisites for understanding:

Short Answer:

Detached Signature, response_type=code id_token with s_hash and c_hash in the id_token is an alternate solution requiring minimal changes in both the Client and the Authorization Server to achieve the same functionality provided by the JARM (JWT Secured Authorization Response Mode) specification without implementing JARM.

Explanation:

The FAPI specification is designed for high-security ecosystems. In such systems, every party involved must ensure that a messages (request or response) originates from the intended party before consuming it. Additionally, every party must maintain a record of every message and be able to undeniably prove who made the request and who responded (non-repudiation). This ensures authenticity (the message is from the intended party), integrity (the message has not been tampered with), and accountability for audit purposes.

English meaning of non-repudiation is a situation where a statement’s author cannot successfully dispute its authorship or the validity of an associated contract.

An Authorization Request is made by the Client to the Authorization Server via the End-User’s User-Agent (browser or mobile web-view) while the End-User is using the Client’s web application. Below are examples.

GET Authorization Request:

GET https://as.com/authorize
?response_type=code
&client_id={client_id}
&redirect_uri=https://client.com/redirect
&scope={scopes}
&code_challenge={code_challenge}
&state={client_creates_same_returned_by_as}

POST Authorization Request:

POST https://as.com/authorize
Content-Type: application/x-www-form-urlencoded

response_type=code
&client_id={client_id}
&redirect_uri=https://client.com/redirect
&scope={scopes}
&code_challenge={code_challenge}
&state={client_creates_same_returned_by_as}

If you closely observe the above Authorization Request(s), there is nothing in the request that allows the Authorization Server to confirm that the request originated from the intended Client before processing the information. Additionally, there is nothing in the request that the Authorization Server can record and use to undeniably prove that the Client made the request. You might think the client_id in the request serves this purpose, but client_id is essentially public information (anyone can get the client_id by using the Client and initiating the OAuth flow) and the request could be made by anyone, not necessarily the intended Client. Even if the intended Client made the request, the Authorization Server would not be able to undeniably prove it if the intended Client denies having made the request.

You might think that since OAuth mandates the use of HTTPS, TLS inherently provides authenticity, integrity, and encryption. You are right but however, in regular TLS, the Server proves to the Client that it is who it claims to be, but the Client does not prove its identity to the Server. In mutual TLS (mTLS), both the Server and the Client verify each other’s identities. Here, we are using regular TLS where Authorization Server proves to Client its identity. Even with mTLS, the Authorization Server cannot confirm that the request has come from the intended Client because the TLS session is terminated at the load balancer, and the request could be modified before reaching the Authorization Server’s backend code.

Similarly, in the Authorization Response shown below to the above Authorization Request,

Redirect Authorization Response with response_type=code:

HTTP/1.1 302 Found
Location: https://client.com/redirect
?code={code}
&state={client_creates_same_returned_by_as}

Form-Post Authorization Response with response_type=code:


HTTP/1.1 200 OK
Content-Type: text/html
<html>
 <head>
  <title>Auto-Submit-Form</title>
 </head>
 <body onload="document.forms[0].submit()">
  <form method="post" action="https://client.com/redirect">
   <input type="hidden" name="code" value="{code}" />
   <input type="hidden" name="state" value="{client_creates_same_returned_by_as}" />
  </form>
 </body>
</html>

there is nothing in the response that allows the Client to confirm that the response has come from the intended Authorization Server before processing the information. Additionally, there is nothing in the response that the Client can record and use to undeniably prove that the response was sent by the Authorization Server.

Again, you might think that since OAuth mandates the use of HTTPS, TLS inherently provides authenticity, integrity, and encryption. You are right but however, if you observe carefully, the Client backend is not directly receiving the response from the Authorization Server. Instead, it is being delivered via the browser. This is true for both HTTP Redirect and HTTP Form Post flows.

When the Client makes an Authorization Request via the browser over HTTPS, a TLS session is established between the browser and the Authorization Server. This TLS session ensures that the response originates from the intended Authorization Server. However, this TLS session is terminated as soon as the redirect or form-post response reaches the browser. A new TLS session is then established between the browser and the Client backend, ensuring the response is sent to the intended Client.

Between these two TLS sessions, the information becomes vulnerable and can be easily altered. A simple malicious browser plugin with permission to read and modify addresses could intercept and tamper with the query params of the redirect. It would be a little difficult to do in case of a form-post, but it can be done if the plugin manages to get the required permissions.

For this reason, even when using HTTPS, HTTP Redirect or HTTP Form Post is inherently insecure.

How to solve this problem?

If Alice sends a message to Bob and wants to prove to Bob that she is the one who sent it, the only way to do so is for Alice to sign the message with her private key. Bob can then verify the signature using Alice’s public key.

Similarly, if Bob responds to Alice with a message and wants to prove to Alice that he is the one who sent it, the only way is for Bob to sign the message with his private key. Alice can then verify the signature using Bob’s public key.

So, to solve this problem, the Client needs to sign the Authorization Request with its private key, and the Authorization Server will verify the Authorization Request using the Client’s corresponding public key. Similarly, the Authorization Server should sign the Authorization Response with its private key, and the Client will verify the Authorization Response using the Authorization Server’s corresponding public key.

Handling Authorization Request Signing Requirement:

To address the Authorization Request situation, we have the RFC 9101 JWT Secured Authorization Request (JAR) specification. In JAR, all the request parameters are encapsulated within a JWT and signed using the Client’s private key.

Request Object JWT Sample:

{
 // header
}
.
{
 “iss”: “{client_id}”,
 “aud”: “https://as.com",
 “response_type”: “code”,
 “client_id”: “{client_id}”,
 “redirect_uri”: “https://client.com/redirect",
 “scope”: “{scopes}”,
 “code_challenge”: “{code_challenge}”,
 “state”: “{client_creates_same_returned_by_as}”,
 “exp”: 1700000000 // UNIX timestamp for expiration time
}
.
signature-generated-with-client-private-key

Request Object is sent by value as request param:

GET https://as.com/authorize
?client_id={client_id}
&request={request_jwt}

Another approach is for the Client to host the JWT at an HTTPS-URL and include it as a reference in the Authorization Request.

Request Object is sent by reference as a request_uri param:


GET https://as.com/authorize
?client_id={client_id}
&request_uri=https://client.com/request_jwt_1234567890

Authorization Server, by verifying the signature of request_jwt using the Client’s public key, can confirm that the request originated from the intended Client. Additionally, the Authorization Server can retain request_jwt as a record to prove that the Client made the request. Since the JWT is signed with the Client’s private key, the Client cannot deny having sent the request, as only the Client possesses that private key.

You might be wondering why the client_id is duplicated both inside and outside the JWT. This duplication addresses two specific scenarios:

Scenario 1: If request_jwt is signed first and then encrypted using a symmetric algorithm, the encryption relies on a shared secret, typically the client_secret. For the Authorization Server to decrypt the request_jwt, it must identify the Client to retrieve the corresponding client_secret. This identification would not be possible if the client_id is only inside the encrypted JWT.

Note: This limitation applies only to symmetric encryption. In the case of asymmetric encryption, the JWT is encrypted by the Client using a dynamically generated symmetric key. That dynamically generated symmetric key is then encrypted using the Authorization Server’s public key obtained from the Authorization Server’s JWKS URL and marked with use=enc. The Authorization Server can decrypt the dynamically generated symmetric key using its private key and then decrypt the JWT using the decrypted symmetric key and then get client_id.

Scenario 2: When the Request Object is sent via request_uri, the Authorization Server needs to retrieve the request_jwt from the provided HTTPS URL. It is unsafe to fetch the JWT from external URL without validation. To white-list the URL, the Authorization Server needs to identify the Client’s white-listed URLs. If the client_id is only inside the request_jwt, the Server cannot perform this validation. Some Authorization Servers offer the option to pre-register request_uris following the OpenID Connect Client Metadata specification.

The specification writers always aim to provide alternatives that offer equivalent security with minimal changes. The Request Object was originally defined in the OIDC specification, and the RFC 9101 JWT Secured Authorization Request (JAR) specification specification follows similar principles. Many Authorization Servers and Clients already support the OIDC version of Request Object signing, so adopting JAR introduced relatively minor changes.

Also, as far as signing the request object is concerned, the specification writers could not find any alternatives, as signing with the Client’s private key is required, and there is no existing option to do that except for the OIDC specification.

Note: ODIC specification request object signing and and JAR specification have some major conflicts which will be explained in another blog and linked here.

Handling Authorization Response Signing Requirement:

To handle the Authorization Response situation, we have the JARM (JWT Secured Authorization Response Mode) specification. In JARM, all response parameters are encapsulated within a JWT and signed with the Authorization Server’s private key.

response_jwt:

{
 // header
}
.
{
 “iss”: “https://as.com",
 “aud”: “{client_id}”,
 “code”: “{code}”,
 “state”: “{client_creates_same_returned_by_as}”,
 “exp”: 1700000000 // UNIX timestamp for expiration
}
.
signature-generated-with-authorization-server-private-key

FAPI Specification writers knew that, many Client implementers and Authorization Server implementers would consider implementing JARM specification as a huge change.

As mentioned earlier, the specification writers always aim to provide alternatives that offer equivalent security with minimal changes. When designing alternatives for Response Object signing using the Authorization Server’s private key, the FAPI specification writers identified id_token as a viable alternative.

Alternate solution to JARM:

If you observe closely, id_token is a JWT signed by the Authorization Server’s private key. When the Client verifies the signature using the Authorization Server’s public key, it can ensure the response originates from the intended Authorization Server. The Client can also retain the id_token as an undeniable record of the Authorization Server’s response.

A possible solution involves using response_type=code id_token instead of just response_type=code. The id_token would be included in the redirect or form-post response. Additionally, code and state would be moved into the id_token, eliminating them from the response parameters outside the JWT.

This approach would break existing Client implementations expecting code and state outside the id_token.

An alternative version could retain code and state outside the id_token while also embedding them within it. However, this approach creates confusion and requires FAPI and non-FAPI Client implementers to handle cases differently.

So, neither of the above two versions is a solution.

The OIDC specification already defines c_hash (code hash) claim in id_token, and adding a new claim, s_hash (state hash), along the lines of c_hash, is a better solution.

In this solution, we will have response_type=code id_token instead of just response_type=code. We will keep code and state outside and add s_hash and c_hash to the id_token.

This solution does not break any existing Clients that expect code and state outside and avoids confusion among FAPI and Non-FAPI Client implementers, as they don’t need to handle the case separately.

id_token with c_hash and s_hash claims:

{
 // header
}
.
{
 “iss”: “https://as.com",
 “aud”: “{client_id}”,
 “c_hash”: “{c_hash}”,
 “s_hash”: “{s_hash}”,
 “sub”: “{sub}”, // Mandatory as per OIDC specification.
 “exp”: 1700000000 // UNIX timestamp for expiration
}
.
signature-generated-with-authorization-server-private-key

Redirect Authorization Response (response_type=code id_token):

HTTP/1.1 302 Found
Location: https://client.com/redirect
?code={code}
&state={client_creates_same_returned_by_as}
&id_token={id_token}

Form-Post Redirect Authorization Response (response_type=code id_token):

HTTP/1.1 200 OK
Content-Type: text/html
<html>
 <head>
  <title>Auto-Submit-Form</title>
 </head>
 <body onload=”document.forms[0].submit()”>
  <form method=”post” action=”https://client.com/redirect">
   <input type=”hidden” name=”code” value=”{code}” />
   <input type=”hidden” name=”state” value=”{client_creates_same_returned_by_as}” />
   <input type=”hidden” name=”id_token” value=”{id_token}” />
  </form>
 </body>
</html>

In a FAPI implementation, the Authorization Server must include both c_hash and s_hash claims in the id_token. The Client must validate the id_token signature, compute the c_hash and s_hash using the outside code and state it received and finally match the computed c_hash and s_hash values with the c_hash and s_hash claims inside the id_token before processing the response.

In this context, id_token is merely an alternative for signing the Authorization Response and is not intended to convey End User information. For this reason, the id_token in such cases should exclude End User details (PII — Personal Identifiable Information).

For this reason, the FAPI specification suggests response_type=code id_token without PII (Personal Identifiable Information) in id_token as alternative JARM implementation.


메타데이터
post_id
b546defb9444
slug
why-fapi-mandates-code-id-token-even-when-client-does-not-id-token-b546defb9444
url
https://medium.com/@arjunballa/why-fapi-mandates-code-id-token-even-when-client-does-not-id-token-b546defb9444
canonical_url
https://medium.com/@arjunballa/why-fapi-mandates-code-id-token-even-when-client-does-not-id-token-b546defb9444
author_url
https://medium.com/@arjunballa
status
ok
fetched_at
2026-06-26 21:52:29