Skip to content

Solving Broken Access Control using FAPI 2.0 and Zero Trust

Broken Access Control is currently the number 1 API Security Risk in the OWASP Top 10 and has been since 2021.  I’ve used concepts from the Financial-Grade API (FAPI) 2.0 specification and a Zero Trust approach to help control this risk at an enterprise financial services client, who built their web front end on a SaaS platform while storing their critical financial and sensitive PII data in their data centre and retrieving it on demand.

Let’s start by defining what problem we’re trying to solve. Pretty much all IT systems built in the last 10 years expose API’s to exchange data.   To facilitate this exchange, each system needs a way to uniquely identify the record that is updated or retrieved –a key that is common across all systems.  System also need to know the identity of the person or system that is making the request, so they can determine if the operation is permitted – otherwise knows as an authorization check.

If systems don’t perform this authorization check before processing an API request, bad things can happen.  The Optus hack in Australia in 2022 which exposed up to 9.7 million current and former customer records was an example of Broken Access Control – where an attacker can quickly enumerate through records by manipulating a single input value.  In the past, I’ve discovered vulnerabilities in systems about to go live where a customer could update the delivery address for any other customer – and redirect goods currently in transit to their own address.  Luckily, we caught that one before it went into production.

It’s now very common for data to be stored separately from the system that the end user is logged into – so data needs to be retrieved or updated in another system.  This is what opens the door for a Broken access Control attack.

Authorization in a distributed model

If you’re not persisting data in the application front end, how do you validate in systems that are not the front end that the logged in user is authorized to access that data?  A common setup I’ve seen is checking coarse grained permissions on a common identity platform (eg. AzureAD) and fine grained permissions are validated in the system the user logs in to – the “front end”.  Most of the time this front end is a web application, and the logic for the permissions check is stored in the front end application in the form of custom metadata or sometimes even hardcoded into the source code.

When the web application needs to retrieve data stored elsewhere, it might make a “system call” to the System of Record (SOR), using credentials provisioned on the SOR.  Typically this is not a normal user account.  This is a specially provisioned user – an “integration user” that will have permission to access to all the records in the SOR’s database.  This is because the front end system could conceivably be asked by any logged in user to retrieve or update any arbitrary record.  The easiest way to enable this is to give the front end system access to all the records in the database.

For those of us who learn better from a diagram, that looks something like this:

That API call to retrieve or update data in the SOR must contain a reference to the object that is being updated – in a bank that might be an account number or a customer reference number.

This is what can be exploited by a threat actor, because if they can manipulate the front-end web application to bypass the checks that it should be making, the attacker could retrieve or update an account number they shouldn’t normally be allowed to.  The SOR doesn’t know if the request is legitimate or not, all it knows is that the request comes from the Front End and is using the correct credentials.

Making it worse with Chain of Trust

The architecture shown above isn’t so bad when you only have 2 tiers in your end-to-end architecture – a front end and a database, however most modern enterprises are significantly more complex, forming complex call chains.

How does the web front end retrieve data on demand in this instance?  To do this, there needs to be a trust relationship established between each system in the call chain, which is called a ‘chain of trust’. System A Trusts System B, System B trusts System C, System C trusts System N. And because of this, System N is forced to trust System A – even though they will never interact directly.

In this chain-of-trust model, System N will have lost any and all context of who is initiating the transaction and if they’re permitted to do so.   That end user could be a customer, a shared account holder, a person who has delegated authority to a customer’s business account (eg. accountant), a bank staff member, a system from another bank requesting an incoming transfer.. the list is endless.  Architecturally, this is great because you only have to build a single system and every other system can leverage it.  You can offload the responsibility of performing authorization checks onto the various front-end systems and then return (or update) the requested data when they ask. 

Earlier I mentioned that this chain could be exploited to steal data. But if the user permissions are configured as read and write, an attacker can now alter data or even insert new records. This scenario is significantly more harmful because it can destroy the organization’s faith in the integrity of the data within the SOR.

Using a Zero Trust approach to secure your APIs

How can this be made more secure?  We need a way to pass some additional details of the transaction, adding context to the API call.  With this additional context, the system of record can then make it’s own determination if that particular end user is permitted access to the data before executing the requested transaction.

Let’s again look at a common use case in a banking setting – a use case that requires a high level of assurance around the integrity of the transaction.  Let’s say a bank staff member needs to view a customer’s account and transaction details.  Let’s also assume that the solution uses a web app based front end that is hosted in a public cloud environment.

First we need to define what information needs to be passed from the front end, down to the System of Record.  We should have the following:

  • The identity of the staff member (user) who is requesting data access;
  • The identity of the account owner;
  • What operation the user wants to perform.  In this case it is an account read request.
  • The account number we’re performing the operation on. 

We then need a way to pass this information securely down to the system of record, using a method that is tamper proof.  We do this by using a signed JSON Web Token (JWT).  You can read more about JWTs here, but the main point is that a JWT uses public key infrastructure (PKI) to protect the integrity of it’s contents.  A token minting service signs the JWTs when they are created (or ‘minted’) and any other system can use a public key to validate the signature in the JWT to make sure that the contents haven’t been altered.

The process looks something like this:

This diagram is somewhat simplified but should be fine for our purposes.  The key steps are:

  1. The staff member logs into the front-end system using Single Sign On (SSO)
  2. The web app front end will check with its delegated identity provider, in this case Azure Active Directory.  Azure will issue an access token, which is part of the normal SSO process.  A copy of this token is stored securely in the web front end.
  3. The user then navigates to the required screen for that customer.  The front end performs it’s fine grained authorization checks as normal and if it determines the user is allowed to, it initiates an API call.  The difference now is that the front-end needs to get a JWT minted before the API call is made.  The front end will first make a request to the token issuing service, passing the saved Azure access token for the logged in staff member.
  4. The token service can now prove the identity of the staff member using the Azure access token by asking AzureAD if it’s valid.  Other entitlement checks can be made at this point if desired – for example checking that the user is a member of a specific Active Directory group.  If any of the checks fail at this stage, the token service will refuse to issue a token.
  5. The Token service must also validate the customer identity – ensuring that the customer is a valid customer and that this particular staff member is permitted to perform the requested account and transaction read operation upon this customer.  It can do this by checking with the customer identity platform (which is hopefully a separate tech stack to the staff IDP)
  6. Assuming all checks pass, a valid short lived JWT is returned to the web app.  Short lived is arbitrary, but a good starting point is 15 minutes.

The result is that the web front end now has a signed token that has the required information above (staff identity, customer identity and permitted operation) encoded as claims inside the JWT.  This token is also set to expire after a short time frame of around 10 minutes. Now when the front end needs to make the API call to the SOR, that token can be passed in the header of each API request, like this:

Each service in the API call chain only has to trust a single centralized system – the token issuing service – in the same way the identity platform is trusted when using Single Sign On.  This JWT can then be independently validated by each service in the call chain without the requirement of trusting other services in the chain (although they can still do that too, see below).  Each service in the chain must perform JWT validation checks, such as:

  1. Is the token valid and signed correctly?
  2. Has the token expired?
  3. Does the token contain the correct scope for this API call? Is this token allowed to be used for this API call?  Or is the system trying to use a token with read permissions on a write operation?
  4. Does the contents of the API payload match the contents of the token?  For example does the token contain account 1234 and the API is trying to retrieve account 1235?

This can be done using a JWKs endpoint on the Token Service, which hosts the JSON Web Key Set (JWKS).  I won’t go into details on how this works (you can read that here), but it’s purpose is to prove that the JWT contents haven’t been tampered with. 

When the API call makes it to its final destination – the SOR that holds the requested data – that system can then validate the JWT and perform it’s own entitlement check because now it has all the information needed to do so.  Is this staff member allowed to retrieve the account and transaction information for the customer John Smith?  Does the account number requested match the account number in the JWT?  Does that customer still own that particular account?

If all the checks pass, perform that operation and send back the requested data, otherwise return an error.   In this way we’re allowing the system that actually holds the financial transaction details to validate that the requested operation is allowed – not the front end, not the API gateway or any of the intervening systems. 

What are the benefits of doing this?

What are the benefits of passing the end user context to the SOR in a secure way?  Well, let’s look at a common internal threat arising from the chain-of-trust approach:

In this scenario, an internal threat actor manages to get hold of the system user credentials that the Internal API Gateway uses to authenticate to the SOR.  Because the SOR has no context of the original transaction the user requested, our hacker can request or even update any data on the SOR that those credentials allow – they could have access to the whole database.  In addition, system user credentials are usually rotated infrequently – sometimes not at all because rotation may cause downtime.  So that attacker could write a script that runs which slowly siphons customer information or financial transaction data away from the SOR without raising any alarm bells.

How does our JWT solution help mitigate this insider threat?

It enables Entitlement Validation by the data owner – Because this method preserves the user context of the original request, this allows entitlements to be validated by the system that owns the data – the data master.  This means that personas that should not have access to that data are stopped at the data source and the data owner now has final control over which entities can access that data.

It provides a method of distributed authorization – By passing a JWT down the call chain, every element in the chain now has a fast way of checking if the API call payload is valid.  The payload of the API request must match the claim that’s in the JWT.  If they don’t, the system can reject the API request outright (and log the attempted request so somebody can investigate).   

This allows you to reject invalid requests long before they ever get to the SOR.

It stops Enumeration Attacks – If any of the system credentials in the chain are lost or stolen in the chain-of-trust model above, an attacker will have access to all the data in the SOR.

But if the JWT is stolen by an attacker, the data loss or data integrity risk has now been constrained to a single record – the account number that is encoded in the JWT.  The attacker can no longer use that JWT as a generic credential and run a script to extract multiple records – or potentially all the enterprise’s data.  Losing 1 record is bad, but losing all records is a disaster.

It provides a time limited method for data access on the SOR – If the credential is lost or stolen, an attacker only has a short amount of time to exploit the token compromise and steal data.  Depending on how the credential is stolen, an attacker might be able to gain access to additional tokens when the one they have expires but expiring access token does make accessing data over a long time period (eg. via a script) significantly more difficult.

It supports non-repudiation and auditing – The system of record now has an irrefutable way to audit exactly who requested the data, as it holds a token signed by the identity provider with this information.  It can then log this data (a subset of the payload data, not the token itself!) if required for auditing purposes.

What are the drawbacks?

This design can provide a very high level of assurance, but there is increased complexity in the design which means an increased operational cost.  It also makes troubleshooting more difficult since the new security solution may be blocking legitimate requests and nobody can understand why.  In addition, all the elements in the chain must be configurable or extensible enough to support the solution – particularly the systems on the two ends of the chain – and this custom extension must be built and maintained.

This doesn’t completely solve the entitlement problem

The organization still needs to understand and define which personas are allowed to access which pieces of data.  You can use this solution to strictly enforce the entitlement policy – but you still need to define what that policy is.  You also need to solve the not so insignificant problem of how to manage entitlements across multiple elements and ensure that the policy enforcement is consistent across each policy enforcement point.

This is made even more complicated because each element in the chain will most likely have completely different technology and will be expecting these policies are defined in their own custom way which won’t be consistent.  The policy needs to be updated at the same time across all elements, otherwise API calls will start failing.

If a staff account is compromised, this solution doesn’t help

If the credentials of a staff member are compromised in a way that allows an attacker to access the cloud system as if they were a staff member, this solution provides no benefit.  There are other controls that you can deploy to prevent this from happening such as only allowing traffic that originates over the corporate network and enabling MFA on each login.  With increasing WFH and mobile workforce, there is no perfect solution that can prevent a hacker literally stealing a physical laptop that’s already logged in and using it to make transactions. 

Similarly, if the entire cloud platform is compromised to such an extent that the token issuer will issue a valid JWT, all the zero trust in the world won’t help you. This solution only forms one piece of the puzzle and you should always be looking to deploy other overlapping controls in conjunction with this one so a single control failure doesn’t take down the entire solution.

The identity solution (IDP) must validate the identity of the calling party prior to minting the token

The identity of the calling party (the staff member in our example above) must be ‘known’ by the token issuing service and there must be a way for that service to validate this identity. If the IDP used by the token service can’t perform this check, then a token shouldn’t be issued.  If the calling party identity can be forged, the authorization checks performed downstream become meaningless.

System Tokens

There will still be scenarios or use cases where a chain of trust is required because a tightly scoped JWT cannot be used.  Batch processes and data reconciliation processes are two examples of this.  In these cases the initiating party is not a person, it’s a system and systems that perform batch updates generally need an elevated level of access because they are updating multiple records at once.  You can still pass a JWT in these cases, but it you can make it clear that these JWTs have an elevated level of access.  If these tokens are stolen, it’s the same as stealing the integration user credential and we’re back to our original problem, so their use must be tightly controlled.

Organizational Inertia

This solution needs cooperation.  Sometimes a lot of cooperation as every element in that chain is probably owned and operated by a different team and their vision and direction will need to be aligned to implement something like this.  Ideally, everybody in the call chain should accept, validate and pass on the assertion JWT, otherwise your chain is weakened.

But if the inertia is too great and you can’t get buy-in from everybody, a hybrid approach may work where the “on-prem” systems form their own chain of trust and anything cloud based uses zero trust.  The on-prem systems may be older, or they may need to support many different consumers and have complicated validation rules so it could take some time to implement any changes. 

This hybrid zero trust approach may be an acceptable intermediate step until everybody in the enterprise gets aligned with an end-to-end solution and the SOR itself is able to validate entitlements.

Further Enhancements

Depending on your requirements, this may not be enough and you may want even more secure APIs.  Here are some more ideas to increase your level of assurance:

Using the JWT as a Bearer Token

It’s possible to combine API authentication and authorization into a single token and use the JWT as a bearer token.  This means turning it into an access token used to make the API call (which is allowed under RFC-9068 using “typ”:”at+JWT”). 

Similarly, it’s possible to define fine grained scopes for each API which are encoded into the JWT.  When an API is called, the JWT is checked for the presence of the custom scope.  If the scope is present in the JWT, the call is authenticated. 

Both these methods turn the JWT into a bearer token – all a calling party needs is to possess the token and they can make the API call.  I don’t really like this bearer token approach since if the JWT is compromised, they can call the API if they have network access to any of the system endpoints in the call chain. 

The FAPI approach is to use a proof-of-possession token, or a sender constrained token.  The problem with applying those approaches to our use cases is that the resource server (the SOR) needs both the token and a client certificate for validation. If you have to pass a certificate down a call chain along with the token, you just broke your own security control – as the certificate should never leave the client. Unfortunately, sender constrained tokens don’t scale out to support the API call chain common in enterprise environments.

The best hybrid approach I have found is to use the JWT for authorization only and not authentication.  This will mean that each API call is authenticated independently from each other, just like in the chain of trust, and the JWT that is passed in the header is relied on for authorization checks only.  An attacker would need to compromise both the system credentials and the JWT to be successful.

A client certificate and using mutual TLS for each API call in the chain also works well, and all three can be used simultaneously – a clientid/secret for basic auth, client certificate (enforcing mTLS) and the JWT for authorization.

Encrypting the token using JWE

You could also use an encrypted token (JWE) instead of an unencrypted token (JWT).  Before going down this path, consider why you want to encrypt the token because there are some significant operational impacts to doing so.  The primary aim with the solution is to pass the identity of the calling party and the data they are acting upon.  To do this we want to protect the integrity of the data in the JWT, but you may not necessarily need to protect the confidentiality. 

To understand if you need to also protect the confidentiality, you need to know the data that will be encoded in the JWT and it’s classification as well as the systems or apps that will have access to the JWT in transit.  In the use case in my example above, I assumed that all the intermediate systems are internal and under control of the organization and that that data encoded in the JWT is not sensitive.  In this case, taking special measures to protect the data in the token is not required and using a JWT is acceptable.

If this was not the case – say the token will be sent to a mobile app running on a customer’s device where it’s undesirable for the customer to inspect the contents.  You can use a JWE, but this means that each system in the call chain will be unable to read the content as well, unless they have access to the private key.  You would need to build a separate solution to distribute, rotate and keep the private key in sync across all systems needing to decrypt the JWE.

Validating all identities prior to minting the token

In the section above, I mentioned that the identity of the calling party must be checked, but you can also verify the identity of the customer as well.  In our example, it was assumed that the staff member was allowed to access the customer records.  This would normally be via the contract the customer signs with the bank that allows them to store and access data about the customer, combined with a bank business rule that says a staff member with a customer service role is permitted to view customer data. 

But what if the staff member couldn’t look up any customer records until the customer had logged into their banking app and once authenticated, sent the staff member a valid token?  The web front-end could then send the staff member’s token plus the customer’s own token to the token issuing service, which would be checked for validity before issuing the JWT.  This would have the effect of confirming that the customer has given explicit permission for the staff member to access their records.

Replay Attack Protection

The “jti” claim (JWT ID) is a unique ID that’s assigned to each JWT by the token issuer.  If added as a claim to the JWT (it’s use is optional), then systems in the chain can keep track of JTI’s that have already been sent and prohibit the use of the same JWT twice.  This will prevent replay attacks but the tradeoff is that it could also introduce other issues such as increasing error handling complexity when an otherwise legitimate request fails.

Use a separate external token format

It’s possible to define different token formats and signing authorities for internal vs external tokens – similar to internal vs external DNS servers.  If an external token is somehow lost, an attacker can’t then take that token and use it to make an API call internally.  To implement this, there needs to be a token exchange service sitting in the network perimeter which validates the external token and then mints a token in the internal format.

Signing the return response

If you wanted to guarantee the integrity of the payload in the return response from the SOR, you could also get the SOR to sign the response payload.  This could be implemented similarly to a JWT Secured Authorization Response Mode (JARM) request. You’re then doing essentially the same approach described above but in the opposite direction.

Final Thoughts

Hopefully this has been helpful in showing you that there are ways to protect your crown jewels in an enterprise environment, when using a cloud front end.  Perhaps you can extend on the work here and if you do so, feel free to let me know!  Leave comments or contact me if you have any questions or feedback.

Further Reading

AOuth – The OAuth 2.0 Framework (RFC 6749) give you all the detail on how OAuth should work.  If you learn more easily via diagrams, you can also check out these OAuth 2.0 flow diagrams by Takahiko Kawasaki which are quite good.

OIDC – The Open ID Connect specification and how it extends OAuth to add authentication.

Find out more about how JSON Web Tokens (JWT) work and how they can be validated by a JSON Web Jey Set (JWKS) service. 

Sender Constrained Tokens – will show you the two methods of implementing sender constrained tokens (mTLS vs DPoP) and a comparison between the two.

Financial Grade API Security Profile (FAPI) 2.0 – Many of the ideas for this implementation came from FAPI part 2 (advanced security) and have been extended for use with longer call chain.

JARM Requests – JWT Secured Authorization Response Mode (JARM).  This gives some more details if you like the idea of signing the return response.

Client Initiated Backchannel Authentication – Has some great discussion around CIBA and including diagrams on the different implementations.

Leave a Reply

Your email address will not be published. Required fields are marked *