logo

Secure React API Access Using Keycloak (OIDC + PKCE)

Introduction

In this article, we explore how a ReactJS frontend application can securely access a protected REST API exposed by a Spring Boot backend, using an access token issued by Keycloak.

The authentication is performed using OpenID Connect (OIDC) with the Authorization Code Flow + PKCE, which is the recommended approach for browser-based applications. The React application acts as the OIDC Relying Party, while Spring Boot functions as an OAuth2 Resource Server, validating incoming JWT access tokens.

Overview of the Architecture

The solution consists of the following components:

  • ReactJS application
    • Acts as an OIDC Relying Party
    • Built using Vite (modern replacement for CRA)
    • Uses the official keycloak-js JavaScript adapter
  • Spring Boot backend
    • Built with Spring Boot 4
    • Uses Spring Web and Spring Security
    • Configured as an OAuth2 Resource Server
  • Keycloak
    • Used as the Identity Provider (IdP)
    • A public OIDC client configured with Authorization Code + PKCE

Vite is a modern frontend build tool and development server that replaces Create React App (CRA) while keeping React unchanged.

More details on JavaScript adaptor of Keycloak are available at the Keycloak documentation over here.

OIDC Flow Used

The ReactJS application authenticates users using:

  • Authorization Code Flow
  • PKCE (Proof Key for Code Exchange)
  • Public client (no client secret)

After successful authentication:

  1. Keycloak issues an access token (JWT)
  2. The React application sends this token as a Bearer token
  3. The Spring Boot backend validates the token and serves the request

Step 1: Configure the Keycloak Client for ReactJS

From the Keycloak Admin Console, create a new client in your realm:

  • Client ID: react-client
  • Client Type: Public
  • Standard Flow: Enabled (Authorization Code)
  • PKCE: Enabled
    • Code Challenge Method: S256

Client URLs

  • Web Origins: http://localhost:5173
  • Root URL: http://localhost:5173
  • Home URL: http://localhost:5173
  • Valid Redirect URIs: http://localhost:5173/*

Step 2: Set Up the ReactJS Application

Create the Project

npm create vite@latest react-keycloak-demo
cd react-keycloak-demo
npm install

Choose:

  • Framework: React
  • Variant: JavaScript or TypeScript (For this article, JavaScript selected)

Install Dependencies

npm install keycloak-js

Create Keycloak Instance

src/keycloak.js

import Keycloak from "keycloak-js";

const keycloak = new Keycloak({
  url: "https://<skycloak_host_name>",
  realm: "myrealm",
  clientId: "react-client"
});

export default keycloak;

Initialize Keycloak Before Rendering React

src/main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import keycloak from "./keycloak";

keycloak
  .init({
    onLoad: "login-required",
    pkceMethod: "S256",
    checkLoginIframe: false
  })
  .then((authenticated) => {
    if (!authenticated) {
      keycloak.login();
    }

    ReactDOM.createRoot(document.getElementById("root")).render(
      <React.StrictMode>
        <App keycloak={keycloak} />
      </React.StrictMode>
    );
  })
  .catch((error) => {
    console.error("Keycloak initialization failed", error);
  });

Secure API Call Helper

src/api/secureApi.js

import keycloak from "../keycloak";

export const callApi = async () => {
  try {
    // Following ensure token is valid (refresh if expiring,)
    await keycloak.updateToken(30);

    const response = await fetch("http://localhost:9090/api/data", {
      method: "GET",
      headers: {
        Authorization: `Bearer ${keycloak.token}`,
        "Content-Type": "application/json"
      }
    });

    if (!response.ok) {
      throw new Error("API call failed");
    }

    return await response.json();
  } catch (err) {
    console.error("API call error", err);

    // Token refresh failed or session expired
    keycloak.login();
  }
};

React UI Component

src/App.jsx

import { callSecureApi } from "./api/secureApi";

function App({ keycloak }) {

  const handleApiCall = async () => {
    const data = await callSecureApi();
    console.log("API Response:", data);
  };

  return (
    <div>
      <h2>
        Welcome {keycloak.tokenParsed?.preferred_username}
      </h2>

      <button onClick={handleApiCall}>
        Call Secure API
      </button>

      <br /><br />

      <button onClick={() => keycloak.logout()}>
        Logout
      </button>
    </div>
  );
}

export default App;

Step 3: Create the Protected Spring Boot Backend

Generate the Project

Go to https://start.spring.io and create a Spring Boot project with:

Dependencies

  • Spring Web
  • Spring Security
  • OAuth2 Resource Server

Security Configuration

Note: CSRF is disabled because this is a stateless REST API secured with Bearer tokens. CORS is handled separately using a dedicated CORS configuration.

SecurityConfig.java

import java.util.List;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
            .cors(Customizer.withDefaults())
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .oauth2ResourceServer(oauth2 ->
            	oauth2.jwt(Customizer.withDefaults())
            );

        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {

        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(
            List.of("http://localhost:5173")
        );
        config.setAllowedMethods(
            List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")
        );
        config.setAllowedHeaders(
            List.of("Authorization", "Content-Type")
        );
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();

        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

URL /api/** is protected

Note: In production, you should also validate the audience (aud claim) to ensure the token
was issued specifically for this API by modifying the above code.

REST Controller

ApiController.java

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class ApiController {

    // Authenticated endpoint β€” token required, no specific role
    @GetMapping("/data")
    public ResponseEntity<Map<String, Object>> secureData() {

        Map<String, Object> response = new HashMap<>();
        response.put("status", "SUCCESS");
        response.put("message", "Success in accessing!");
        response.put("timestamp", Instant.now());

        return ResponseEntity.ok(response);
    }
}

Application Configuration

application.properties

server.port=9090
spring.application.name=reactjsdemo
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://<skycloak_hostname>/realms/<realm_name>

Testing the Integration

  1. Access the React application: (http://localhost:5173)
  2. You are redirected to the Keycloak login page
  3. After successful authentication:
    • The React app loads
    • The username from the token (preferred_username) is displayed
  4. Click β€œCall Secure API”
  5. Observe the successful JSON response in the browser console
Chrome inspect window console tab view

Note: In production environments, HTTPS must be enforced and access tokens should be short-lived.

Summary

In this article, we demonstrated how a ReactJS frontend can securely call a Spring Boot protected REST API using a Keycloak-issued JWT access token.

This approach follows modern security best practices:

  • Authorization Code Flow with PKCE
  • Public OIDC client for SPAs
  • Stateless backend secured with JWT Bearer tokens

We used Skycloak, a fully managed Keycloak hosting solution, for demonstration.

Skycloak provides production-ready managed Keycloak hosting, helping teams avoid the complexity of maintaining and scaling Keycloak themselves.
If you’re new to Skycloak, visit theΒ Skycloak Getting Started GuideΒ to learn more.

Leave a Comment

Β© 2025 All Rights Reserved. Made by Yasser