Application Front-Ends Must Not Make Authorization Decisions

Access denied Part II / Carl Graph

First, let’s get the usual introductions out of the way. For an in-depth discussion of what Authorization is, check out this post. For a complete introduction to Authorization concepts see my Authorization Series. This post continues my long-running Authorization Series. In this post, we’re going to explore in what layer of the architecture authorization decisions can securely be made — and, most importantly, what not to do.

Front-End Application Developers (Web, Mobile, Native Desktop, etc) seem to have a common belief that if they do not give the user an opportunity to do something, then it cannot happen. I’ve seen this many times in different contexts. An unfortunate side-effect of modern, distributed applications is that there are numerous calls to some type of back-end “service” where the real-work happens. A typical example comes in the form of a Single Page Application (SPA) with the front-end running in a browser, but the same could be said of traditional web applications, a mobile app running on your phone, or a native desktop app running on your laptop.

In the realm of web applications, maybe you are running a traditional “HTTP is generated on the server-side and returned to a browser” web application. Maybe, you are running a Single Page Application (SPA) that makes REST API calls to a back-end API Gateway (I have written about this architecture frequently). Maybe, it’s GraphQL calls to the back-end. Maybe, you have a similar Javascript running in the browser application that maintains a persistent WebSocket connection to some back-end that receives a continuous stream of updates. There are many variations of this same general theme: front-end calling some type of service on the back-end.

The back-end of these distributed applications must make the data access authorization decisions.

Now, it is possible to construct a counter example, but that isn’t what typically happens with business applications. You could have a desktop or mobile app that is entirely self-contained. Maybe, it doesn’t make any calls out into the world. It is probably kind of boring, but I suppose it could happen.

If that is what you’ve got, this post doesn’t really apply to you. For everyone else, gather around.

All authorization decisions regarding access to critical / private / important user or business data MUST be made in an architectural context that cannot be manipulated by a third-party. That “architectural context” is going to be on the server-side somewhere. Those third-parties include your own users, your customers, former employees, hostile actors, and anyone else beyond the application development team or supporting operations teams (with appropriate code reviews, secured software supply chains, and all the other checks and balances of code deployment).

“All authorization decisions regarding access to critical / private / important user or business data MUST be made in an architectural context that cannot be manipulated by a third-party.”

It’s such an important point I repeated it there just now to make sure I caught your attention. Authorization decisions must not be made in the web browser, native desktop application, or mobile app for any distributed application that relies on back-end services to obtain data. Authorization decisions must be made in a back-end, server-side, component that has been properly secured against external tampering. That “properly secured against external tampering” is the real trick, obviously. I’ve been writing about how to accomplish this for years. The goal of this post is not to dive into those murky details, but instead make the point in bold above.

These authorization decisions can be split into Coarse Grained Authorization (CGA) and Fine Grained Authorization (FGA) decisions. I talk about the distinction here. Regardless of how authorization decisions are implemented or the application context these exist in, the principal noted above holds. Furthermore, once we’ve all agreed where authorization decisions for your distributed application should live, there is the question of how the appropriate application security context is established to provide the information needed to make an authorization decision — this post talks about how to do that.

Why is putting an authorization decision in the front-end so bad? It’s easy for the front-end developer to do and seems to work just fine? There are a couple of reasons:

  1. Javascript code running in the browser can be modified / manipulated on the fly by a skilled attacker using a debugger built directly into the browser (See Chrome Developer Tools). Every major browser on the market today has a similar set of features. If a user can access the browser developer tools, they have access to a debugger and a code editor to modify Javascript code executing fromyour site (in their browser) on the fly. Cconvenient, huh?
  2. Maybe, your organization has disabled these browser features, but for a subset of users (development teams, etc), it will still be enabled so they can do their job.
  3. For native desktop applications, a debugger and IDE can do something very similar. Modern OSes provide a mechanism that allow a program to detect that they are in a debugger, but there are ways around this. The end result is the same as #1 above.
  4. It’s a similar situation with mobile apps. There are sophisticated debuggers / emulators that allow for analysis and modification of mobile app behavior.
  5. Maybe an attacker has your OpenAPI spec and sample calls already (maybe these were published on a developer site intentionally) and can easily bypass all front-end authorization decision mechanisms that your UI has developed.
  6. Maybe #5 combined with stolen credentials completely undermines that same front-end authorization decision mechanisms.

Now, what can you do in the browser? By all means, make decisions about what UI elements to display. However, do not assume for a moment that the lack of a UI element means the user can never call your back-end service. Never assume that an authenticated user will not attempt to force something to happen just because they can’t see a form, page, link, or other UI element. Your user population can potentially contain hostile actors. Your user’s credentials may have been stolen by hostile actors. An internal actor may pose a threat. There are many possibilities. So, let’s put minimum faith in the browser (or device) execution environment and focus on what we have a reasonable chance of securing from all of these potential bad actors.

If you are using OpenID Connect (OIDC), the ID token can be used to make decisions about UI elements shown to the user. For other identity protocols, an equivalent mechanism can be used to make decisions about UI elements on the front-end.

Leave a Reply