Curity sponsored this post.
However, browser security remains a challenging area of web development. Threats such as cross-site scripting (XSS) are a major concern, and a secure development life cycle must be followed to prevent them. Robust and extensible user authentication also must be implemented, including the use of security libraries for your technology stack. All of this effort can be difficult for companies that just want to focus on their business functionality.
Below are some commonly desired behaviors that architects and engineers at software companies aim for when building web apps:
- The web app must be considered secure and pass any security reviews.
- There must be a highly interactive and fast experience for end users.
- Web developers must be freed up to focus on the user interface.
- Code in web apps and the APIs they call must be reliable and easy to extend.
- The web app must perform well for all users globally.
To deliver web apps that meet these requirements, companies often choose to build single-page applications (SPA). However, security for SPAs is a complex topic. Here, I will show how an API-driven approach can provide additional options for meeting all the above goals.
Security Best Current Practice
Today, web security is often implemented via OAuth 2.0 and OpenID Connect, which help to externalize the difficult security work to an identity and access management (IAM) system. This enables the user to authenticate in multiple ways, after which tokens are returned to the web app. During data access, the browser calls APIs, which validate JWT access tokens and then authorize access to resources using scopes and claims.
The current best practice in OAuth 2.0 for browser-based apps is to avoid returning tokens directly in the browser due to the extra attack vectors this presents. Instead, an extra application cookie layer should be developed, and a Backend for Frontend (BFF) is recommended. The browser then sends the latest and strongest browser-based cookies in data access requests, with properties such as these:
Even if your web apps do not yet use OAuth, it’s a best practice to only use secure cookies in the browser. This post is not specific to OAuth technologies. Instead, it focuses on managing certain web technical concerns to achieve the best overall results.
Website Backend for Frontend
The traditional way to manage cookie responsibilities is by implementing a website. If a company uses Java for their backend APIs, they might also choose Java for their website with a setup similar to this:
The web backend then issues cookies but also introduces some wider problems that work against the key requirements mentioned earlier:
- Developers cannot develop only in React since they need to run a Java backend to retrieve static content. They may also have to write backend code to route requests to APIs.
- Using website stacks can result in usability problems such as dropped cookies during navigation or abrupt redirects when a session expires. Getting the frontend and backend technologies to interact reliably in areas like these can be a struggle.
- The React app cannot be deployed without a Java runtime, which may limit deployment options to containers or virtual machines.
Website stacks can provide a quick way for developers to get started and implement working logins. However, some of the deeper requirements in areas such as development productivity may not become apparent until much later.
API-Driven Backend for Frontend
An alternative deployment is displayed below, where the web and API concerns are more cleanly separated, and Java is used only for API responsibilities. For secure cookies to work correctly for the frontend, the BFF must run in the same parent domain as the web origin:
In this setup, the BFF is a utility API that runs on the same site but has a different origin to the frontend. The BFF assists the frontend during authentication and issues cookies once it completes. This setup works in all modern browsers, since they all support cross-origin request sharing (CORS):
- The SPA runs in a web origin such as https://www.product.com.
- The SPA calls a Backend for Frontend at https://api.product.com to start authentication.
- The SPA calls https://api.product.com as an entry point when calling APIs.
Cookies are then issued only for the API subdomain with properties such as these:
Cookies are not used in requests for static web content, which does not need securing and is not secured in websites either. With the API-driven design, though, anything that you consider confidential, including assets such as PDF files containing private data, must be served by APIs and not the web host.
Once this setup is in place, web developers can focus solely on the UI, and a component such as the webpack dev server can be used to serve static content during development, with the BFF running remotely. This avoids the need to run backend components on the local computer.
In production, you can deploy the unsecured web resources anywhere, such as to a cloud provider’s content delivery network (CDN). This enables you to push static content to many global hosting servers at low cost.
For static unsecured web pages, server-side rendering (SSR) can be a valuable technique to achieve fast initial page loads and to ensure good search engine optimization (SEO) results. This involves bundling HTML and data together when the app is built and deployed. The user then downloads static HTML documents from the server, which are cached in the browser.
A different design is usually required for secured views within a single-page application. This is because users typically need to see personalized data, and the browser page must then be updated dynamically. Fast Ajax calls from the browser to APIs are usually the best way to manage this. Web resources for the secured SPA can then be deployed to a CDN. In some setups this can considerably improve performance for remote users by ensuring that web downloads in all locations have a similar latency.
Token Handler Pattern
At Curity, we have developed an API-driven Backend for Frontend that can be used by SPAs secured by OAuth 2.0 and OpenID Connect. The solution consists firstly of a utility API called an OAuth Agent, which helps initiate the authentication flow.
The agent then issues separate cookies containing the different types of OAuth tokens, and small opaque tokens are used that fit comfortably inside cookie size limits of browsers and HTTP servers. Use of cookie storage also keeps the BFF stateless and easy to manage.
During API requests, an OAuth Proxy gateway plugin is used to translate from cookies to JWT access tokens in a high-performance manner. JWTs are then forwarded to APIs, where they are used for authorization:
The OAuth Agent and OAuth Proxy only need to be deployed, not developed. They deal with the tricky security plumbing, including OpenID Connect, cookie issuing and CORS. All of this simplifies the code in your web apps and APIs.
Scaling the Architecture
Cookies add deployment complexity, though, especially when you have multiple web apps. An API-driven Backend for Frontend forces you to think through your current and future deployment early on to identify URLs, domains and subdomains.
For any web architecture where cookies secure the app, you need distinct routes into APIs for each web app. This ensures that if a user is logged into multiple apps, cookies for web app 1 can never be sent by web app 2. This is best managed at the gateway level, and the following illustration shows one way to achieve a valid routing:
When considering your web architecture and an API-driven Backend for Frontend, start by designing your deployed domains to see if this type of solution will work for you. Include development domains in this design process to enable an excellent setup for your web developers.
It is challenging to develop for the browser and meet your essential requirements. As part of your web security design, follow the current best practice of keeping tokens out of the browser and using only the latest and most secure HTTP-only cookies. Implement a Backend for Frontend for each web app to issue secure cookies and aim to separate web and API concerns. Also, follow a secure development life cycle and OWASP guidance for mitigating the Top 10 web security risks.
The following Curity resources provide further information on securing web apps for the best overall architecture, while keeping the security plumbing out of application code. There is also an end-to-end code example that you can run against any standards-based IAM system:
- React SPA Code Example
- Single-Page Application Security Whitepaper
- Overview of the Token Handler Pattern
Feature image via Pixabay.