So, ID Token in OpenID connect is audience restricted to the client while the OAuth bearer access token is audience restricted to the protected resource. It is a bearer. It can be used by anybody. It is a common model in the real world. Cash and many train tickets are such an examples. Other example bearing the name “bearer” is “bearer bond“. It clearly is a valid use case. I call the token in this category “bearer token”.
There are however times that you want to restrict the party who can exercise the token. There are many transportation ticket that are “registered” (e.g., airline ticket) and there are category of bond called “registered bond”. It can only be used by the registered/named party. I call the token in this category “registered token”. This is inherently safer, though it has privacy impact in some cases.
Currently, OAuth only has the bearer token variant. It would be very useful to have the registered token variant as well. There can be many ways to do it, but what I propose here is very simple.
Introduce a structured into access token. JWT would be my choice. The payload would look like this:
{
"typ":"AT",
"iss":"server.example.com",
"sub":"1357924",
"azp":"client.example.net",
"aud":"resource.example.org",
"iat":1343947082,
"exp":1356892034,
"jti":"1234642",
"obl":"api123:r,api54:w"
}
Note that I have created a new claim name called “azp” standing for “authorized presenter”, which is a client in OAuth speak. Also, I am using “typ” claim to state that this is an access token. “obl” is a new claim that stores the authorization decision/privilege granted/obligation that the resource server has towards the request. The “obl” claim can be omitted, in which case, the protected resource has to go to the token resolution service to find out what it is meant for. It is specific to the protected resource.
This is signed by the issuer to create a JWS token. Typical algorithm would be RS256. i.e., the header would be:
{"alg":"RS256"}
The compact serialization of JWS is then produced and is used as the access token.
Given this structure, the protected resource can figure out to whom the access token was issued.
Response Authentication
The next step then is the client authentication[1]. If the protected resource is read only, we are pretty much there: If we encrypt the response by using the public key of “azp”, it is done. We do not need anything else. Response authentication will do. (Actually, this is what I have pointed out in the OAuth session in IETF 84 today.)
However, if the protected resource involves “write” operation, this does not work. The request has to be authenticated.
Request Authentication
There are many ways to do this as it was proposed in the OAuth session in IETF 84.
One of the simplest way to do it is to over-sign the access token. To avoid the size bloat, I would like to propose the following.
- Take the sha256 of the access token.
- use it as a payload and create a JWS Signature in compact seriazlization format. .
- Send it with the WWW-Authorize: header together with the access token like below:
GET /resource/1?b=1&a=2 HTTP/1.1 Host: example.com Authorization: Reg at="this.is.access.token", sig="this.is.jws.sig"
The protected resource, upon receipt of the above, checks to see the signature actually is that of “azp” of the received access token. If it is not, reject.
Now that the requesting client is authenticated, the requested action can be fulfilled.
What to do?
For a read only API, response authentication is more efficient. For the API that involves write operation, request also needs to be authenticated. (Note: if there is substantial response, then it would be better to combine with response authentication as well.)
From the point of view of the pure efficiency, picking the suitable method is best.
However, from the point of view of the inter-operability, picking exactly one way of doing is probably better. If that is the case, the option is singular: do both the request and response authentication.
But what about the public client?!
Then, you may ask, “what about the public client? They cannot hold the client secret, so they cannot sign.”
Well, not quite.
The reason why public client is deemed to be not able to hold the secret is that there is going to be multiple instances of the same client. In fact, there can be million copies of the same app on peoples phone etc. They can be reverse engineered and the key can then be extracted. Then, one can write a code to impersonate other client installations.
This is however easy to solve in case of the installed apps.
Just generate the public-private key pair at the installation time. Then, register the public key at the authorization server. (Alternatively, the client can send the public key at the authorization request time like in OpenID Connect to be more dynamic.) The JWT access token that is returned will then have an extra claim, “jku” that points to the public key JWK file of the client [2]. So, it will look like:
{
"typ":"AT",
"iss":"server.example.com",
"sub":"1357924",
"azp":"client.example.net",
"aud":"resource.example.org",
"iat":1343947082,
"exp":1356892034,
"jti":"1234642",
"obl":"api123:r,api54:w",
"jku":"https://client.example.net/jwk/1/"
}
In fact, the same could be applied to a confidential client as well. So, in the spirit of “just one way of doing it”, I suppose using this method is the way to go [3].
Summary
- Registered token can be very useful in many cases.
- To achieve it, the client should publish its JWK at a url and register the url to the authorization server.
- The authorization server then creates a JWS access token that includes bunch of essential claims including “jku” and “azp”.
- The client JWS signs the access token using its key and send it in the HTTP Authorize request header with scheme “named”.
[1] Authentication: formalized process to determine that presented identity information associated with a particular entity is applicable for the entity to be recognized in a particular domain at the time
[2] It could contain jwk claim as well, but it will probably make the result too big to be used as access token.
[3] Apart from the request authentication, there may be a use case that wants to integrity protect the request parameters. One way to do it is to use a similar construct as in OpenID Connect’s request object. Another is to create a normalization scheme around the request and sign it. My take though is that this should be dealt with the channel protection such as TLS instead of doing the complex and error prone normalization.
Yes bos