Seamless Authentication System: Integrating Keycloak with Spring Boot, Thymeleaf, and React Using OAuth2 and JWT

Authentication is a critical aspect of modern web applications, and Keycloak provides a powerful, open-source identity and access management solution. In this article, we’ll explore integrating Keycloak with a Spring Boot backend—using Thymeleaf for server-side rendering—and a React frontend styled with Tailwind CSS 4 and DaisyUI 5 (beta). We’ll use the same credentials across both, leveraging OAuth2 for session-based web authentication and JWT for stateless API security, with endpoint-specific configurations. This approach ensures a unified user experience across traditional web pages and modern single-page applications (SPAs).


Prerequisites

To follow this tutorial, ensure you have:

  • Java 17+: Required for Spring Boot 3.x.
  • Node.js 20+: For Vite and React.
  • Docker: To run Keycloak and PostgreSQL.
  • Dependencies: Spring Boot starters for web, security, oauth2-client, oauth2-resource-server, and thymeleaf.

Setting Up Keycloak with PostgreSQL

We’ll deploy Keycloak and PostgreSQL using Docker Compose for a persistent database setup:

<span># docker-compose.yaml</span>
<span>services</span><span>:</span>
<span>postgres</span><span>:</span>
<span>image</span><span>:</span> <span>postgres:latest</span> <span># Official PostgreSQL image</span>
<span>environment</span><span>:</span>
<span>POSTGRES_USER</span><span>:</span> <span>postgres</span> <span># Database username</span>
<span>POSTGRES_PASSWORD</span><span>:</span> <span>mysecretpassword</span> <span># Database password</span>
<span>POSTGRES_DB</span><span>:</span> <span>keycloak</span> <span># Database name for Keycloak</span>
<span>ports</span><span>:</span>
<span>-</span> <span>"</span><span>5432:5432"</span> <span># Expose PostgreSQL port</span>
<span>volumes</span><span>:</span>
<span>-</span> <span>postgres_data:/var/lib/postgresql/data</span> <span># Persist data</span>
<span>networks</span><span>:</span>
<span>-</span> <span>database</span> <span># Connect to custom network</span>
<span>keycloak</span><span>:</span>
<span>image</span><span>:</span> <span>quay.io/keycloak/keycloak:latest</span> <span># Latest Keycloak image</span>
<span>environment</span><span>:</span>
<span>KEYCLOAK_ADMIN</span><span>:</span> <span>admin</span> <span># Admin username for Keycloak</span>
<span>KEYCLOAK_ADMIN_PASSWORD</span><span>:</span> <span>admin</span> <span># Admin password</span>
<span>KC_HTTP_ENABLED</span><span>:</span> <span>true</span> <span># Enable HTTP access</span>
<span>KC_DB</span><span>:</span> <span>postgres</span> <span># Use PostgreSQL as the database</span>
<span>KC_DB_URL</span><span>:</span> <span>jdbc:postgresql://postgres:5432/keycloak</span> <span># Database URL</span>
<span>KC_DB_USERNAME</span><span>:</span> <span>postgres</span> <span># Database username</span>
<span>KC_DB_PASSWORD</span><span>:</span> <span>mysecretpassword</span> <span># Database password</span>
<span>ports</span><span>:</span>
<span>-</span> <span>"</span><span>8088:8080"</span> <span># Map host port 8088 to container port 8080</span>
<span>command</span><span>:</span>
<span>-</span> <span>start-dev</span> <span># Run in development mode</span>
<span>depends_on</span><span>:</span>
<span>-</span> <span>postgres</span> <span># Ensure PostgreSQL starts first</span>
<span>networks</span><span>:</span>
<span>-</span> <span>database</span> <span># Connect to custom network</span>
<span>volumes</span><span>:</span>
<span>postgres_data</span><span>:</span> <span># Named volume for PostgreSQL data persistence</span>
<span>networks</span><span>:</span>
<span>database</span><span>:</span>
<span>driver</span><span>:</span> <span>bridge</span> <span># Use bridge networking</span>
<span>name</span><span>:</span> <span>database</span> <span># Network name</span>
<span># docker-compose.yaml</span>
<span>services</span><span>:</span>
  <span>postgres</span><span>:</span>
    <span>image</span><span>:</span> <span>postgres:latest</span>                  <span># Official PostgreSQL image</span>
    <span>environment</span><span>:</span>
      <span>POSTGRES_USER</span><span>:</span> <span>postgres</span>              <span># Database username</span>
      <span>POSTGRES_PASSWORD</span><span>:</span> <span>mysecretpassword</span>  <span># Database password</span>
      <span>POSTGRES_DB</span><span>:</span> <span>keycloak</span>                <span># Database name for Keycloak</span>
    <span>ports</span><span>:</span>
      <span>-</span> <span>"</span><span>5432:5432"</span>                        <span># Expose PostgreSQL port</span>
    <span>volumes</span><span>:</span>
      <span>-</span> <span>postgres_data:/var/lib/postgresql/data</span>  <span># Persist data</span>
    <span>networks</span><span>:</span>
      <span>-</span> <span>database</span>                           <span># Connect to custom network</span>

  <span>keycloak</span><span>:</span>
    <span>image</span><span>:</span> <span>quay.io/keycloak/keycloak:latest</span>  <span># Latest Keycloak image</span>
    <span>environment</span><span>:</span>
      <span>KEYCLOAK_ADMIN</span><span>:</span> <span>admin</span>                <span># Admin username for Keycloak</span>
      <span>KEYCLOAK_ADMIN_PASSWORD</span><span>:</span> <span>admin</span>       <span># Admin password</span>
      <span>KC_HTTP_ENABLED</span><span>:</span> <span>true</span>                <span># Enable HTTP access</span>
      <span>KC_DB</span><span>:</span> <span>postgres</span>                      <span># Use PostgreSQL as the database</span>
      <span>KC_DB_URL</span><span>:</span> <span>jdbc:postgresql://postgres:5432/keycloak</span>  <span># Database URL</span>
      <span>KC_DB_USERNAME</span><span>:</span> <span>postgres</span>             <span># Database username</span>
      <span>KC_DB_PASSWORD</span><span>:</span> <span>mysecretpassword</span>     <span># Database password</span>
    <span>ports</span><span>:</span>
      <span>-</span> <span>"</span><span>8088:8080"</span>                        <span># Map host port 8088 to container port 8080</span>
    <span>command</span><span>:</span>
      <span>-</span> <span>start-dev</span>                          <span># Run in development mode</span>
    <span>depends_on</span><span>:</span>
      <span>-</span> <span>postgres</span>                           <span># Ensure PostgreSQL starts first</span>
    <span>networks</span><span>:</span>
      <span>-</span> <span>database</span>                           <span># Connect to custom network</span>

<span>volumes</span><span>:</span>
  <span>postgres_data</span><span>:</span>                          <span># Named volume for PostgreSQL data persistence</span>

<span>networks</span><span>:</span>
  <span>database</span><span>:</span>
    <span>driver</span><span>:</span> <span>bridge</span>                        <span># Use bridge networking</span>
    <span>name</span><span>:</span> <span>database</span>                        <span># Network name</span>
# docker-compose.yaml services: postgres: image: postgres:latest # Official PostgreSQL image environment: POSTGRES_USER: postgres # Database username POSTGRES_PASSWORD: mysecretpassword # Database password POSTGRES_DB: keycloak # Database name for Keycloak ports: - "5432:5432" # Expose PostgreSQL port volumes: - postgres_data:/var/lib/postgresql/data # Persist data networks: - database # Connect to custom network keycloak: image: quay.io/keycloak/keycloak:latest # Latest Keycloak image environment: KEYCLOAK_ADMIN: admin # Admin username for Keycloak KEYCLOAK_ADMIN_PASSWORD: admin # Admin password KC_HTTP_ENABLED: true # Enable HTTP access KC_DB: postgres # Use PostgreSQL as the database KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak # Database URL KC_DB_USERNAME: postgres # Database username KC_DB_PASSWORD: mysecretpassword # Database password ports: - "8088:8080" # Map host port 8088 to container port 8080 command: - start-dev # Run in development mode depends_on: - postgres # Ensure PostgreSQL starts first networks: - database # Connect to custom network volumes: postgres_data: # Named volume for PostgreSQL data persistence networks: database: driver: bridge # Use bridge networking name: database # Network name

Enter fullscreen mode Exit fullscreen mode

Run docker-compose up to start Keycloak at http://localhost:8088 and PostgreSQL at localhost:5432. Log in with admin/admin, create a realm called my-realm, and configure two clients:

  • spring-boot-app: A confidential client for Spring Boot.
  • react-app: A public client for React.

Create a realm role USER and assign it to a test user (e.g., username: testuser, password: password) to enable unified login.

Keycloak Client Configurations

  • spring-boot-app:
    • Client ID: spring-boot-app
    • Client Authentication: On (confidential)
    • Valid Redirect URIs: http://localhost:8081/*
    • Valid Post Logout Redirect URIs: http://localhost:8081/login?logout
    • Web Origins: *

  • react-app:
    • Client ID: react-app
    • Client Authentication: Off (public)
    • Valid Redirect URIs: http://localhost:5173/*
    • Valid Post Logout Redirect URIs: http://localhost:5173/*
    • Web Origins: *


Spring Boot Backend Setup

The backend combines a Thymeleaf web UI with session-based OAuth2 and a REST API secured with JWT, powered by Keycloak.

Dependencies

In pom.xml, include:

<span><dependencies></span>
<span><!-- Core web functionality --></span>
<span><dependency></span>
<span><groupId></span>org.springframework.boot<span></groupId></span>
<span><artifactId></span>spring-boot-starter-web<span></artifactId></span>
<span></dependency></span>
<span><!-- Security framework --></span>
<span><dependency></span>
<span><groupId></span>org.springframework.boot<span></groupId></span>
<span><artifactId></span>spring-boot-starter-security<span></artifactId></span>
<span></dependency></span>
<span><!-- OAuth2 client for session-based login --></span>
<span><dependency></span>
<span><groupId></span>org.springframework.boot<span></groupId></span>
<span><artifactId></span>spring-boot-starter-oauth2-client<span></artifactId></span>
<span></dependency></span>
<span><!-- OAuth2 resource server for JWT validation --></span>
<span><dependency></span>
<span><groupId></span>org.springframework.boot<span></groupId></span>
<span><artifactId></span>spring-boot-starter-oauth2-resource-server<span></artifactId></span>
<span></dependency></span>
<span><!-- Thymeleaf for server-side rendering --></span>
<span><dependency></span>
<span><groupId></span>org.springframework.boot<span></groupId></span>
<span><artifactId></span>spring-boot-starter-thymeleaf<span></artifactId></span>
<span></dependency></span>
<span></dependencies></span>
<span><dependencies></span>
    <span><!-- Core web functionality --></span>
    <span><dependency></span>
        <span><groupId></span>org.springframework.boot<span></groupId></span>
        <span><artifactId></span>spring-boot-starter-web<span></artifactId></span>
    <span></dependency></span>
    <span><!-- Security framework --></span>
    <span><dependency></span>
        <span><groupId></span>org.springframework.boot<span></groupId></span>
        <span><artifactId></span>spring-boot-starter-security<span></artifactId></span>
    <span></dependency></span>
    <span><!-- OAuth2 client for session-based login --></span>
    <span><dependency></span>
        <span><groupId></span>org.springframework.boot<span></groupId></span>
        <span><artifactId></span>spring-boot-starter-oauth2-client<span></artifactId></span>
    <span></dependency></span>
    <span><!-- OAuth2 resource server for JWT validation --></span>
    <span><dependency></span>
        <span><groupId></span>org.springframework.boot<span></groupId></span>
        <span><artifactId></span>spring-boot-starter-oauth2-resource-server<span></artifactId></span>
    <span></dependency></span>
    <span><!-- Thymeleaf for server-side rendering --></span>
    <span><dependency></span>
        <span><groupId></span>org.springframework.boot<span></groupId></span>
        <span><artifactId></span>spring-boot-starter-thymeleaf<span></artifactId></span>
    <span></dependency></span>
<span></dependencies></span>
<dependencies> <!-- Core web functionality --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Security framework --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- OAuth2 client for session-based login --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <!-- OAuth2 resource server for JWT validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <!-- Thymeleaf for server-side rendering --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies>

Enter fullscreen mode Exit fullscreen mode

Application Configuration

Configure Keycloak in application.yml:

<span>spring</span><span>:</span>
<span>security</span><span>:</span>
<span>oauth2</span><span>:</span>
<span>client</span><span>:</span>
<span>registration</span><span>:</span>
<span>keycloak</span><span>:</span>
<span>client-id</span><span>:</span> <span>spring-boot-app</span> <span># Client ID for Spring Boot</span>
<span>client-secret</span><span>:</span> <span>oFKSAvp334RG5oTQwjlmS3LJNSNkvMTN</span> <span># Client secret</span>
<span>scope</span><span>:</span> <span>openid,profile,email</span> <span># Requested scopes</span>
<span>provider</span><span>:</span>
<span>keycloak</span><span>:</span>
<span>issuer-uri</span><span>:</span> <span>http://localhost:8088/realms/my-realm</span> <span># Keycloak realm URL</span>
<span>server</span><span>:</span>
<span>port</span><span>:</span> <span>8081</span> <span># Spring Boot runs on port 8081</span>
<span>spring</span><span>:</span>
  <span>security</span><span>:</span>
    <span>oauth2</span><span>:</span>
      <span>client</span><span>:</span>
        <span>registration</span><span>:</span>
          <span>keycloak</span><span>:</span>
            <span>client-id</span><span>:</span> <span>spring-boot-app</span>          <span># Client ID for Spring Boot</span>
            <span>client-secret</span><span>:</span> <span>oFKSAvp334RG5oTQwjlmS3LJNSNkvMTN</span>  <span># Client secret</span>
            <span>scope</span><span>:</span> <span>openid,profile,email</span>         <span># Requested scopes</span>
        <span>provider</span><span>:</span>
          <span>keycloak</span><span>:</span>
            <span>issuer-uri</span><span>:</span> <span>http://localhost:8088/realms/my-realm</span>  <span># Keycloak realm URL</span>
<span>server</span><span>:</span>
  <span>port</span><span>:</span> <span>8081</span>                                   <span># Spring Boot runs on port 8081</span>
spring: security: oauth2: client: registration: keycloak: client-id: spring-boot-app # Client ID for Spring Boot client-secret: oFKSAvp334RG5oTQwjlmS3LJNSNkvMTN # Client secret scope: openid,profile,email # Requested scopes provider: keycloak: issuer-uri: http://localhost:8088/realms/my-realm # Keycloak realm URL server: port: 8081 # Spring Boot runs on port 8081

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • spring.security.oauth2.client: Configures the OAuth2 client for session-based login.
  • client-id and client-secret: Credentials for spring-boot-app.
  • issuer-uri: Keycloak’s realm endpoint for OAuth2 discovery.
  • server.port: Runs the app on 8081 to avoid conflicts.

Security Configuration

The updated SecurityConfig class defines two filter chains with explicit endpoint matching:

<span>package</span> <span>com.mahmud.backend.config</span><span>;</span>
<span>import</span> <span>org.springframework.context.annotation.Bean</span><span>;</span>
<span>import</span> <span>org.springframework.context.annotation.Configuration</span><span>;</span>
<span>import</span> <span>org.springframework.security.config.annotation.web.builders.HttpSecurity</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.client.registration.ClientRegistrationRepository</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.jwt.JwtDecoder</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.jwt.NimbusJwtDecoder</span><span>;</span>
<span>import</span> <span>org.springframework.security.web.SecurityFilterChain</span><span>;</span>
<span>import</span> <span>org.springframework.security.web.authentication.logout.LogoutSuccessHandler</span><span>;</span>
<span>import</span> <span>org.springframework.web.cors.CorsConfiguration</span><span>;</span>
<span>import</span> <span>java.util.Arrays</span><span>;</span>
<span>@Configuration</span>
<span>public</span> <span>class</span> <span>SecurityConfig</span> <span>{</span>
<span>private</span> <span>final</span> <span>ClientRegistrationRepository</span> <span>clientRegistrationRepository</span><span>;</span> <span>// Repository for OAuth2 client registrations</span>
<span>public</span> <span>SecurityConfig</span><span>(</span><span>ClientRegistrationRepository</span> <span>clientRegistrationRepository</span><span>)</span> <span>{</span>
<span>this</span><span>.</span><span>clientRegistrationRepository</span> <span>=</span> <span>clientRegistrationRepository</span><span>;</span>
<span>}</span>
<span>// Filter chain for session-based web UI</span>
<span>@Bean</span>
<span>public</span> <span>SecurityFilterChain</span> <span>securityFilterChain</span><span>(</span><span>HttpSecurity</span> <span>http</span><span>)</span> <span>throws</span> <span>Exception</span> <span>{</span>
<span>http</span>
<span>.</span><span>securityMatcher</span><span>(</span>
<span>"/login"</span><span>,</span> <span>// Custom login page</span>
<span>"/login/oauth2/*/**"</span><span>,</span> <span>// OAuth2 callback endpoints</span>
<span>"/oauth2/*/**"</span><span>,</span> <span>// Additional OAuth2 paths</span>
<span>"/home"</span><span>,</span> <span>// Protected home page</span>
<span>"/logout"</span><span>,</span> <span>// Logout endpoint</span>
<span>"/public"</span> <span>// Public page</span>
<span>)</span>
<span>.</span><span>cors</span><span>(</span><span>cors</span> <span>-></span> <span>cors</span><span>.</span><span>configurationSource</span><span>(</span><span>request</span> <span>-></span> <span>{</span> <span>// CORS configuration</span>
<span>CorsConfiguration</span> <span>config</span> <span>=</span> <span>new</span> <span>CorsConfiguration</span><span>();</span>
<span>config</span><span>.</span><span>setAllowedOrigins</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"http://localhost:5173"</span><span>));</span> <span>// Allow React origin</span>
<span>config</span><span>.</span><span>setAllowedMethods</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"GET"</span><span>,</span> <span>"POST"</span><span>));</span> <span>// Allowed HTTP methods</span>
<span>config</span><span>.</span><span>setAllowedHeaders</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"Authorization"</span><span>));</span> <span>// Allow Authorization header</span>
<span>config</span><span>.</span><span>setAllowCredentials</span><span>(</span><span>false</span><span>);</span> <span>// No credentials for simplicity</span>
<span>return</span> <span>config</span><span>;</span>
<span>}))</span>
<span>.</span><span>authorizeHttpRequests</span><span>(</span><span>auth</span> <span>-></span> <span>auth</span>
<span>.</span><span>requestMatchers</span><span>(</span><span>"/login"</span><span>,</span> <span>"/public"</span><span>).</span><span>permitAll</span><span>()</span> <span>// Public access to these endpoints</span>
<span>.</span><span>anyRequest</span><span>().</span><span>authenticated</span><span>()</span> <span>// All other endpoints require auth</span>
<span>)</span>
<span>.</span><span>oauth2Login</span><span>(</span><span>oauth2</span> <span>-></span> <span>oauth2</span>
<span>.</span><span>loginPage</span><span>(</span><span>"/login"</span><span>)</span> <span>// Custom login page</span>
<span>.</span><span>defaultSuccessUrl</span><span>(</span><span>"/home"</span><span>,</span> <span>true</span><span>)</span> <span>// Redirect after login</span>
<span>)</span>
<span>.</span><span>logout</span><span>(</span><span>logout</span> <span>-></span> <span>logout</span>
<span>.</span><span>logoutUrl</span><span>(</span><span>"/logout"</span><span>)</span> <span>// Logout endpoint</span>
<span>.</span><span>logoutSuccessHandler</span><span>(</span><span>oidcLogoutSuccessHandler</span><span>())</span> <span>// Handle logout with Keycloak</span>
<span>.</span><span>invalidateHttpSession</span><span>(</span><span>true</span><span>)</span> <span>// Clear session</span>
<span>.</span><span>clearAuthentication</span><span>(</span><span>true</span><span>)</span> <span>// Clear auth context</span>
<span>);</span>
<span>return</span> <span>http</span><span>.</span><span>build</span><span>();</span>
<span>}</span>
<span>// Filter chain for JWT-based API</span>
<span>@Bean</span>
<span>public</span> <span>SecurityFilterChain</span> <span>apiSecurityFilterChain</span><span>(</span><span>HttpSecurity</span> <span>http</span><span>)</span> <span>throws</span> <span>Exception</span> <span>{</span>
<span>http</span>
<span>.</span><span>securityMatcher</span><span>(</span><span>"/secured"</span><span>)</span> <span>// Apply to /secured endpoint only</span>
<span>.</span><span>cors</span><span>(</span><span>cors</span> <span>-></span> <span>cors</span><span>.</span><span>configurationSource</span><span>(</span><span>request</span> <span>-></span> <span>{</span> <span>// CORS configuration</span>
<span>CorsConfiguration</span> <span>config</span> <span>=</span> <span>new</span> <span>CorsConfiguration</span><span>();</span>
<span>config</span><span>.</span><span>setAllowedOrigins</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"http://localhost:5173"</span><span>));</span> <span>// Allow React origin</span>
<span>config</span><span>.</span><span>setAllowedMethods</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"GET"</span><span>,</span> <span>"POST"</span><span>));</span> <span>// Allowed methods</span>
<span>config</span><span>.</span><span>setAllowedHeaders</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"Authorization"</span><span>));</span> <span>// Allow Authorization header</span>
<span>return</span> <span>config</span><span>;</span>
<span>}))</span>
<span>.</span><span>authorizeHttpRequests</span><span>(</span><span>auth</span> <span>-></span> <span>auth</span>
<span>.</span><span>anyRequest</span><span>().</span><span>authenticated</span><span>()</span> <span>// Require authentication</span>
<span>)</span>
<span>.</span><span>oauth2ResourceServer</span><span>(</span><span>oauth2</span> <span>-></span> <span>oauth2</span>
<span>.</span><span>jwt</span><span>(</span><span>jwt</span> <span>-></span> <span>jwt</span><span>.</span><span>decoder</span><span>(</span><span>jwtDecoder</span><span>()))</span> <span>// Validate JWT with custom decoder</span>
<span>);</span>
<span>return</span> <span>http</span><span>.</span><span>build</span><span>();</span>
<span>}</span>
<span>// Custom logout handler for OAuth2 logout with Keycloak</span>
<span>private</span> <span>LogoutSuccessHandler</span> <span>oidcLogoutSuccessHandler</span><span>()</span> <span>{</span>
<span>OidcClientInitiatedLogoutSuccessHandler</span> <span>logoutSuccessHandler</span> <span>=</span>
<span>new</span> <span>OidcClientInitiatedLogoutSuccessHandler</span><span>(</span><span>clientRegistrationRepository</span><span>);</span>
<span>logoutSuccessHandler</span><span>.</span><span>setPostLogoutRedirectUri</span><span>(</span><span>"http://localhost:8081/login?logout"</span><span>);</span> <span>// Redirect after logout</span>
<span>return</span> <span>logoutSuccessHandler</span><span>;</span>
<span>}</span>
<span>// JWT decoder to validate tokens from Keycloak</span>
<span>@Bean</span>
<span>public</span> <span>JwtDecoder</span> <span>jwtDecoder</span><span>()</span> <span>{</span>
<span>return</span> <span>NimbusJwtDecoder</span><span>.</span><span>withJwkSetUri</span><span>(</span><span>"http://localhost:8088/realms/my-realm/protocol/openid-connect/certs"</span><span>).</span><span>build</span><span>();</span>
<span>}</span>
<span>}</span>
<span>package</span> <span>com.mahmud.backend.config</span><span>;</span>

<span>import</span> <span>org.springframework.context.annotation.Bean</span><span>;</span>
<span>import</span> <span>org.springframework.context.annotation.Configuration</span><span>;</span>
<span>import</span> <span>org.springframework.security.config.annotation.web.builders.HttpSecurity</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.client.registration.ClientRegistrationRepository</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.jwt.JwtDecoder</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.jwt.NimbusJwtDecoder</span><span>;</span>
<span>import</span> <span>org.springframework.security.web.SecurityFilterChain</span><span>;</span>
<span>import</span> <span>org.springframework.security.web.authentication.logout.LogoutSuccessHandler</span><span>;</span>
<span>import</span> <span>org.springframework.web.cors.CorsConfiguration</span><span>;</span>

<span>import</span> <span>java.util.Arrays</span><span>;</span>

<span>@Configuration</span>
<span>public</span> <span>class</span> <span>SecurityConfig</span> <span>{</span>

    <span>private</span> <span>final</span> <span>ClientRegistrationRepository</span> <span>clientRegistrationRepository</span><span>;</span> <span>// Repository for OAuth2 client registrations</span>

    <span>public</span> <span>SecurityConfig</span><span>(</span><span>ClientRegistrationRepository</span> <span>clientRegistrationRepository</span><span>)</span> <span>{</span>
        <span>this</span><span>.</span><span>clientRegistrationRepository</span> <span>=</span> <span>clientRegistrationRepository</span><span>;</span>
    <span>}</span>

    <span>// Filter chain for session-based web UI</span>
    <span>@Bean</span>
    <span>public</span> <span>SecurityFilterChain</span> <span>securityFilterChain</span><span>(</span><span>HttpSecurity</span> <span>http</span><span>)</span> <span>throws</span> <span>Exception</span> <span>{</span>
        <span>http</span>
            <span>.</span><span>securityMatcher</span><span>(</span>
                <span>"/login"</span><span>,</span>                     <span>// Custom login page</span>
                <span>"/login/oauth2/*/**"</span><span>,</span>         <span>// OAuth2 callback endpoints</span>
                <span>"/oauth2/*/**"</span><span>,</span>               <span>// Additional OAuth2 paths</span>
                <span>"/home"</span><span>,</span>                      <span>// Protected home page</span>
                <span>"/logout"</span><span>,</span>                    <span>// Logout endpoint</span>
                <span>"/public"</span>                     <span>// Public page</span>
            <span>)</span>
            <span>.</span><span>cors</span><span>(</span><span>cors</span> <span>-></span> <span>cors</span><span>.</span><span>configurationSource</span><span>(</span><span>request</span> <span>-></span> <span>{</span>   <span>// CORS configuration</span>
                <span>CorsConfiguration</span> <span>config</span> <span>=</span> <span>new</span> <span>CorsConfiguration</span><span>();</span>
                <span>config</span><span>.</span><span>setAllowedOrigins</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"http://localhost:5173"</span><span>));</span> <span>// Allow React origin</span>
                <span>config</span><span>.</span><span>setAllowedMethods</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"GET"</span><span>,</span> <span>"POST"</span><span>));</span>          <span>// Allowed HTTP methods</span>
                <span>config</span><span>.</span><span>setAllowedHeaders</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"Authorization"</span><span>));</span>        <span>// Allow Authorization header</span>
                <span>config</span><span>.</span><span>setAllowCredentials</span><span>(</span><span>false</span><span>);</span>                               <span>// No credentials for simplicity</span>
                <span>return</span> <span>config</span><span>;</span>
            <span>}))</span>
            <span>.</span><span>authorizeHttpRequests</span><span>(</span><span>auth</span> <span>-></span> <span>auth</span>
                <span>.</span><span>requestMatchers</span><span>(</span><span>"/login"</span><span>,</span> <span>"/public"</span><span>).</span><span>permitAll</span><span>()</span>    <span>// Public access to these endpoints</span>
                <span>.</span><span>anyRequest</span><span>().</span><span>authenticated</span><span>()</span>                        <span>// All other endpoints require auth</span>
            <span>)</span>
            <span>.</span><span>oauth2Login</span><span>(</span><span>oauth2</span> <span>-></span> <span>oauth2</span>
                <span>.</span><span>loginPage</span><span>(</span><span>"/login"</span><span>)</span>                                 <span>// Custom login page</span>
                <span>.</span><span>defaultSuccessUrl</span><span>(</span><span>"/home"</span><span>,</span> <span>true</span><span>)</span>                    <span>// Redirect after login</span>
            <span>)</span>
            <span>.</span><span>logout</span><span>(</span><span>logout</span> <span>-></span> <span>logout</span>
                <span>.</span><span>logoutUrl</span><span>(</span><span>"/logout"</span><span>)</span>                                <span>// Logout endpoint</span>
                <span>.</span><span>logoutSuccessHandler</span><span>(</span><span>oidcLogoutSuccessHandler</span><span>())</span>    <span>// Handle logout with Keycloak</span>
                <span>.</span><span>invalidateHttpSession</span><span>(</span><span>true</span><span>)</span>                         <span>// Clear session</span>
                <span>.</span><span>clearAuthentication</span><span>(</span><span>true</span><span>)</span>                           <span>// Clear auth context</span>
            <span>);</span>
        <span>return</span> <span>http</span><span>.</span><span>build</span><span>();</span>
    <span>}</span>

    <span>// Filter chain for JWT-based API</span>
    <span>@Bean</span>
    <span>public</span> <span>SecurityFilterChain</span> <span>apiSecurityFilterChain</span><span>(</span><span>HttpSecurity</span> <span>http</span><span>)</span> <span>throws</span> <span>Exception</span> <span>{</span>
        <span>http</span>
            <span>.</span><span>securityMatcher</span><span>(</span><span>"/secured"</span><span>)</span>                             <span>// Apply to /secured endpoint only</span>
            <span>.</span><span>cors</span><span>(</span><span>cors</span> <span>-></span> <span>cors</span><span>.</span><span>configurationSource</span><span>(</span><span>request</span> <span>-></span> <span>{</span>      <span>// CORS configuration</span>
                <span>CorsConfiguration</span> <span>config</span> <span>=</span> <span>new</span> <span>CorsConfiguration</span><span>();</span>
                <span>config</span><span>.</span><span>setAllowedOrigins</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"http://localhost:5173"</span><span>));</span> <span>// Allow React origin</span>
                <span>config</span><span>.</span><span>setAllowedMethods</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"GET"</span><span>,</span> <span>"POST"</span><span>));</span>          <span>// Allowed methods</span>
                <span>config</span><span>.</span><span>setAllowedHeaders</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"Authorization"</span><span>));</span>        <span>// Allow Authorization header</span>
                <span>return</span> <span>config</span><span>;</span>
            <span>}))</span>
            <span>.</span><span>authorizeHttpRequests</span><span>(</span><span>auth</span> <span>-></span> <span>auth</span>
                <span>.</span><span>anyRequest</span><span>().</span><span>authenticated</span><span>()</span>                        <span>// Require authentication</span>
            <span>)</span>
            <span>.</span><span>oauth2ResourceServer</span><span>(</span><span>oauth2</span> <span>-></span> <span>oauth2</span>
                <span>.</span><span>jwt</span><span>(</span><span>jwt</span> <span>-></span> <span>jwt</span><span>.</span><span>decoder</span><span>(</span><span>jwtDecoder</span><span>()))</span>               <span>// Validate JWT with custom decoder</span>
            <span>);</span>
        <span>return</span> <span>http</span><span>.</span><span>build</span><span>();</span>
    <span>}</span>

    <span>// Custom logout handler for OAuth2 logout with Keycloak</span>
    <span>private</span> <span>LogoutSuccessHandler</span> <span>oidcLogoutSuccessHandler</span><span>()</span> <span>{</span>
        <span>OidcClientInitiatedLogoutSuccessHandler</span> <span>logoutSuccessHandler</span> <span>=</span>
            <span>new</span> <span>OidcClientInitiatedLogoutSuccessHandler</span><span>(</span><span>clientRegistrationRepository</span><span>);</span>
        <span>logoutSuccessHandler</span><span>.</span><span>setPostLogoutRedirectUri</span><span>(</span><span>"http://localhost:8081/login?logout"</span><span>);</span> <span>// Redirect after logout</span>
        <span>return</span> <span>logoutSuccessHandler</span><span>;</span>
    <span>}</span>

    <span>// JWT decoder to validate tokens from Keycloak</span>
    <span>@Bean</span>
    <span>public</span> <span>JwtDecoder</span> <span>jwtDecoder</span><span>()</span> <span>{</span>
        <span>return</span> <span>NimbusJwtDecoder</span><span>.</span><span>withJwkSetUri</span><span>(</span><span>"http://localhost:8088/realms/my-realm/protocol/openid-connect/certs"</span><span>).</span><span>build</span><span>();</span>
    <span>}</span>
<span>}</span>
package com.mahmud.backend.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.web.cors.CorsConfiguration; import java.util.Arrays; @Configuration public class SecurityConfig { private final ClientRegistrationRepository clientRegistrationRepository; // Repository for OAuth2 client registrations public SecurityConfig(ClientRegistrationRepository clientRegistrationRepository) { this.clientRegistrationRepository = clientRegistrationRepository; } // Filter chain for session-based web UI @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher( "/login", // Custom login page "/login/oauth2/*/**", // OAuth2 callback endpoints "/oauth2/*/**", // Additional OAuth2 paths "/home", // Protected home page "/logout", // Logout endpoint "/public" // Public page ) .cors(cors -> cors.configurationSource(request -> { // CORS configuration CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(Arrays.asList("http://localhost:5173")); // Allow React origin config.setAllowedMethods(Arrays.asList("GET", "POST")); // Allowed HTTP methods config.setAllowedHeaders(Arrays.asList("Authorization")); // Allow Authorization header config.setAllowCredentials(false); // No credentials for simplicity return config; })) .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/public").permitAll() // Public access to these endpoints .anyRequest().authenticated() // All other endpoints require auth ) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") // Custom login page .defaultSuccessUrl("/home", true) // Redirect after login ) .logout(logout -> logout .logoutUrl("/logout") // Logout endpoint .logoutSuccessHandler(oidcLogoutSuccessHandler()) // Handle logout with Keycloak .invalidateHttpSession(true) // Clear session .clearAuthentication(true) // Clear auth context ); return http.build(); } // Filter chain for JWT-based API @Bean public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { http .securityMatcher("/secured") // Apply to /secured endpoint only .cors(cors -> cors.configurationSource(request -> { // CORS configuration CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(Arrays.asList("http://localhost:5173")); // Allow React origin config.setAllowedMethods(Arrays.asList("GET", "POST")); // Allowed methods config.setAllowedHeaders(Arrays.asList("Authorization")); // Allow Authorization header return config; })) .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() // Require authentication ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt.decoder(jwtDecoder())) // Validate JWT with custom decoder ); return http.build(); } // Custom logout handler for OAuth2 logout with Keycloak private LogoutSuccessHandler oidcLogoutSuccessHandler() { OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository); logoutSuccessHandler.setPostLogoutRedirectUri("http://localhost:8081/login?logout"); // Redirect after logout return logoutSuccessHandler; } // JWT decoder to validate tokens from Keycloak @Bean public JwtDecoder jwtDecoder() { return NimbusJwtDecoder.withJwkSetUri("http://localhost:8088/realms/my-realm/protocol/openid-connect/certs").build(); } }

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • webSecurityFilterChain: Secures Thymeleaf endpoints with oauth2Login. Updated securityMatcher includes OAuth2 callback paths (/login/oauth2/*/**, /oauth2/*/**) for proper redirect handling.
  • apiSecurityFilterChain: Secures /secured with oauth2ResourceServer for JWT validation.
  • cors: Allows React at http://localhost:5173 to access endpoints, supporting Authorization headers.
  • jwtDecoder: Validates JWTs using Keycloak’s JWKS endpoint.
  • oidcLogoutSuccessHandler: Manages logout with Keycloak integration.

Web Controller (Thymeleaf)

<span>package</span> <span>com.mahmud.backend.controller</span><span>;</span>
<span>import</span> <span>org.springframework.security.core.annotation.AuthenticationPrincipal</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.core.oidc.user.OidcUser</span><span>;</span>
<span>import</span> <span>org.springframework.stereotype.Controller</span><span>;</span>
<span>import</span> <span>org.springframework.ui.Model</span><span>;</span>
<span>import</span> <span>org.springframework.web.bind.annotation.GetMapping</span><span>;</span>
<span>@Controller</span>
<span>public</span> <span>class</span> <span>WebController</span> <span>{</span>
<span>@GetMapping</span><span>(</span><span>"/login"</span><span>)</span>
<span>public</span> <span>String</span> <span>login</span><span>()</span> <span>{</span>
<span>return</span> <span>"login"</span><span>;</span> <span>// Returns the login Thymeleaf template</span>
<span>}</span>
<span>@GetMapping</span><span>(</span><span>"/public"</span><span>)</span>
<span>public</span> <span>String</span> <span>publicPage</span><span>(</span><span>Model</span> <span>model</span><span>)</span> <span>{</span>
<span>model</span><span>.</span><span>addAttribute</span><span>(</span><span>"message"</span><span>,</span> <span>"This is a public page!"</span><span>);</span> <span>// Adds message to the model</span>
<span>return</span> <span>"public"</span><span>;</span> <span>// Returns the public Thymeleaf template</span>
<span>}</span>
<span>@GetMapping</span><span>(</span><span>"/home"</span><span>)</span>
<span>public</span> <span>String</span> <span>home</span><span>(</span><span>Model</span> <span>model</span><span>,</span> <span>@AuthenticationPrincipal</span> <span>OidcUser</span> <span>user</span><span>)</span> <span>{</span>
<span>model</span><span>.</span><span>addAttribute</span><span>(</span><span>"username"</span><span>,</span> <span>user</span><span>.</span><span>getPreferredUsername</span><span>());</span> <span>// Adds username from OIDC user</span>
<span>return</span> <span>"home"</span><span>;</span> <span>// Returns the home Thymeleaf template</span>
<span>}</span>
<span>}</span>
<span>package</span> <span>com.mahmud.backend.controller</span><span>;</span>

<span>import</span> <span>org.springframework.security.core.annotation.AuthenticationPrincipal</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.core.oidc.user.OidcUser</span><span>;</span>
<span>import</span> <span>org.springframework.stereotype.Controller</span><span>;</span>
<span>import</span> <span>org.springframework.ui.Model</span><span>;</span>
<span>import</span> <span>org.springframework.web.bind.annotation.GetMapping</span><span>;</span>

<span>@Controller</span>
<span>public</span> <span>class</span> <span>WebController</span> <span>{</span>

    <span>@GetMapping</span><span>(</span><span>"/login"</span><span>)</span>
    <span>public</span> <span>String</span> <span>login</span><span>()</span> <span>{</span>
        <span>return</span> <span>"login"</span><span>;</span>  <span>// Returns the login Thymeleaf template</span>
    <span>}</span>

    <span>@GetMapping</span><span>(</span><span>"/public"</span><span>)</span>
    <span>public</span> <span>String</span> <span>publicPage</span><span>(</span><span>Model</span> <span>model</span><span>)</span> <span>{</span>
        <span>model</span><span>.</span><span>addAttribute</span><span>(</span><span>"message"</span><span>,</span> <span>"This is a public page!"</span><span>);</span> <span>// Adds message to the model</span>
        <span>return</span> <span>"public"</span><span>;</span>  <span>// Returns the public Thymeleaf template</span>
    <span>}</span>

    <span>@GetMapping</span><span>(</span><span>"/home"</span><span>)</span>
    <span>public</span> <span>String</span> <span>home</span><span>(</span><span>Model</span> <span>model</span><span>,</span> <span>@AuthenticationPrincipal</span> <span>OidcUser</span> <span>user</span><span>)</span> <span>{</span>
        <span>model</span><span>.</span><span>addAttribute</span><span>(</span><span>"username"</span><span>,</span> <span>user</span><span>.</span><span>getPreferredUsername</span><span>());</span> <span>// Adds username from OIDC user</span>
        <span>return</span> <span>"home"</span><span>;</span>  <span>// Returns the home Thymeleaf template</span>
    <span>}</span>
<span>}</span>
package com.mahmud.backend.controller; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class WebController { @GetMapping("/login") public String login() { return "login"; // Returns the login Thymeleaf template } @GetMapping("/public") public String publicPage(Model model) { model.addAttribute("message", "This is a public page!"); // Adds message to the model return "public"; // Returns the public Thymeleaf template } @GetMapping("/home") public String home(Model model, @AuthenticationPrincipal OidcUser user) { model.addAttribute("username", user.getPreferredUsername()); // Adds username from OIDC user return "home"; // Returns the home Thymeleaf template } }

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • login: Serves a login page linking to Keycloak’s OAuth2 flow.
  • publicPage: A publicly accessible page with a message.
  • home: A protected page displaying the authenticated user’s username.

API Controller (JWT)

<span>package</span> <span>com.mahmud.backend.controller</span><span>;</span>
<span>import</span> <span>org.springframework.security.core.annotation.AuthenticationPrincipal</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.jwt.Jwt</span><span>;</span>
<span>import</span> <span>org.springframework.web.bind.annotation.GetMapping</span><span>;</span>
<span>import</span> <span>org.springframework.web.bind.annotation.RestController</span><span>;</span>
<span>import</span> <span>java.util.HashMap</span><span>;</span>
<span>import</span> <span>java.util.Map</span><span>;</span>
<span>@RestController</span>
<span>public</span> <span>class</span> <span>DemoController</span> <span>{</span>
<span>@GetMapping</span><span>(</span><span>"/secured"</span><span>)</span>
<span>public</span> <span>Map</span><span><</span><span>String</span><span>,</span> <span>String</span><span>></span> <span>securedMethod</span><span>(</span><span>@AuthenticationPrincipal</span> <span>Jwt</span> <span>jwt</span><span>)</span> <span>{</span>
<span>Map</span><span><</span><span>String</span><span>,</span> <span>String</span><span>></span> <span>map</span> <span>=</span> <span>new</span> <span>HashMap</span><span><>();</span>
<span>map</span><span>.</span><span>put</span><span>(</span><span>"message"</span><span>,</span> <span>"this is a secure message"</span><span>);</span> <span>// Response message</span>
<span>map</span><span>.</span><span>put</span><span>(</span><span>"username"</span><span>,</span> <span>jwt</span><span>.</span><span>getClaimAsString</span><span>(</span><span>"preferred_username"</span><span>));</span> <span>// Username from JWT</span>
<span>return</span> <span>map</span><span>;</span> <span>// Returns JSON response</span>
<span>}</span>
<span>}</span>
<span>package</span> <span>com.mahmud.backend.controller</span><span>;</span>

<span>import</span> <span>org.springframework.security.core.annotation.AuthenticationPrincipal</span><span>;</span>
<span>import</span> <span>org.springframework.security.oauth2.jwt.Jwt</span><span>;</span>
<span>import</span> <span>org.springframework.web.bind.annotation.GetMapping</span><span>;</span>
<span>import</span> <span>org.springframework.web.bind.annotation.RestController</span><span>;</span>

<span>import</span> <span>java.util.HashMap</span><span>;</span>
<span>import</span> <span>java.util.Map</span><span>;</span>

<span>@RestController</span>
<span>public</span> <span>class</span> <span>DemoController</span> <span>{</span>

    <span>@GetMapping</span><span>(</span><span>"/secured"</span><span>)</span>
    <span>public</span> <span>Map</span><span><</span><span>String</span><span>,</span> <span>String</span><span>></span> <span>securedMethod</span><span>(</span><span>@AuthenticationPrincipal</span> <span>Jwt</span> <span>jwt</span><span>)</span> <span>{</span>
        <span>Map</span><span><</span><span>String</span><span>,</span> <span>String</span><span>></span> <span>map</span> <span>=</span> <span>new</span> <span>HashMap</span><span><>();</span>
        <span>map</span><span>.</span><span>put</span><span>(</span><span>"message"</span><span>,</span> <span>"this is a secure message"</span><span>);</span>              <span>// Response message</span>
        <span>map</span><span>.</span><span>put</span><span>(</span><span>"username"</span><span>,</span> <span>jwt</span><span>.</span><span>getClaimAsString</span><span>(</span><span>"preferred_username"</span><span>));</span> <span>// Username from JWT</span>
        <span>return</span> <span>map</span><span>;</span>                                                  <span>// Returns JSON response</span>
    <span>}</span>
<span>}</span>
package com.mahmud.backend.controller; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; @RestController public class DemoController { @GetMapping("/secured") public Map<String, String> securedMethod(@AuthenticationPrincipal Jwt jwt) { Map<String, String> map = new HashMap<>(); map.put("message", "this is a secure message"); // Response message map.put("username", jwt.getClaimAsString("preferred_username")); // Username from JWT return map; // Returns JSON response } }

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • securedMethod: A REST endpoint secured with JWT, returning a message and username from the token.

Thymeleaf Templates

  • login.html:
<span><!DOCTYPE html></span>
<span><html</span> <span>xmlns:th=</span><span>"http://www.thymeleaf.org"</span><span>></span>
<span><head><title></span>Login<span></title></head></span>
<span><body></span>
<span><h1></span>Login<span></h1></span>
<span><!-- Link to initiate Keycloak OAuth2 login --></span>
<span><a</span> <span>href=</span><span>"/oauth2/authorization/keycloak"</span><span>></span>Login with Keycloak<span></a></span>
<span><!-- Display logout message if present --></span>
<span><p</span> <span>th:if=</span><span>"${param.logout}"</span><span>></span>You have been logged out.<span></p></span>
<span></body></span>
<span></html></span>
<span><!DOCTYPE html></span>
<span><html</span> <span>xmlns:th=</span><span>"http://www.thymeleaf.org"</span><span>></span>
<span><head><title></span>Login<span></title></head></span>
<span><body></span>
    <span><h1></span>Login<span></h1></span>
    <span><!-- Link to initiate Keycloak OAuth2 login --></span>
    <span><a</span> <span>href=</span><span>"/oauth2/authorization/keycloak"</span><span>></span>Login with Keycloak<span></a></span>
    <span><!-- Display logout message if present --></span>
    <span><p</span> <span>th:if=</span><span>"${param.logout}"</span><span>></span>You have been logged out.<span></p></span>
<span></body></span>
<span></html></span>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head><title>Login</title></head> <body> <h1>Login</h1> <!-- Link to initiate Keycloak OAuth2 login --> <a href="/oauth2/authorization/keycloak">Login with Keycloak</a> <!-- Display logout message if present --> <p th:if="${param.logout}">You have been logged out.</p> </body> </html>

Enter fullscreen mode Exit fullscreen mode

  • public.html:
<span><!DOCTYPE html></span>
<span><html</span> <span>xmlns:th=</span><span>"http://www.thymeleaf.org"</span><span>></span>
<span><head><title></span>Public Page<span></title></head></span>
<span><body></span>
<span><h1></span>Public Page<span></h1></span>
<span><!-- Display message from the model --></span>
<span><p</span> <span>th:text=</span><span>"${message}"</span><span>></p></span>
<span><!-- Link to login page --></span>
<span><a</span> <span>href=</span><span>"/login"</span><span>></span>Go to Login<span></a></span>
<span></body></span>
<span></html></span>
<span><!DOCTYPE html></span>
<span><html</span> <span>xmlns:th=</span><span>"http://www.thymeleaf.org"</span><span>></span>
<span><head><title></span>Public Page<span></title></head></span>
<span><body></span>
    <span><h1></span>Public Page<span></h1></span>
    <span><!-- Display message from the model --></span>
    <span><p</span> <span>th:text=</span><span>"${message}"</span><span>></p></span>
    <span><!-- Link to login page --></span>
    <span><a</span> <span>href=</span><span>"/login"</span><span>></span>Go to Login<span></a></span>
<span></body></span>
<span></html></span>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head><title>Public Page</title></head> <body> <h1>Public Page</h1> <!-- Display message from the model --> <p th:text="${message}"></p> <!-- Link to login page --> <a href="/login">Go to Login</a> </body> </html>

Enter fullscreen mode Exit fullscreen mode

  • home.html:
<span><!DOCTYPE html></span>
<span><html</span> <span>xmlns:th=</span><span>"http://www.thymeleaf.org"</span><span>></span>
<span><head><title></span>Home<span></title></head></span>
<span><body></span>
<span><!-- Display welcome message with username --></span>
<span><h1></span>Welcome, <span><span</span> <span>th:text=</span><span>"${username}"</span><span>></span></span>!<span></h1></span>
<span><!-- Logout form --></span>
<span><form</span> <span>th:action=</span><span>"@{/logout}"</span> <span>method=</span><span>"post"</span><span>></span>
<span><button</span> <span>type=</span><span>"submit"</span><span>></span>Logout<span></button></span>
<span></form></span>
<span></body></span>
<span></html></span>
<span><!DOCTYPE html></span>
<span><html</span> <span>xmlns:th=</span><span>"http://www.thymeleaf.org"</span><span>></span>
<span><head><title></span>Home<span></title></head></span>
<span><body></span>
    <span><!-- Display welcome message with username --></span>
    <span><h1></span>Welcome, <span><span</span> <span>th:text=</span><span>"${username}"</span><span>></span></span>!<span></h1></span>
    <span><!-- Logout form --></span>
    <span><form</span> <span>th:action=</span><span>"@{/logout}"</span> <span>method=</span><span>"post"</span><span>></span>
        <span><button</span> <span>type=</span><span>"submit"</span><span>></span>Logout<span></button></span>
    <span></form></span>
<span></body></span>
<span></html></span>
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head><title>Home</title></head> <body> <!-- Display welcome message with username --> <h1>Welcome, <span th:text="${username}"></span>!</h1> <!-- Logout form --> <form th:action="@{/logout}" method="post"> <button type="submit">Logout</button> </form> </body> </html>

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • login.html: Links to Keycloak’s OAuth2 login flow.
  • public.html: Displays a public message with a login option.
  • home.html: Shows the username and a logout button for authenticated users.

React Frontend Setup

The React frontend uses Vite, Tailwind CSS 4, DaisyUI 5 (beta), and keycloak-js.

Project Initialization

  1. Create the React App with Vite:
npm create vite@latest keycloak-react <span>--template</span> react
<span>cd </span>keycloak-react
npm <span>install</span>
   npm create vite@latest keycloak-react <span>--template</span> react
   <span>cd </span>keycloak-react
   npm <span>install</span>
npm create vite@latest keycloak-react --template react cd keycloak-react npm install

Enter fullscreen mode Exit fullscreen mode

  1. Install Tailwind CSS 4:
npm <span>install</span> <span>-D</span> tailwindcss @tailwindcss/vite
   npm <span>install</span> <span>-D</span> tailwindcss @tailwindcss/vite
npm install -D tailwindcss @tailwindcss/vite

Enter fullscreen mode Exit fullscreen mode

Update vite.config.js:

<span>// vite.config.js</span>
<span>import</span> <span>{</span> <span>defineConfig</span> <span>}</span> <span>from</span> <span>'</span><span>vite</span><span>'</span><span>;</span>
<span>import</span> <span>tailwindcss</span> <span>from</span> <span>'</span><span>@tailwindcss/vite</span><span>'</span><span>;</span>
<span>export</span> <span>default</span> <span>defineConfig</span><span>({</span>
<span>plugins</span><span>:</span> <span>[</span>
<span>react</span><span>(),</span>
<span>tailwindcss</span><span>(),</span> <span>// Integrate Tailwind CSS with Vite</span>
<span>],</span>
<span>});</span>
   <span>// vite.config.js</span>
   <span>import</span> <span>{</span> <span>defineConfig</span> <span>}</span> <span>from</span> <span>'</span><span>vite</span><span>'</span><span>;</span>
   <span>import</span> <span>tailwindcss</span> <span>from</span> <span>'</span><span>@tailwindcss/vite</span><span>'</span><span>;</span>

   <span>export</span> <span>default</span> <span>defineConfig</span><span>({</span>
     <span>plugins</span><span>:</span> <span>[</span>
       <span>react</span><span>(),</span>
       <span>tailwindcss</span><span>(),</span> <span>// Integrate Tailwind CSS with Vite</span>
     <span>],</span>
   <span>});</span>
// vite.config.js import { defineConfig } from 'vite'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ react(), tailwindcss(), // Integrate Tailwind CSS with Vite ], });

Enter fullscreen mode Exit fullscreen mode

  1. Install DaisyUI 5 (Beta):
npm <span>install</span> <span>-D</span> daisyui@beta
   npm <span>install</span> <span>-D</span> daisyui@beta
npm install -D daisyui@beta

Enter fullscreen mode Exit fullscreen mode

Update src/index.css:

<span>/* src/index.css */</span>
<span>@import</span> <span>"tailwindcss"</span><span>;</span> <span>//</span> <span>Import</span> <span>Tailwind</span> <span>CSS</span>
<span>@plugin</span> <span>"daisyui"</span><span>;</span> <span>//</span> <span>Add</span> <span>DaisyUI</span> <span>as</span> <span>a</span> <span>plugin</span>
   <span>/* src/index.css */</span>
   <span>@import</span> <span>"tailwindcss"</span><span>;</span>  <span>//</span> <span>Import</span> <span>Tailwind</span> <span>CSS</span>
   <span>@plugin</span> <span>"daisyui"</span><span>;</span>      <span>//</span> <span>Add</span> <span>DaisyUI</span> <span>as</span> <span>a</span> <span>plugin</span>
/* src/index.css */ @import "tailwindcss"; // Import Tailwind CSS @plugin "daisyui"; // Add DaisyUI as a plugin

Enter fullscreen mode Exit fullscreen mode

  1. Install Additional Dependencies:
npm <span>install </span>keycloak-js react-router-dom
   npm <span>install </span>keycloak-js react-router-dom
npm install keycloak-js react-router-dom

Enter fullscreen mode Exit fullscreen mode

Keycloak Initialization

In src/keycloak.js:

<span>// src/keycloak.js</span>
<span>import</span> <span>Keycloak</span> <span>from</span> <span>"</span><span>keycloak-js</span><span>"</span><span>;</span>
<span>// Initialize Keycloak instance with configuration</span>
<span>const</span> <span>keycloak</span> <span>=</span> <span>new</span> <span>Keycloak</span><span>({</span>
<span>url</span><span>:</span> <span>"</span><span>http://localhost:8088/</span><span>"</span><span>,</span> <span>// Keycloak server URL</span>
<span>realm</span><span>:</span> <span>"</span><span>my-realm</span><span>"</span><span>,</span> <span>// Realm name</span>
<span>clientId</span><span>:</span> <span>"</span><span>react-app</span><span>"</span><span>,</span> <span>// Client ID for React</span>
<span>});</span>
<span>export</span> <span>default</span> <span>keycloak</span><span>;</span>
<span>// src/keycloak.js</span>
<span>import</span> <span>Keycloak</span> <span>from</span> <span>"</span><span>keycloak-js</span><span>"</span><span>;</span>

<span>// Initialize Keycloak instance with configuration</span>
<span>const</span> <span>keycloak</span> <span>=</span> <span>new</span> <span>Keycloak</span><span>({</span>
    <span>url</span><span>:</span> <span>"</span><span>http://localhost:8088/</span><span>"</span><span>,</span> <span>// Keycloak server URL</span>
    <span>realm</span><span>:</span> <span>"</span><span>my-realm</span><span>"</span><span>,</span>            <span>// Realm name</span>
    <span>clientId</span><span>:</span> <span>"</span><span>react-app</span><span>"</span><span>,</span>        <span>// Client ID for React</span>
<span>});</span>

<span>export</span> <span>default</span> <span>keycloak</span><span>;</span>
// src/keycloak.js import Keycloak from "keycloak-js"; // Initialize Keycloak instance with configuration const keycloak = new Keycloak({ url: "http://localhost:8088/", // Keycloak server URL realm: "my-realm", // Realm name clientId: "react-app", // Client ID for React }); export default keycloak;

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • keycloak: Configures keycloak-js with the react-app client settings.

Main Entry (main.jsx)

<span>// src/main.jsx</span>
<span>import</span> <span>{</span> <span>createRoot</span> <span>}</span> <span>from</span> <span>'</span><span>react-dom/client</span><span>'</span><span>;</span>
<span>import</span> <span>'</span><span>./index.css</span><span>'</span><span>;</span>
<span>import</span> <span>App</span> <span>from</span> <span>'</span><span>./App.jsx</span><span>'</span><span>;</span>
<span>import</span> <span>{</span> <span>createBrowserRouter</span><span>,</span> <span>RouterProvider</span> <span>}</span> <span>from</span> <span>'</span><span>react-router-dom</span><span>'</span><span>;</span>
<span>import</span> <span>Public</span> <span>from</span> <span>'</span><span>./pages/Public.jsx</span><span>'</span><span>;</span>
<span>// Define routes for the app</span>
<span>const</span> <span>router</span> <span>=</span> <span>createBrowserRouter</span><span>([</span>
<span>{</span> <span>path</span><span>:</span> <span>'</span><span>/</span><span>'</span><span>,</span> <span>element</span><span>:</span> <span><</span><span>App</span> <span>/></span> <span>},</span> <span>// Root route to App</span>
<span>{</span> <span>path</span><span>:</span> <span>'</span><span>/public</span><span>'</span><span>,</span> <span>element</span><span>:</span> <span><</span><span>Public</span> <span>/></span> <span>}</span> <span>// Public page route</span>
<span>]);</span>
<span>// Render the app with RouterProvider</span>
<span>createRoot</span><span>(</span><span>document</span><span>.</span><span>getElementById</span><span>(</span><span>'</span><span>root</span><span>'</span><span>)).</span><span>render</span><span>(</span>
<span><</span><span>RouterProvider</span> <span>router</span><span>=</span><span>{</span><span>router</span><span>}</span> <span>/</span><span>> </span><span>);</span>
<span>// src/main.jsx</span>
<span>import</span> <span>{</span> <span>createRoot</span> <span>}</span> <span>from</span> <span>'</span><span>react-dom/client</span><span>'</span><span>;</span>
<span>import</span> <span>'</span><span>./index.css</span><span>'</span><span>;</span>
<span>import</span> <span>App</span> <span>from</span> <span>'</span><span>./App.jsx</span><span>'</span><span>;</span>
<span>import</span> <span>{</span> <span>createBrowserRouter</span><span>,</span> <span>RouterProvider</span> <span>}</span> <span>from</span> <span>'</span><span>react-router-dom</span><span>'</span><span>;</span>
<span>import</span> <span>Public</span> <span>from</span> <span>'</span><span>./pages/Public.jsx</span><span>'</span><span>;</span>

<span>// Define routes for the app</span>
<span>const</span> <span>router</span> <span>=</span> <span>createBrowserRouter</span><span>([</span>
    <span>{</span> <span>path</span><span>:</span> <span>'</span><span>/</span><span>'</span><span>,</span> <span>element</span><span>:</span> <span><</span><span>App</span> <span>/></span> <span>},</span>         <span>// Root route to App</span>
    <span>{</span> <span>path</span><span>:</span> <span>'</span><span>/public</span><span>'</span><span>,</span> <span>element</span><span>:</span> <span><</span><span>Public</span> <span>/></span> <span>}</span> <span>// Public page route</span>
<span>]);</span>

<span>// Render the app with RouterProvider</span>
<span>createRoot</span><span>(</span><span>document</span><span>.</span><span>getElementById</span><span>(</span><span>'</span><span>root</span><span>'</span><span>)).</span><span>render</span><span>(</span>
    <span><</span><span>RouterProvider</span> <span>router</span><span>=</span><span>{</span><span>router</span><span>}</span> <span>/</span><span>> </span><span>);</span>
// src/main.jsx import { createRoot } from 'react-dom/client'; import './index.css'; import App from './App.jsx'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import Public from './pages/Public.jsx'; // Define routes for the app const router = createBrowserRouter([ { path: '/', element: <App /> }, // Root route to App { path: '/public', element: <Public /> } // Public page route ]); // Render the app with RouterProvider createRoot(document.getElementById('root')).render( <RouterProvider router={router} /> );

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • router: Sets up routing with react-router-dom.
  • createRoot: Renders the React app.

App Component (App.jsx)

<span>// src/App.jsx</span>
<span>import</span> <span>{</span> <span>useEffect</span><span>,</span> <span>useState</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>keycloak</span> <span>from</span> <span>"</span><span>./keycloak</span><span>"</span><span>;</span>
<span>const</span> <span>App</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
<span>const</span> <span>[</span><span>authenticated</span><span>,</span> <span>setAuthenticated</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>false</span><span>);</span> <span>// Track authentication status</span>
<span>const</span> <span>[</span><span>data</span><span>,</span> <span>setData</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>null</span><span>);</span> <span>// Store API response</span>
<span>useEffect</span><span>(()</span> <span>=></span> <span>{</span>
<span>// Initialize Keycloak and require login</span>
<span>keycloak</span>
<span>.</span><span>init</span><span>({</span> <span>onLoad</span><span>:</span> <span>"</span><span>login-required</span><span>"</span> <span>})</span>
<span>.</span><span>then</span><span>((</span><span>authenticated</span><span>)</span> <span>=></span> <span>{</span>
<span>setAuthenticated</span><span>(</span><span>authenticated</span><span>);</span>
<span>if </span><span>(</span><span>authenticated</span><span>)</span> <span>{</span>
<span>// Fetch secured endpoint with JWT</span>
<span>fetch</span><span>(</span><span>"</span><span>http://localhost:8081/secured</span><span>"</span><span>,</span> <span>{</span>
<span>headers</span><span>:</span> <span>{</span>
<span>Authorization</span><span>:</span> <span>`Bearer </span><span>${</span><span>keycloak</span><span>.</span><span>token</span><span>}</span><span>`</span><span>,</span> <span>// Attach JWT</span>
<span>},</span>
<span>})</span>
<span>.</span><span>then</span><span>((</span><span>res</span><span>)</span> <span>=></span> <span>{</span>
<span>if </span><span>(</span><span>!</span><span>res</span><span>.</span><span>ok</span><span>)</span> <span>throw</span> <span>new</span> <span>Error</span><span>(</span><span>`HTTP error! status: </span><span>${</span><span>res</span><span>.</span><span>status</span><span>}</span><span>`</span><span>);</span>
<span>return</span> <span>res</span><span>.</span><span>json</span><span>();</span> <span>// Parse JSON response</span>
<span>})</span>
<span>.</span><span>then</span><span>((</span><span>d</span><span>)</span> <span>=></span> <span>setData</span><span>(</span><span>d</span><span>))</span> <span>// Set response data</span>
<span>.</span><span>catch</span><span>((</span><span>err</span><span>)</span> <span>=></span> <span>console</span><span>.</span><span>error</span><span>(</span><span>"</span><span>Fetch error:</span><span>"</span><span>,</span> <span>err</span><span>));</span>
<span>}</span>
<span>})</span>
<span>.</span><span>catch</span><span>((</span><span>err</span><span>)</span> <span>=></span> <span>console</span><span>.</span><span>error</span><span>(</span><span>"</span><span>Keycloak init error:</span><span>"</span><span>,</span> <span>err</span><span>));</span>
<span>},</span> <span>[]);</span>
<span>// Handle logout with redirect</span>
<span>const</span> <span>handleLogout</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
<span>keycloak</span><span>.</span><span>logout</span><span>({</span> <span>redirectUri</span><span>:</span> <span>"</span><span>http://localhost:5173/public</span><span>"</span> <span>});</span>
<span>};</span>
<span>if </span><span>(</span><span>!</span><span>authenticated</span><span>)</span> <span>{</span>
<span>return</span> <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>text-center mt-10</span><span>"</span><span>></span><span>Loading</span><span>...</span><span><</span><span>/div>; /</span><span>/</span> <span>Loading</span> <span>state</span>
<span>}</span>
<span>return </span><span>(</span>
<span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>min-h-screen bg-base-200 p-4</span><span>"</span><span>></span>
<span>{</span><span>/* Welcome message with Tailwind and DaisyUI styling */</span><span>}</span>
<span><</span><span>h1</span> <span>className</span><span>=</span><span>"</span><span>text-3xl font-bold text-center mb-4</span><span>"</span><span>></span>
<span>Welcome</span><span>,</span> <span>{</span><span>keycloak</span><span>.</span><span>tokenParsed</span><span>?.</span><span>preferred_username</span><span>}</span>
<span><</span><span>/h1</span><span>> </span> <span>{</span><span>/* Logout button */</span><span>}</span>
<span><</span><span>button</span> <span>onClick</span><span>=</span><span>{</span><span>handleLogout</span><span>}</span> <span>className</span><span>=</span><span>"</span><span>btn btn-secondary mb-4</span><span>"</span><span>></span>
<span>Logout</span>
<span><</span><span>/button</span><span>> </span> <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>card bg-base-100 shadow-xl p-4</span><span>"</span><span>></span>
<span><</span><span>h3</span> <span>className</span><span>=</span><span>"</span><span>text-xl font-semibold</span><span>"</span><span>></span><span>Response</span> <span>from</span> <span>back</span><span>-</span><span>end</span><span>:</span><span><</span><span>/h3</span><span>> </span> <span>{</span><span>data</span> <span>?</span> <span>(</span>
<span><</span><span>div</span><span>></span>
<span><</span><span>h1</span> <span>className</span><span>=</span><span>"</span><span>text-2xl mt-2</span><span>"</span><span>></span><span>{</span><span>data</span><span>.</span><span>message</span><span>}</span><span><</span><span>/h1</span><span>> </span> <span><</span><span>p</span> <span>className</span><span>=</span><span>"</span><span>mt-2</span><span>"</span><span>></span><span>Username</span><span>:</span> <span>{</span><span>data</span><span>.</span><span>username</span><span>}</span><span><</span><span>/p</span><span>> </span> <span><</span><span>/div</span><span>> </span> <span>)</span> <span>:</span> <span>(</span>
<span><</span><span>p</span> <span>className</span><span>=</span><span>"</span><span>mt-2</span><span>"</span><span>></span><span>Loading</span> <span>data</span><span>...</span><span><</span><span>/p</span><span>> </span> <span>)}</span>
<span><</span><span>/div</span><span>> </span> <span><</span><span>/div</span><span>> </span> <span>);</span>
<span>};</span>
<span>export</span> <span>default</span> <span>App</span><span>;</span>
<span>// src/App.jsx</span>
<span>import</span> <span>{</span> <span>useEffect</span><span>,</span> <span>useState</span> <span>}</span> <span>from</span> <span>"</span><span>react</span><span>"</span><span>;</span>
<span>import</span> <span>keycloak</span> <span>from</span> <span>"</span><span>./keycloak</span><span>"</span><span>;</span>

<span>const</span> <span>App</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
    <span>const</span> <span>[</span><span>authenticated</span><span>,</span> <span>setAuthenticated</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>false</span><span>);</span> <span>// Track authentication status</span>
    <span>const</span> <span>[</span><span>data</span><span>,</span> <span>setData</span><span>]</span> <span>=</span> <span>useState</span><span>(</span><span>null</span><span>);</span>                   <span>// Store API response</span>

    <span>useEffect</span><span>(()</span> <span>=></span> <span>{</span>
        <span>// Initialize Keycloak and require login</span>
        <span>keycloak</span>
            <span>.</span><span>init</span><span>({</span> <span>onLoad</span><span>:</span> <span>"</span><span>login-required</span><span>"</span> <span>})</span>
            <span>.</span><span>then</span><span>((</span><span>authenticated</span><span>)</span> <span>=></span> <span>{</span>
                <span>setAuthenticated</span><span>(</span><span>authenticated</span><span>);</span>
                <span>if </span><span>(</span><span>authenticated</span><span>)</span> <span>{</span>
                    <span>// Fetch secured endpoint with JWT</span>
                    <span>fetch</span><span>(</span><span>"</span><span>http://localhost:8081/secured</span><span>"</span><span>,</span> <span>{</span>
                        <span>headers</span><span>:</span> <span>{</span>
                            <span>Authorization</span><span>:</span> <span>`Bearer </span><span>${</span><span>keycloak</span><span>.</span><span>token</span><span>}</span><span>`</span><span>,</span> <span>// Attach JWT</span>
                        <span>},</span>
                    <span>})</span>
                        <span>.</span><span>then</span><span>((</span><span>res</span><span>)</span> <span>=></span> <span>{</span>
                            <span>if </span><span>(</span><span>!</span><span>res</span><span>.</span><span>ok</span><span>)</span> <span>throw</span> <span>new</span> <span>Error</span><span>(</span><span>`HTTP error! status: </span><span>${</span><span>res</span><span>.</span><span>status</span><span>}</span><span>`</span><span>);</span>
                            <span>return</span> <span>res</span><span>.</span><span>json</span><span>();</span>                     <span>// Parse JSON response</span>
                        <span>})</span>
                        <span>.</span><span>then</span><span>((</span><span>d</span><span>)</span> <span>=></span> <span>setData</span><span>(</span><span>d</span><span>))</span>                   <span>// Set response data</span>
                        <span>.</span><span>catch</span><span>((</span><span>err</span><span>)</span> <span>=></span> <span>console</span><span>.</span><span>error</span><span>(</span><span>"</span><span>Fetch error:</span><span>"</span><span>,</span> <span>err</span><span>));</span>
                <span>}</span>
            <span>})</span>
            <span>.</span><span>catch</span><span>((</span><span>err</span><span>)</span> <span>=></span> <span>console</span><span>.</span><span>error</span><span>(</span><span>"</span><span>Keycloak init error:</span><span>"</span><span>,</span> <span>err</span><span>));</span>
    <span>},</span> <span>[]);</span>

    <span>// Handle logout with redirect</span>
    <span>const</span> <span>handleLogout</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
        <span>keycloak</span><span>.</span><span>logout</span><span>({</span> <span>redirectUri</span><span>:</span> <span>"</span><span>http://localhost:5173/public</span><span>"</span> <span>});</span>
    <span>};</span>

    <span>if </span><span>(</span><span>!</span><span>authenticated</span><span>)</span> <span>{</span>
        <span>return</span> <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>text-center mt-10</span><span>"</span><span>></span><span>Loading</span><span>...</span><span><</span><span>/div>; /</span><span>/</span> <span>Loading</span> <span>state</span>
    <span>}</span>

    <span>return </span><span>(</span>
        <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>min-h-screen bg-base-200 p-4</span><span>"</span><span>></span>
            <span>{</span><span>/* Welcome message with Tailwind and DaisyUI styling */</span><span>}</span>
            <span><</span><span>h1</span> <span>className</span><span>=</span><span>"</span><span>text-3xl font-bold text-center mb-4</span><span>"</span><span>></span>
                <span>Welcome</span><span>,</span> <span>{</span><span>keycloak</span><span>.</span><span>tokenParsed</span><span>?.</span><span>preferred_username</span><span>}</span>
            <span><</span><span>/h1</span><span>> </span>            <span>{</span><span>/* Logout button */</span><span>}</span>
            <span><</span><span>button</span> <span>onClick</span><span>=</span><span>{</span><span>handleLogout</span><span>}</span> <span>className</span><span>=</span><span>"</span><span>btn btn-secondary mb-4</span><span>"</span><span>></span>
                <span>Logout</span>
            <span><</span><span>/button</span><span>> </span>            <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>card bg-base-100 shadow-xl p-4</span><span>"</span><span>></span>
                <span><</span><span>h3</span> <span>className</span><span>=</span><span>"</span><span>text-xl font-semibold</span><span>"</span><span>></span><span>Response</span> <span>from</span> <span>back</span><span>-</span><span>end</span><span>:</span><span><</span><span>/h3</span><span>> </span>                <span>{</span><span>data</span> <span>?</span> <span>(</span>
                    <span><</span><span>div</span><span>></span>
                        <span><</span><span>h1</span> <span>className</span><span>=</span><span>"</span><span>text-2xl mt-2</span><span>"</span><span>></span><span>{</span><span>data</span><span>.</span><span>message</span><span>}</span><span><</span><span>/h1</span><span>> </span>                        <span><</span><span>p</span> <span>className</span><span>=</span><span>"</span><span>mt-2</span><span>"</span><span>></span><span>Username</span><span>:</span> <span>{</span><span>data</span><span>.</span><span>username</span><span>}</span><span><</span><span>/p</span><span>> </span>                    <span><</span><span>/div</span><span>> </span>                <span>)</span> <span>:</span> <span>(</span>
                    <span><</span><span>p</span> <span>className</span><span>=</span><span>"</span><span>mt-2</span><span>"</span><span>></span><span>Loading</span> <span>data</span><span>...</span><span><</span><span>/p</span><span>> </span>                <span>)}</span>
            <span><</span><span>/div</span><span>> </span>        <span><</span><span>/div</span><span>> </span>    <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>App</span><span>;</span>
// src/App.jsx import { useEffect, useState } from "react"; import keycloak from "./keycloak"; const App = () => { const [authenticated, setAuthenticated] = useState(false); // Track authentication status const [data, setData] = useState(null); // Store API response useEffect(() => { // Initialize Keycloak and require login keycloak .init({ onLoad: "login-required" }) .then((authenticated) => { setAuthenticated(authenticated); if (authenticated) { // Fetch secured endpoint with JWT fetch("http://localhost:8081/secured", { headers: { Authorization: `Bearer ${keycloak.token}`, // Attach JWT }, }) .then((res) => { if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); return res.json(); // Parse JSON response }) .then((d) => setData(d)) // Set response data .catch((err) => console.error("Fetch error:", err)); } }) .catch((err) => console.error("Keycloak init error:", err)); }, []); // Handle logout with redirect const handleLogout = () => { keycloak.logout({ redirectUri: "http://localhost:5173/public" }); }; if (!authenticated) { return <div className="text-center mt-10">Loading...</div>; // Loading state } return ( <div className="min-h-screen bg-base-200 p-4"> {/* Welcome message with Tailwind and DaisyUI styling */} <h1 className="text-3xl font-bold text-center mb-4"> Welcome, {keycloak.tokenParsed?.preferred_username} </h1> {/* Logout button */} <button onClick={handleLogout} className="btn btn-secondary mb-4"> Logout </button> <div className="card bg-base-100 shadow-xl p-4"> <h3 className="text-xl font-semibold">Response from back-end:</h3> {data ? ( <div> <h1 className="text-2xl mt-2">{data.message}</h1> <p className="mt-2">Username: {data.username}</p> </div> ) : ( <p className="mt-2">Loading data...</p> )} </div> </div> ); }; export default App;

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • useEffect: Forces login via Keycloak and fetches /secured with JWT.
  • handleLogout: Logs out and redirects to /public.
  • UI: Uses Tailwind CSS 4 and DaisyUI 5 for a responsive card layout.

Public Page (pages/Public.jsx)

<span>// src/pages/Public.jsx</span>
<span>const</span> <span>Public</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
<span>return </span><span>(</span>
<span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>min-h-screen bg-base-200 flex items-center justify-center</span><span>"</span><span>></span>
<span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>card bg-base-100 shadow-xl p-6</span><span>"</span><span>></span>
<span><</span><span>h1</span> <span>className</span><span>=</span><span>"</span><span>text-2xl font-bold</span><span>"</span><span>></span><span>Public</span> <span>Page</span><span><</span><span>/h1</span><span>> </span> <span><</span><span>p</span> <span>className</span><span>=</span><span>"</span><span>mt-2</span><span>"</span><span>></span><span>This</span> <span>is</span> <span>a</span> <span>public</span> <span>page</span> <span>accessible</span> <span>to</span> <span>all</span><span>.</span><span><</span><span>/p</span><span>> </span> <span><</span><span>/div</span><span>> </span> <span><</span><span>/div</span><span>> </span> <span>);</span>
<span>};</span>
<span>export</span> <span>default</span> <span>Public</span><span>;</span>
<span>// src/pages/Public.jsx</span>
<span>const</span> <span>Public</span> <span>=</span> <span>()</span> <span>=></span> <span>{</span>
    <span>return </span><span>(</span>
        <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>min-h-screen bg-base-200 flex items-center justify-center</span><span>"</span><span>></span>
            <span><</span><span>div</span> <span>className</span><span>=</span><span>"</span><span>card bg-base-100 shadow-xl p-6</span><span>"</span><span>></span>
                <span><</span><span>h1</span> <span>className</span><span>=</span><span>"</span><span>text-2xl font-bold</span><span>"</span><span>></span><span>Public</span> <span>Page</span><span><</span><span>/h1</span><span>> </span>                <span><</span><span>p</span> <span>className</span><span>=</span><span>"</span><span>mt-2</span><span>"</span><span>></span><span>This</span> <span>is</span> <span>a</span> <span>public</span> <span>page</span> <span>accessible</span> <span>to</span> <span>all</span><span>.</span><span><</span><span>/p</span><span>> </span>            <span><</span><span>/div</span><span>> </span>        <span><</span><span>/div</span><span>> </span>    <span>);</span>
<span>};</span>

<span>export</span> <span>default</span> <span>Public</span><span>;</span>
// src/pages/Public.jsx const Public = () => { return ( <div className="min-h-screen bg-base-200 flex items-center justify-center"> <div className="card bg-base-100 shadow-xl p-6"> <h1 className="text-2xl font-bold">Public Page</h1> <p className="mt-2">This is a public page accessible to all.</p> </div> </div> ); }; export default Public;

Enter fullscreen mode Exit fullscreen mode

Breakdown

  • Public: A styled public page using Tailwind and DaisyUI components.

How It Works

  • Session-Based Web UI:

    • http://localhost:8081/login triggers Keycloak’s OAuth2 login.
    • Post-login, /home uses a session cookie to display the username.
    • Logout redirects to /login?logout.
  • JWT-Based React Frontend:

    • React forces login via keycloak-js and fetches /secured with a JWT.
    • Displays the response in a styled UI.
  • Unified Credentials:

    • Both clients share the my-realm realm and USER role, allowing testuser/password to work across the board.

Testing the Integration

  1. Start Keycloak and PostgreSQL: docker-compose up.
  2. Start Spring Boot: mvn spring-boot:run.
  3. Start React: npm run dev (runs on http://localhost:5173).
  4. Test:
    • http://localhost:8081/public: Public Thymeleaf page.
    • http://localhost:8081/home: Protected Thymeleaf page.
    • http://localhost:5173/: React app with secured API data.

Conclusion

This integration harnesses Keycloak’s versatility to unify authentication across Spring Boot with Thymeleaf and React with Tailwind CSS 4 and DaisyUI 5 (beta). By splitting security into session-based and JWT-based flows with precise endpoint matching, we cater to diverse client needs seamlessly. For production, secure with HTTPS and refine CORS settings. This setup provides a robust foundation for modern full-stack applications.

原文链接:Seamless Authentication System: Integrating Keycloak with Spring Boot, Thymeleaf, and React Using OAuth2 and JWT

© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
Success is a battle between YOU and YOURSELF only.
成功是一场和自己的比赛
评论 抢沙发

请登录后发表评论

    暂无评论内容