Macaroon access tokens for OAuth: Part 2 – transactional auth

In part 1, I showed how Macaroon access tokens in ForgeRock Access Management 7.0 can be used as a lightweight and easy-to-deploy alternative to proof of possession (PoP) schemes for securing tokens in browser-based apps. The same techniques can be adapted to secure tokens in microservice architectures and IoT applications, and I hope to expand on some of the patterns they enable in future blog posts. But in this post, I want to look at third-party caveats and their application to transactional authorization.

What is transactional authorization?

In a normal OAuth flow, a user is granting some part of their authority to an app or service for ongoing access. For example, you might grant an app read-only access to files in your Dropbox or Google Drive. In these cases the scope of access is usually well defined, allowing the client to easily determine what scope to ask for (eg, read-all-files), and the grant of authority creates a long-lived relationship between the user and the client. If the user stops using the app, or otherwise decides to end this relationship, they can typically go to a page on the authorization server and revoke access from that client.

In transactional authorization the setup is somewhat different. The paradigmatic example is of making a high-value bank transfer. Suppose you want use an independently developed app to send $500 to your friend Alice. In a normal OAuth flow you’d grant the app some static scope like bank-transfer, and then it would be authorized to make any bank transfer on your behalf. But this is a bit risky, unless you really really trust that app, because it could then make lots of bank transfers that you haven’t asked for. The solution is to require authorization for each individual transfer. When you click the button to send $500 to Alice, your bank actively confirms this with you outside of the app to make sure that you really intended it.

Illustration of money being transferred using a mobile app.

For this to work, the user needs to be told the details of the transaction she is approving: how much money is being transferred, from which account, and to who? It seems intuitive that you could use OAuth to approve these individual transactions. Unfortunately, OAuth is not very good at details. The set of scopes understood by an authorization server is usually fixed and independent of any particular request, so there is no easy way to encode the specific details of a transaction. To get around this, the real Open Banking specifications (in the UK at least) require that the payment details are registered with the bank first to obtain a ConsentId that is then included in the OAuth authorization request by (ab)using the OpenID Connect claims parameter. My employer, ForgeRock, have a good explainer of this process.

This is all a little awkward, so there are proposals to add native support for transactions to OAuth 2, or to develop a new protocol that supports this from the start (gnow a separate IETF working group).

Macaroons and transactions

You may wonder how macaroons fit into all of this, seeing as this is a post about macaroons. Well, it turns out that they are a very good fit for transactional authorization, due to something known as third-party caveats. I’ll get to what those are in a moment, but first let’s consider some aspects of using OAuth for this. Suppose you had an OAuth provider that let you send a richly structured description of the transaction instead of a simple scope string. This is what the Rich Authorization Requests (RAR) proposal for OAuth 2 allows. The process would look something like the following:

  • To initiate a payment, the client (app) would send an authorization request to the authorization server (bank) with the details of the transfer.
  • The bank would then authenticate the user and ask for consent to approve this specific transaction.
  • If approved, the client would get back an access token with the scope being the details of the transaction.
  • The client uses the access token to call an API on the bank to actually perform the transfer. As part of this, the bank would check that the approved detailed scope matches the actual transfer being requested.

In principle this all works fine, but it lacks some safeguards. Every transaction is effectively a brand new interaction with the client and the access grant is only valid for that one specific transaction. This has some negative consequences:

  • There is no permanent relationship between the user and the client app stored at the AS. This means that if the user uninstalls the app or otherwise decides she no longer wants to use it, there is no way for her to revoke the ability for it to initiate payment consent requests.
  • Any registered client with the bank can suddenly decide to initiate a payment, even if I have no pre-existing relationship with them. As the continued success of phishing attacks shows, if you can pop up a consent screen on a user’s device then the battle is half won. In a highly regulated ecosystem like Open Banking you can perhaps rely on legal controls to prevent clients acting unethically, but as a user I’d prefer that only clients that I have explicitly approved can ask me to initiate transactions.

You can read more detailed discussion of these issues in the email thread I linked above. Not everyone agrees with my position, so make your own mind up.

We can use macaroon access tokens to overcome these issues. The basic idea is that the user would approve a long-lived access token (and optional refresh token) when she first installs an app or signs up for a service. This access token would have a normal static OAuth scope like initiate_payments. Then individual transfers can be authorized by appending a caveat that ties the token to a specific transaction, either by referencing a unique transaction ID (like the ConsentId of Open Banking) or directly including the structured transaction details in the caveat. This provides the best of both worlds. The user has a single long-lived authorization grant with the client, which she can revoke whenever she wants, and we can derive unique per-transaction tokens from that to approve individual requests.

Third-party caveats

The major downside of the scheme I just described is that the client can just approve its own transactions by appending a suitable caveat to its access token. No need to involve the user at all! What we want is some kind of caveat attached to the access token that forces the client to obtain authorization from the user for each transaction. This is exactly what macaroon third-party caveats allow.

The caveats we saw in part 1 are all examples of first-party caveats: simple restrictions on how a token can be used that can be checked at the point of use (by the resource server). A third-party caveat can’t be checked locally by the RS, but instead requires the client to visit another service (the third party) and obtain proof that it satisfies some condition. For example, you might have an access token that allows placing orders for alcohol, but only if the client gets a proof from a government service that the user is over 18. The proof is in the form of a discharge macaroon that is cryptographically tied to the original caveat. The client presents the original macaroon and the discharge macaroon to the API to gain access.

(A cool feature is that discharge macaroons can themselves contain caveats, including more third-party caveats, requiring the client to get more proofs before they can access a protected resource).

We can use this property of third-party caveats to implement transactional authorization. The original long-lived access token issued to the client will contain a third-party caveat that requires the client to obtain a discharge macaroon from a separate transaction authorization service in order to initiate a payment. This transaction authorization service will authenticate the user and approve the transaction and then issue a discharge macaroon that approves that one transaction (by adding appropriate first-party caveats to bind it to the transaction ID or details). The client then presents its original long-lived access token and the discharge macaroon to a payment service to action the transfer.

This has some very nice properties:

  • The user gets to manage their relationship with the client over time exactly as they would for any other OAuth client.
  • The permission to ask to initiate transactions is itself properly represented as a separate scope that the user can consent to in an informed manner, rather than just letting any client initiate transactions.
  • Discharge macaroons are completely stateless and so consume no resources on the authorization server.
  • The AS need not be involved in individual transaction authorizations, which is nice if you are outsourcing the AS to a cloud service but don’t want them tracking individual user activity at that fine grained level. (Although you could use features of the AS if you wished, such as ForgeRock’s rich existing support for transactional authorization).

3rd-party caveats in AM 7.0

An idea is one thing, but it would be nice if you could actually implement this. Well, we thought so too, so we built some experimental support for 3rd-party caveats into AM 7.0 alongside the macaroon access token support I described in part 1.

Adding the 3rd-party caveat is simple using AM’s support for access token modification scripts. This allows an administrator to configure a JavaScript or Groovy script to make modifications to access tokens before they are issued. If you turn on macaroon access tokens then you can also add caveats to an access token as shown in the screenshot below:

In this example, we’re adding a third-party caveat to the access token to force the client to go to the transactional authorization service at https://txauth.example whenever it wants to use the access token. The third-party caveat consists of three parts:

  • A hint for where the client has to go to obtain a discharge macaroon
  • A unique random secret key that will be used to sign the discharge macaroon (known as the caveat key). This gets encrypted and encoded into the macaroon so that only the verifier can recover it.
  • An identifier to give to the third party service so that it knows what condition needs to be checked and how to recover the caveat key. In this example, I’ve encrypted the caveat key using a shared secret between the AS and the transactional authorization service, but you could also use public key encryption or pre-register the caveat key with the service and include a unique identifier instead.

Once the 3rd-party caveat is attached, the access token will be considered invalid unless it is accompanied by a discharge macaroon signed with the caveat key. The transaction authorization service can easily create the discharge macaroon using a macaroon library, as shown in this example code I wrote to test the idea using our internal macaroon library:

// Parse the macaroon access token and find the 3rd-party caveat 
// that requires authorization
Macaroon accessTokenMacaroon = Macaroon.deserialize(accessToken);
Caveat txCaveat = accessTokenMacaroon.getThirdPartyCaveats()
        .filter(caveat -> SELF.equals(caveat.getLocationHint()))
        .findAny()
        .orElseThrow(() -> new IllegalArgumentException());

// The key to create the discharge macaroon is encrypted in the 3rd-party 
// caveat, so decrypt it with the shared secret to recover it.
Key caveatKey = decryptCaveatKey(txCaveat);

// Now generate a discharge macaroon that satisfies the 3rd-party caveat.
// We add additional caveats to this discharge macaroon that tie it to 
// this specific transaction and limiting the time it can be used for. 
String dischargeMacaroon = Macaroon.create(caveatKey, SELF, 
            txCaveat.getIdentifier())
        .addFirstPartyCaveat(new JsonCaveatSet()
               .expiresAt(now().plus(1, MINUTES))
               .audience("https://payments.bank.example.com")
               .put("tx", json.toMap()))
        .serialize();

To verify the 3rd-party caveat has been satisfied, we added a new (non-standard!) header to the OAuth introspection endpoint that allows passing the discharge macaroon: X-Discharge-Macaroon. This is a header rather than a body parameter partly to give a hint that this is a non-standard extension and partly because there may be multiple discharge macaroons, but introspection request parameters can’t be repeated.

In the spirit of making this as transparent as possible for the RS (apart from the extra header of course), the introspection response combines the caveats of the original access token and any attached to the discharge macaroon. For example, if the discharge macaroon has an earlier expiry time than the access token then this will be reflected in the expiry time in the response. The same occurs for scope and audience restrictions on the discharge macaroon, making it easy for the transaction authorization service to limit what that client can do in the context of this one specific transaction.

Any unrecognized caveats are returned in a new “caveats” element on the introspection response, allowing the transaction details associated with the discharge macaroon to be returned:

{
  "active": true",
  ...
  "caveats": {
    "tx": {
      "type": "payment_initiation",
      "locations": [
         "https://example.com/payments"
      ],
      "instructedAmount": {
         "currency": "EUR",
         "amount": "123.50"
      },
      "creditorName": "Merchant123",
      "creditorAccount": {
         "iban": "DE02100100109307118603"
      },
      ...
    }
}

The payment service can then check these details match the transaction being processed (for example, using the scripting features of ForgeRock Identity Gateway 7.0 which support extracting and checking individual fields in the introspection response). Importantly, the use of the introspection endpoint ensures that the access token signing key never has to leave the AS.

Summary

In my opinion, third-party caveats present a really interesting alternative way of thinking about transactional authorization in OAuth. They have a lot of advantages compared to other approaches, and you can put together a working solution with very little code. It took me less than 2 hours to prototype a fully working version of the solution presented here using the existing features of AM I’d already added, before I’d even considered that they might be useful for transactional authZ. To me this is one of the real benefits of macaroon access tokens: they enable an extraordinary level of flexibility in the solution design space.

I’m sure there are plenty of downsides to this approach too. It relies pretty heavily on token introspection, which isn’t always a suitable solution. It also requires the RS to handle two (or more) tokens rather than just a single one. (Perhaps it would be better to “staple” them together?) The support for 3rd-party caveats in AM 7.0 is most definitely experimental, but I believe it points the way to some really interesting future capabilities. I’d love to hear feedback on this feature, and I’d love to eventually move towards standardisation of macaroon access tokens for OAuth. Let me know what you think in the comments.

PS – I’ll try and publish the demo code I have so that you can try it out if anyone is interested.

Author: Neil Madden

Security Director at ForgeRock. Experienced software engineer with a PhD in computer science. Interested in application security, applied cryptography, logic programming and intelligent agents.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.