Keycloak can secure your frontend applications, but its configuration often feels like a black box that only works if you accidentally stumble upon the right settings.

Let’s look at a typical setup. Imagine a React app that needs to access a protected API.

// React App (src/auth.js)
import Keycloak from 'keycloak-js';

const keycloak = new Keycloak({
  url: 'http://localhost:8080/auth',
  realm: 'myrealm',
  clientId: 'my-react-app',
});

export default keycloak;

// In your App.js
import React, { useEffect, useState } from 'react';
import keycloak from './auth';

function App() {
  const [authenticated, setAuthenticated] = useState(false);

  useEffect(() => {
    keycloak.init({ onLoad: 'login-required' }).then(authenticated => {
      setAuthenticated(authenticated);
    });
  }, []);

  if (!authenticated) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>Welcome to your protected app!</h1>
      <button onClick={() => keycloak.logout()}>Logout</button>
    </div>
  );
}

export default App;

This code uses the keycloak-js adapter. When the app loads, keycloak.init() is called. If onLoad is set to login-required, it will automatically redirect the user to Keycloak’s login page. Once authenticated, Keycloak redirects back to your app with an authorization code. The adapter then exchanges this code for tokens (ID token, access token, refresh token). The access token is what you’ll use to call your backend API.

The real magic happens when you need to make an API call. The adapter automatically attaches the access token to outgoing requests to your backend.

// Example API call from React
async function callApi() {
  const response = await fetch('/api/protected-resource', {
    headers: {
      'Authorization': `Bearer ${keycloak.token}`,
    },
  });
  const data = await response.json();
  console.log(data);
}

This Authorization: Bearer <token> header is the standard way to present JWTs (JSON Web Tokens) for authentication. Your backend API, if configured to trust Keycloak, will validate this token.

The mental model for this is fairly straightforward: your frontend app is a public client in Keycloak’s terminology. It doesn’t store secrets. When a user logs in, Keycloak issues tokens directly to the browser. These tokens are then presented to your backend API. The backend API is typically a confidential client (though in some SPAs, it can also be public if it’s purely static assets served from S3/CDN).

Here’s a breakdown of the Keycloak client configuration in the admin console that makes this work:

  • Client ID: my-react-app (This must match what’s in your keycloak-js config.)
  • Client Protocol: openid-connect
  • Root URL: http://localhost:3000 (Your React app’s URL. This is crucial for redirect URIs.)
  • Valid Redirect URIs: http://localhost:3000/* (Allows Keycloak to redirect back to any path on your app after login. The * is important for SPA routing.)
  • Web Origins: + (This is a wildcard meaning "allow all origins". For production, you’d restrict this to http://localhost:3000 and your production domain. This controls CORS headers.)
  • Standard Flow Enabled: Checked (This enables the authorization code flow, which is what onLoad: 'login-required' uses.)
  • Implicit Flow Enabled: Unchecked (Implicit flow is older and less secure, generally not recommended for SPAs.)
  • Direct Access Grants Enabled: Unchecked (This is for direct username/password login, not suitable for browser-based SPAs.)
  • Service Accounts Enabled: Unchecked (This is for backend-to-backend communication, not user authentication.)

The keycloak-js adapter handles token refresh automatically. When the access token expires, it uses the refresh token to get a new access token without requiring the user to log in again. This is managed by the keycloak.init() call and subsequent keycloak.updateToken() operations that happen under the hood.

For an Angular app, the setup is very similar, just using the @react-keycloak/web equivalent, often @react-keycloak/angular. The configuration objects and principles remain the same.

// Angular App (app.module.ts)
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';

function initializeKeycloak(keycloak: KeycloakService) {
  return () =>
    keycloak.init({
      config: {
        url: 'http://localhost:8080/auth',
        realm: 'myrealm',
        clientId: 'my-angular-app',
      },
      initOptions: {
        onLoad: 'login-required',
        checkLoginIframe: false // Important for SPAs to avoid cross-origin issues
      }
    });
}

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule,
    KeycloakAngularModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

// In your component (app.component.ts)
import { Component } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor(private keycloakService: KeycloakService) {}

  logout() {
    this.keycloakService.logout();
  }

  // To access the token:
  getToken() {
    return this.keycloakService.getKeycloakInstance().token;
  }
}

The checkLoginIframe: false option in Angular is particularly important. By default, keycloak-js might use an iframe to check session status without a full redirect. For SPAs running on different origins (like localhost:4000 for your app vs. localhost:8080 for Keycloak), this can lead to cross-origin security errors. Disabling the iframe relies solely on redirects for authentication flow, which is generally safer and more compatible with modern SPA setups.

When you’re troubleshooting, pay close attention to the "Valid Redirect URIs" and "Web Origins" settings in Keycloak. Mismatches here are the most common reason for login flows failing to return to your application, often resulting in a blank page or a "Page Not Found" error from Keycloak’s side. Ensure your frontend’s actual running URL matches one of the registered redirect URIs exactly, and that the web origin allows your frontend’s domain.

The next hurdle you’ll likely face is configuring your backend API to validate these incoming JWTs.

Want structured learning?

Take the full Keycloak course →