17 minutes read
In this article, we’ll use the BFF (Backend for Frontend) pattern to build a secure system comprising a web app, a Keycloak server, and a dedicated backend service that brokers authentication flows between them. Our goal? Design an application that never stores sensitive data in the browser. Why? Web apps operate in hostile territory as their runtime environment can’t be trusted. Browser caches, localStorage, and cookies are low-hanging fruit for attackers: easily hijacked, instantly stolen, and fundamentally insecure. To shield users, we must avoid client-side persistence of sensitive data entirely. Enter Backends for Frontends (BFFs). This pattern exiles security operations from vulnerable clients to hardened backend services. Acting as a secure intermediary between Keycloak (our authentication server) and the Angular app (our public client), the BFF handles all communications with Keycloak’s API while insulating the web app from token management. The result? Architectural clarity where services remain blissfully unaware of each other’s internals.
The full code presented in this article can be found here.
What is a BFF?
A BFF (Backend for Frontend) is an architectural pattern where a dedicated intermediary server isolates client applications from backend services. This middleman component adapts interactions to match the specific needs of each client type, such as web apps or mobile interfaces, while shielding them from backend complexity. You deploy one BFF per client category, allowing both clients and services to evolve independently. BFFs are not tied to security and can be used for various reasons.
In this article, we will focus on the BFF’s role in security. Our BFF acts as a trusted broker between an Angular web app, which operates as an untrusted client, and Keycloak, our authentication service. It handles all OAuth2 and PKCE flows, stores tokens securely in server-side sessions, and ensures sensitive operations such as token refresh never reach the browser. The Angular app remains unaware of Keycloak entirely. It interacts only with the BFF’s simplified endpoints, which abstract away the underlying authentication complexity.
We will, of course, use HTTPS and a few self-signed certificates to run the system in a realistic scenario. You don’t have to use the provided docker-compose.yaml to set up the Keycloak server. Feel free to deploy your own because BFFs allow you to replace components easily. All you need to take care of are the URLs that lead to those servers. Although I am a big fan of Nest.js, I decided to use Express.js to keep everything as basic as possible. This component, too, can be replaced very easily.
Is BFF just another Proxy type?
It’s important to understand that a BFF is more than just an intermediary. The BFF pattern is a recognized architectural approach, popularized by Sam Newman in his work on microservices. Unlike a basic proxy, a BFF is tailored to the specific needs of each frontend client, handling client-specific logic, enhancing security by managing authentication flows, aggregating APIs from multiple backend services, and maintaining a clear separation of concerns. This specialized handling ensures that each frontend, whether it’s a web app, mobile app, or desktop client, has a dedicated backend service optimized for its unique requirements.
Setting up Keycloak
My favorite IAM platform is Keycloak. I don’t think you’ll find anything in the open-source world that matches its versatility and the range of security-related options, protocols, and configurations. However, I won’t be talking much about Keycloak here, but I have a few articles to offer. In this article, we will use a preconfigured deployment based on the latest version of Keycloak, v26.1.0. This deployment will import a test realm that contains a single user and a public client definition for our Angular app.
The most important settings are those for volumes. Depending on how you are using the provided sources, you might need to adapt the local paths that point to test-realm.json and the certs folder. For example, I run everything inside my DevContainers in VSCode and therefore prepend my ${HOST_WORKSPACE}
variable that expands to my local user folder. The provided certificates can be directly used as long as you don’t change the dummy passwords I used to generate them. If you have regenerated the Keycloak certificate or want to use your own, you will need to provide the new password, of course. There is also a helper script available to generate certificates for Keycloak, the web app, and the BFF. It also imports the dummy CA into the Trusted Store of your local Linux (Ubuntu) environment. You don’t have to use any of them and can easily replace them with your own certificates.
Public Client Settings in Keycloak
If you have used the provided docker-compose.yml to run the Keycloak instance, a TestRealm will be imported at the initial start. This realm contains a user, test-user, and a Public OIDC Client that uses PKCE to authenticate users. If you are interested in PKCE and what it does, I can offer you an article of mine. When you open the TestRealm on your Keycloak instance and list the available clients, one of them will be angular-public-client.
When you scroll down a bit, you get to the list of URLs that decide which URLs are available to access the server and where the server should return its tokens.
In our case, we state that only addresses that begin with https://localhost:3000/* are allowed to communicate with Keycloak when using this Public Client. The URL https://localhost:3000 belongs to the BFF, which will serve as the “public client”. The actual client, the Angular app, will never talk directly to Keycloak, nor will Keycloak send any data or tokens directly to it. Everything related to security and authentication will happen between the BFF and Keycloak. This strategy is often used to establish SSO (single sign-on) so that users don’t need to re-authenticate themselves each time they attempt to access a protected resource. Currently, the BFF is not doing much with these tokens apart from keeping them in its store. However, one can easily extend the BFF and add logic like automatic logouts after a timeout or token refreshes.
Setting up the BFF
As I mentioned at the beginning, I am a big fan of NestJS and would have preferred to use it instead of Express.js, but because not everyone out there might know NestJS well (it’s complex enough that I am using Angular for the frontend), I decided that a bare-bones Express.js might be a better alternative for this article. The server in question is basically a single index.ts file comprising three parts:
- Express.js setup (taking care of config, session, CORS, logging)
- Passport.js KeycloakStrategy (for setting up the OIDC Public Client)
- The actual API (authentication callbacks, login/logout routes, demo API)
I won’t show the whole index.ts because it’s pretty basic and certainly not a complex piece of code. Here are only the most important things.
We set up CORS to allow the Angular app to call the BFF from its original URL. Additionally, we set up session handling. Although we are using “local” URLs, everything is going over HTTPS to make the whole project as realistic as possible. Ideally, you’d only need to change URLs when going to production.
The .env.example file serves as a blueprint for configuring your backend server’s environment variables. Before starting the server, you need to create a .env file by copying `.env.example` and filling in the required values such as PORT, SESSION_SECRET, and Keycloak-related configurations. This setup ensures that the server is properly configured to run. Particularly in test or development environments, the NODE_TLS_REJECT_UNAUTHORIZED=0
setting is included to allow self-signed certificates, which would otherwise be rejected by the Node.js engine. Additionally, other settings in the .env file, like KEYCLOAK_REALM and KEYCLOAK_AUTH_SERVER_URL, are crucial for establishing secure communication between the backend and the Keycloak authentication service.
We then configure the Passport.js strategy called KeycloakStrategy, which is based on an npm package I recently “adopted” because it wasn’t updated for years. I fixed some bugs, converted it to TypeScript, and added a few little helpers as shown above. The returned profile data also contains the id_token that can be used by clients to log out the user. The usage of id_token is straightforward and also directly supported by OIDC (OpenID Connect), upon which PKCE is based.
To take a look into its API, just open the OIDC configuration of the TestRealm:
https://localhost:8443/realms/TestRealm/.well-known/openid-configuration
API
The last part of the BFF’s code belongs to the configuration of RESTful APIs that both the Angular client and the Keycloak server will use. The client will need them to authenticate itself, while Keycloak will be calling them to complete login/logout procedures and exchange tokens.
The three API calls are used for these tasks:
- /auth/keycloak initiates the connection and is always the first one to be called by the Public Client
- /auth/keycloak/callback is used by Keycloak to complete the authentication
- /auth/logout is used by the client to close an existing session
However, the implementations inside these APIs are a bit different from what is usually expected. As the backend is playing the role of the client’s BFF, it must also ensure the client provides its credentials. That’s why at /auth/keycloak, a small frame window will be opened in the client to display Keycloak’s Login Form. The script code for the frame window is provided by the client, by the auth.service.ts of the Angular app. For the time being, simply remember that the successful completion of /auth/keycloak depends on user interaction. This is similar to HTTP redirects that send users to another web page to log in and then return them back to where they’ve started from. /auth/keycloak also generates the needed code_verifier and code_challenge, which play a crucial role in PKCE. This is the reason why in /auth/callback the current session state is saved to preserve code_verifier, because it will be needed in subsequent callbacks to complete the login procedure.
The next call in line, /auth/keycloak/callback, will be used by Keycloak to complete the procedure. We have declared this route to be used by Keycloak at the beginning when we initialized KeycloakStrategy. You can change the callback by modifying the .env setting KEYCLOAK_CALLBACK_URL. Let’s now assume that the user has entered correct credentials and the needed code_challenge and the code_challenge_method have been provided by the BFF. Keycloak would process that data and then call https://localhost:3000/auth/keycloak/callback because it’s compatible with the URL settings of the angular-public-client in TestRealm. Had we given wrong base URLs, the callback would be rejected by Keycloak. Therefore, whenever you encounter problems with “wrong” callbacks, make sure you have provided valid base URLs in your Public Client’s settings. In /auth/keycloak/callback, we see that the BFF server is sending back a small piece of HTML that contains a JS script to close the previously opened frame window. And this is exactly what would happen next in the Angular app. This, of course, improves the UX massively as users don’t need to switch pages as it usually happens when redirects are in use. Additionally, the BFF saves the user data by calling req.logIn(user)
, which persists the login state in the session instance. We have now achieved two things at once:
- a session with Keycloak for the public client (the BFF)
- a session in the Angular client
And nothing sensitive needed to be persisted on the client to achieve that, which is a very big win from the security perspective, because no sensitive data also means no tampered or stolen sensitive data.
The third call, /auth/logout, is for closing active sessions on the BFF. Similarly to /auth/keycloak, the auth.service.ts opens a small frame window that calls /auth/logout on the BFF, which then checks for the existence of the user session. If there is none, the frame window will just be closed. Otherwise, the idToken will be retrieved from the persisted user object to create an OIDC-Logout-URL that will be sent back to the frame window in the Angular app. We put this URL into window.location.href, which forces the frame window to load it immediately. The logout happens instantly, and because we declared the BFF’s /auth/logout/callback to be the value of the parameter post_logout_redirect_uri, Keycloak will ultimately call this URL to finalize the logout.
It’s surely a bit mouthful when encountered the first time, but as time progresses, it all becomes pretty straightforward. The important part is that neither the client nor Keycloak know about each other. Everything goes through the BFF, which takes care of establishing connections, logins/logouts, persisting user data, and tokens.
The Web Application
My web framework of choice is Angular, although I am still missing the good old (bad?) days of BackboneJS, Handlebars, and “how to do views properly”. I don’t know, maybe it’s because I’m getting older and slowly losing the critical view of my own past. Nostalgia kicks in. Anyway, this is an article on modern patterns, so it’s logical we use modern frameworks. Angular is, unlike React, a fully fledged framework and not only a view layer, which makes it best suited for building digital products. In our case, we are building a more or less simple app that shows the current user’s details, products, and transactions. I am, of course, not using real data but relying on the excellent Faker project that can generate almost any kind of structured data. You can see it at work in the mock-data.service.ts in the BFF’s code.
The web app is structured as follows:
The most important part of the whole app is, of course, the auth.service.ts we already talked about. I am not going to copy/paste the whole code because only these two functions are sufficient to understand the “other side” of the communication strategy between BFF and Keycloak.
Both in login() and logout(), we create two frame windows and establish message handlers that wait for “success messages” from the BFF. It is now obvious how the whole thing actually works.
- Open a small window and let users enter their credentials
- Wait for the BFF to complete the task
- Close the window and, depending on the message returned, set the boolean isLoggedIn to true or false
This is the only marker the application sets. There is no session handling, no additional packages to communicate with Keycloak, no extra Passport.js strategies, nothing. Sure, one could also use one of those many “angular keycloak” packages, but I think that SPAs and other public clients are not well-suited for handling anything related to authentication and secure management of sensitive data like passwords, tokens, and other credentials. It also makes those already complex applications even more complex. I understand, it’s very tempting to install just this one package™ to “solve security”, but I consider it a bad solution. Public Clients never deserve any trust and therefore should never get powers beyond their original role.
Angular Configuration
Although rather simple, the web app needs a few things to be configured properly. One of them is the HTTPS access to the API, which we do via proxy.conf.json.
Because we’re using self-signed certificates here and also need to avoid CORS violations, we set “secure” to false for certificates and “changeOrigin” to true. The two APIs we’ll be calling are /api and /auth. Via /api, we access user profile data and the Faker APIs, while /auth is reserved for Keycloak-related calls. The “target” is the BFF’s own URL.
This also leads to more flexible API calls in components and services. For example, this is how we get a product list from the BFF.
By configuring the angular.json file to use serve-options, we enable app to run over HTTPS instead of the default HTTP.
The other important part of app configuration is the dynamic generation of navigation tab elements. As some of them never need an authenticated user, like “Home”, or others should never appear as a tab, like “Access Denied”, the route entries had to be extended. If we look into the routes array, we notice a few changes.
We have new properties: the nav object and isErrorPage. These have been defined under types/routing.
We then use this new information to decide if a route should be shown in the navigation bar.
Conclusion
I hope this article could help you understand how the BFF pattern can be used to create secure environments for public clients while using PKCE with Keycloak. Security is hard, and I doubt it’ll keep getting easier as time progresses. That’s why we have to keep moving and never rely on old solutions that back then might have worked but now have only become yet another stone in our bag of technical debt. Just like it would make little sense to start a complex frontend with pure JavaScript and a bit of BackboneJS, the same applies to secure protocols for authentication and token exchange. We must keep moving and always question old solutions. I still remember the days in 2011 when everyone was talking about OAuth2 and OpenID Connect. Are we still talking now? How many web apps were around back then? How many mobile clients were supported by publicly accessible web apps? Not many, I’m sure. But these days, the situation is vastly different as web apps are becoming more and more “native” and very often practically indistinguishable from normal “native” apps. So, keep on moving and rethink your current security solution. You might surprise yourself.