Transaction Authorization or why we need to re-think OAuth scopes
Have you ever come across limitations of the way OAuth expresses the requested scope of an access token? Well, I have several times in the course of the last couple of years in the areas of open banking and remote electronic signature creation.
Let’s take the example of a payment authorization: If you want to authorize a payment using OAuth, you need to pass amount, currency, recipient, and purpose to the authorization server. Otherwise, the authorization server cannot gather the user’s consent for that particular transaction and mint an access token that is really constrained to the transaction’s parameters (e.g., the amount). A data structure to carry this data could look like this (based on NextGenPSD2):
{
"instructedAmount":{
"currency":"EUR",
"amount":"123.50"
},
"debtorAccount":{
"iban":"DE40100100103307118608"
},
"creditorName":"Merchant123",
"creditorAccount":{
"iban":"DE02100100109307118603"
},
"remittanceInformationUnstructured":"Ref Number Merchant"
}
Another example is electronic signing where the user needs to authorize the creation of signatures for one or more documents. This effectively means to pass the type of signature (different types result in different legal consequences!), the list of document labels along with the respective hash values into the OAuth authorization process. Here is an example (based on the latest ETSi standard):
{
"credentialID":"qes_eidas",
"documentDigests":[
{
"hash":"sTOgwOm+474gFj0q0x1iSNspKqbcse4IeiqlDg/HWuI=",
"label":"Mobile Subscription Contract"
}
],
"hashAlgorithmOID":"2.16.840.1.101.3.4.2.1"
}
All this transaction data is needed to fully define the intended scope of the access token. Passing a fixed scope like „payment“ or “sign” is not enough.
Let’s face it: authorizing transactions using OAuth is a challenging use case, and it’s rather new. It goes beyond what had been done with OAuth so far.
OAuth Scope Parameter
So it does not come as a surprise that the structured data as described above won’t easily fit into the OAuth scope parameter. First of all, the syntax definition of the scope parameter [RFC 6749] does not allow to use backslashes and quotation marks. Well, one could Base64URI encode the JSON structure and put it into the scope parameter, something like this:
https://server.example.com/authorize?
response_type=code&
client_id=s6BhdRkqt3&
redirect_uri=https://client.example.org/cb&
scope=eyJ0eXBlIjoic2VwYS1jcmVkaXQtdHJhbnNmZXIiLCJpbnN0cnVjdGVkQW1vdW50Ijp7ImN1cnJlbmN5IjoiRVVSIiwiYW1vdW50IjoiMTIzLjUwIn0sImRlYnRvckFjY291bnQiOnsiaWJhbiI6IkRFNDAxMDAxMDAxMDMzMDcxMTg2MDgifSwiY3JlZGl0b3JOYW1lIjoiTWVyY2hhbnQxMjMiLCJjcmVkaXRvckFjY291bnQiOnsiaWJhbiI6IkRFMDIxMDAxMDAxMDkzMDcxMTg2MDMifSwicmVtaXR0YW5jZUluZm9ybWF0aW9uVW5zdHJ1Y3R1cmVkIjoiUmVmIE51bWJlciBNZXJjaGFudCJ9&
state=af0ifjsldkj&
code_challenge_method=S256&
code_challenge=5c305578f8f19b2dcdb6c...0b4642eb890b97e HTTP/1.1
Host: as.bank.example
Looks a bit alien and has a significant downside — it will break most existing OAuth implementations since they directly process the parameter as space delimited set of string values. Now, the authorization server would first need to Base64URI decode the scope parameter and, at best, parse the result into a JSON document for further processing.
Additionally, the request URL might get really big and the request data is not protected from modifications by the user.
Are there other options?
Sure! Several patterns to carry rich authorization data in conformance with RFC 6749 have been implemented in the last couple of years, e.g., driven by PSD2 and eIDAS. Let’s take a look into them.
Lodging Intent Pattern
This pattern is, for example, used by UK Open Banking and NextGenPSD2. An extensive description can be found at the OpenID Foundation’s FAPI WG site.
This pattern basically uses a HTTP resource to represent the transaction authorization data and passes a link to this resource into the authorization process.
Let’s assume a client wants to conduct a payment transaction. In the first step, it creates a payment resource using the structure introduced above in a request like this:
POST /payments HTTP/1.1
Host: api.bank.example
Content-Type: application/json
Authorization: Bearer eyJraWQiOiJOQnlW…
{
"instructedAmount":{
"currency":"EUR",
"amount":"123.50"
},
"debtorAccount":{
"iban":"DE40100100103307118608"
},
"creditorName":"Merchant123",
"creditorAccount":{
"iban":"DE02100100109307118603"
},
"remittanceInformationUnstructured":"Ref Number Merchant"
}
The HTTP response contains the id of the resource.
HTTP/1.1 201 Created
Content-Type: application/json
Location: /payments/36fc67776
{
"paymentId":"36fc67776"
}
This identifier is then used in the authorization request to refer the authorization server to this resource. UK Open Banking uses a special claim value to carry this data, NextGenPSD2 uses a dynamic scope value (like “payment:36fc67776”). The following example uses the latter approach.
GET /authorise?
responseType=code&
client_id=3630BF72-E979–477A-A8FF-8A338F07C852&
redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&
scope=payment%3A36fc67776&
state=S8NJ7uqk5fY4EjNvP_G_FtyJu6pUsvH9jsYni9dMAJw&
code_challenge_method=S256&
code_challenge=5c305578f8f19b2dcdb6c...0b4642eb890b97e HTTP/1.1
Host: as.bank.example
The authorization server obtains the transaction details from this resource and use it to parameterize the authorization process.
Most significantly, the user must be presented with all the details in the user consent dialog and the authorization server also needs to convey this data to the respective resource server(s) with the access token issued as result of the process.
Open Banking implementation experience has shown that this kind of dynamically parameterized authorization process requires changes to most existing OAuth implementations.
For example, most implementations today require a static list of supported scope values that are mapped to fixed text for the user consent screen. That’s fine for requesting “read” access to a user’s profile since the description can be static. In case of a payment authorization, however, the authorization server needs to render a consent screen showing the actual transaction details, e.g. the amount, to the user for confirmation. Apparently, fixed scopes, simple text blocks, and a bit of css/template magic don’t work.
In some cases, the user will even be asked to select options, e.g. the account to withdraw the money from. And not to forget, this selection and the other transaction details need to be carried through the rest of the process to the resource server via access tokens and token introspection.
In my opinion, vendors face the challenge to provide a new kind of extensibility via programmatic extensions points and APIs so implementers can develop application-specific consents.
Important to keep in mind: This challenges are not specific to the lodging intent pattern but caused by the dynamic and rich nature of the use cases in open banking or signature creation.
So let’s come back to the lodging intent and its pros and cons:
- Lodging intent complies to RFC 6749 (although most OAuth implementations so far did not support non-static scope values).
- It keeps the (additional) authorization data out of the authorization request URI, i.e. it does not inflate authorization URLs and provides data integrity and authenticity without the need to sign data.
- In my experience this pattern works very well, in particular if the resource represents a certain transaction over its various stages, e.g., setup, confirmation, submission and (potentially) cancellation of a payment.
- But it somehow feels overdone if applied to just complex authorization in a non-transactional case, such as access to account information. I’m in doubt that the cost of implementing another resource „just“ to convey the authorization data is the simplest solution.
So let’s look for simpler solutions.
Scope-specific URI request parameters
OpenID Connect, for example, combines the static scope value “openid” with the additional JSON-encoded „claims“ parameter. This parameter might be used to, in detail, define the requested claims and their allocation to the ID Token and the User Info response.
Let’s map this to our payment example. We could use a scope value „payment“ and a corresponding URI query parameter with the same name like this:
GET /authorize?
response_type=code&
client_id=s6BhdRkqt3&
redirect_uri=https://client.example.org/cb&
scope=payment&
state=af0ifjsldkj&
payment={"instructedAmount":{"currency":"EUR","amount":"123.50"},"debtorAccount":{"iban":"DE40100100103307118608"},"creditorName":"Merchant123","creditorAccount":{"iban":"DE02100100109307118603"},"remittanceInformationUnstructured":"Ref Number Merchant"}
code_challenge_method=S256&
code_challenge=5c305578f8f19b2dcdb6c...0b4642eb890b97e HTTP/1.1 Host: as.bank.example
- As the first observation, this pattern requires the authorization server to be aware of the application-specific relationships between a scope values and a certain URI query parameters (e.g. “payment” and “payment”). I bet every OpenID Connect product has hard coded this for „openid” and „claims”. But is this desirable for additional applications, such as payments, as well? I don’t think so. As a vendor or maintainer, I would rather prefer to have a generic solution that can be customized to the application‘s needs. Using the same name for both the scope value and the parameter would be one option, in more sophisticated solutions the scope value could refer to the associated query parameter similar to the way the dynamic part of the scope value refers to a certain resource in the lodging intend pattern.
- The pattern has further challenges: The requests will get quite voluminous, the authorization data is not protected against modifications, and JSON in URI query parameters just looks alien and isn’t that straightforward to handle.
Let’s first look how the secure transmission of the authorization data can be achieved.
Request object
OpenID Connect provides a solution with the request object mechanism, which is also making its way into OAuth through the JWT Secured Authorization Request (JAR) draft.
A request object allows a client to represent a request as a JSON Web Token (JWT). This JWT contains all OAuth authorization request parameters as top level elements and is signed and (optionally) encrypted.
The following example transforms our payment authorization request into a request object:
{
"iss":"s6BhdRkqt3",
"aud":"https://server.example.com",
"response_type":"code",
"client_id":"s6BhdRkqt3",
"redirect_uri":"https://client.example.org/cb",
"scope":"payment",
"state":"af0ifjsldkj",
"code_challenge_method":"S256",
"code_challenge":"5c305578f8f19b2dcdb6c3c955c0a…97e43917cd",
"payment":{
"instructedAmount":{
"currency":"EUR",
"amount":"123.50"
},
"debtorAccount":{
"iban":"DE40100100103307118608"
},
"creditorName":"Merchant123",
"creditorAccount":{
"iban":"DE02100100109307118603"
},
"remittanceInformationUnstructured":"Ref Number Merchant"
}
}
Looks good, but sending this JWT in the request URI would clearly intensify the URI size problem.
The request object mechanism therefore allows the client to host the request object and just pass the respective URI to the authorization request, which is shown in the following example.
GET /authorize?
request_uri=https://client.example.org/67e652c0... HTTP/1.1
Host: as.bank.example
The AS will then fetch the request data from this URI.
- This pattern has clear advantages: rich authorization data can be sent in a JSON object with integrity and authenticity protection while keeping the request URI size pretty small (actually even smaller than with plain OAuth).
- But there is also a downside to it: The client needs to maintain and expose request objects. This might look easy on first sight, but the client needs to be able to handle inbound requests from the authorization server and, potentially, store a large number of objects in its database including the need to properly clean them up.
- It also means the availability and latency of the authorization process at the authorization server depends on the availability and latency of the client’s backend.
- Moreover, server-side requests brings all the problems of server-side request forgery.
But help is approaching …
Pushed Request Object
… as OpenID Foundation’s Financial Grade API Working group specifies a new mechanisms to host request objects at the authorization server.
In order to utilize this mechanism, the client creates a request object and posts it to the authorization server as shown in the following example (using a JWT representing the payment request object as shown above).
POST https://as.example.com/ros/ HTTP/1.1
Host: as.example.com
Content-Type: application/jws
Content-Length: 1288
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzNkJoZFJrcXQzIiwiYXVkIjoiaHR0cHM6Ly9zZXJ2ZXIuZXhhbXBsZS5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF9pZCI6InM2QmhkUmtxdDMiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2NsaWVudC5leGFtcGxlLm9yZy9jYiIsInNjb3BlIjoiYWNjb3VudHMiLCJzdGF0ZSI6ImFmMGlmanNsZGtqIiwiY29kZV9jaGFsbGVuZ2VfbWV0aG9kIjoiUzI1NiIsImNvZGVfY2hhbGxlbmdlIjoiNWMzMDU1NzhmOGYxOWIyZGNkYjZjM2M5NTVjMGEuLi45N2U0MzkxN2NkIiwiYWNjb3VudHMiOnsiYWNjZXNzIjp7ImJhbGFuY2VzIjpbeyJpYmFuIjoiREU0MDEwMDEwMDEwMzMwNzExODYwOCJ9LHsiaWJhbiI6IkRFMDIxMDAxMDAxMDkzMDcxMTg2MDMiLCJjdXJyZW5jeSI6IlVTRCJ9LHsiaWJhbiI6IkRFNjcxMDAxMDAxMDEzMDYxMTg2MDUifV0sInRyYW5zYWN0aW9ucyI6W3siaWJhbiI6IkRFNDAxMDAxMDAxMDMzMDcxMTg2MDgifV19LCJyZWN1cnJpbmdJbmRpY2F0b3IiOnRydWUsInZhbGlkVW50aWwiOiIyMDE34oCTMTHigJMwMSIsImZyZXF1ZW5jeVBlckRheSI6IjQifX0.KeGf3wYWPpzkKESQUjGIC9IJCY_aZLRY1gyvMy4qzZwwi0iMudQVgpR4–83HHbOXtUidvd74VU7ReBBFzmi4PC8
B29XXr7FFLffgtYzQPbzXQ7kaqcnTbwSvVJ57h27hGXQh3NifwDhjgcUt1pBskvjDRG8JPmUUcTgVb-ZSsQavuzADl9WgVN64pWT51I0zykOa9t_XbzZuKv8atQFIhRK7exEeoR8
8lRubtMWPd9pRca2GNL1A-2Qnhab6aG9CimIjh9rjHcFwOUUw0Ovmf5KuVqU_TGGgBln
uxp6YlL2L4aSBo-JQbrLPLyNxPe6P30oM-w5Hda6zklTJMfedhg
Note: The client authenticates towards the AS by signing the request object.
The Authorization server responds with a request URN
HTTP/1.1 201 Created
Date: Tue, 2 May 2017 15:22:31 GMT
Content-Type: application/json
{
...
"request_uri":"urn:example:MTAyODAK"
}
The client then uses this URN in the following authorization request.
GET /authorize?
request_uri=urn:example:MTAyODAK HTTP/1.1
Host: as.bank.example
- This approach solves all the problems mentioned in the last section. The client is relieved from the burden to maintain and expose request objects. The user experience at the authorization server no longer depends on availability and latency of client provided services and server-side request forgery went away.
- And there are further advantages: The authorization server is able to authenticate the client and to check the integrity of the request before the actual authorization request is sent. This helps to refuse bad requests early and it significantly increases the trust an authorization server may place on the client’s identity in the authorization flow! This is new to OAuth 2 (and conceptually re-introduces OAuth 1 request tokens :-)).
- In my opinion, pushed request objects and lodging intent share the same advantages while the pushed request object has the appeal of being a universal solution.
- But the pushed request object as described so far still does not overcome one of the key disadvantages of the “scope-specific URI query parameter” pattern: It requires the authorization server to be aware of application specific relationships between scope values and URI query parameters (e.g. “payment” & “payment”) making a fully generic implementation of this pattern in OAuth products difficult to achieve.
So let’s recap why we landed here: the intent was to come up with solutions to carry rich authorization data in conformance to RFC 6749. And the limitations of the OAuth scope parameter caused the development of solution that split the data across scope values and additional data representations, either in a parameter or as external resource.
What if we approach the problem a bit more disruptive and extend OAuth? Why not just introducing a new kind of scope?
Structured Scope
Let’s call it structured scope. And well, yes, it’s JSON-encoded and may contain multiple objects each of them describing the permissions requested on a certain API or resource.
The following example shows the authorization details for the payment transaction within the new structured_scope element.
{
"iss":"s6BhdRkqt3",
"aud":"https://server.example.com",
"response_type":"code",
"client_id":"s6BhdRkqt3",
"redirect_uri":"https://client.example.org/cb",
"state":"af0ifjsldkj",
"code_challenge_method":"S256",
"code_challenge":"5c305578f8f19b2dcdb6c3c955c0a…97e43917cd",
"structured_scope":{
"payment":{
"instructedAmount":{
"currency":"EUR",
"amount":"123.50"
},
"debtorAccount":{
"iban":"DE40100100103307118608"
},
"creditorName":"Merchant123",
"creditorAccount":{
"iban":"DE02100100109307118603"
},
"remittanceInformationUnstructured":"Ref Number Merchant"
}
}
}
You see the scope definition is now self-contained, no need to wire scope values and parameters or external resources any longer because everything needed to parameterize the authorization request is provided in a single object.
The new syntax also allows to carry authorization data for different resources as named objects in a single authorization request as shown in the following example.
{
"iss":"s6BhdRkqt3",
"aud":"https://server.example.com",
"response_type":"code",
"client_id":"s6BhdRkqt3",
"redirect_uri":"https://client.example.org/cb",
"state":"af0ifjsldkj",
"code_challenge_method":"S256",
"code_challenge":"5c305578f8f19b2dcdb6c3c955c0a…97e43917cd",
"structured_scope":{
"sign":{
"credentialID":"qes_eidas",
"documentDigests":[
{
"hash":
"sTOgwOm+474gFj0q0x1iSNspKqbcse4IeiqlDg/HWuI=",
"label":"Mobile Subscription Contract"
}
],
"hashAlgorithmOID":"2.16.840.1.101.3.4.2.1"
},
"payment":{
"type":"sepa-credit-transfer",
"instructedAmount":{
"currency":"EUR",
"amount":"123.50"
},
"debtorAccount":{
"iban":"DE40100100103307118608"
},
"creditorName":"Merchant123",
"creditorAccount":{
"iban":"DE02100100109307118603"
},
"remittanceInformationUnstructured":"new Smartphone"
}
}
}
This example combines the request to get the confirmation to sign a contract for a mobile subscription along with the approval for a credit transfer to pay the new smartphone.
The AS is supposed to pass the structured_scope object (or the sub objects) to suitable extension modules that are able to render the respective user consent and add the required data to access tokens or token introspection responses.
To me combining pushed request objects and a structured scope looks like the optimal solution to solve the rich authorization challenge in OAuth.
- It’s generic since new applications must only publish the name of its scope object. The authorization server then needs to know what extension module will process the respective new scope object.
- It’s developer friendly (for both client and AS developers), secure, and less error prone than the other patterns discussed throughout this article.
Summary
In this article, I explained some of the pattern to support rich and transaction authorization with OAuth. All patterns presented are reasonable, but somehow also feel like workarounds to cope with the limitations of standard OAuth scopes.
I therefore propose to extend OAuth to natively supports the new use cases in open banking and electronic signing by introducing both structured scopes and pushed request objects.
What do you think?
Acknowledgements
I would like to thank Brian Campbell, Daniel Fett, Sebastian Ebling, and Dave Tonge for their feedback and advice.