Every serious backend eventually has to answer two questions for every request: who is this? and what are they allowed to do? You can build login, password reset, token issuing, SSO and role management yourself, but it is hard, slow and very easy to get wrong. Or you can hand the whole job to Keycloak and keep your Spring Boot code focused on the business logic.
This article is the written companion to my full Keycloak + Spring Boot course on YouTube. We go from zero to a working, role-secured REST API: install Keycloak, set up a realm, client, roles and users, move Keycloak off its default H2 database onto PostgreSQL, test the OpenID Connect endpoints in Postman, and finally protect Spring Boot endpoints with JWTs and Spring Security.
What We Are Building
Two servers run side by side: Keycloak on localhost:8080 and our Spring Boot API on localhost:8082. Keycloak has one realm (codingcenter), one client for our app, two roles (employee and manager) and two users:
| User | Role | Can call |
|---|---|---|
| John Doe | manager | /api/v1 and /api/v1/manager |
| Jane Doe | employee | /api/v1 and /api/v1/employee |
The API exposes three endpoints. Any valid token can call the common one; the other two check the role inside the token:
| Endpoint | Token from John (manager) | Token from Jane (employee) |
|---|---|---|
GET /api/v1 | 200 – Hello! | 200 – Hello! |
GET /api/v1/manager | 200 – Hello from Admin!! | 403 Forbidden |
GET /api/v1/employee | 403 Forbidden | 200 – Hello from User!! |
On top of that we build a Postman collection for the Keycloak side: login, logout, logout validation and guest login, all driven by environment variables so you never copy-paste a token by hand.
Why Do We Need Keycloak?
Picture a big party with a gaming room, a movie room, a food room and a VIP lounge. You don't want guests proving they're invited at every single door. So you hire one bouncer at the front door. He checks your ID once and stamps your hand. With that stamp you walk into every room, and when you reach the VIP lounge the bouncer only has to check whether your stamp says VIP.
Keycloak is that bouncer, and the stamp is a token. In technical terms, Keycloak is an open-source identity and access management (IAM) solution that gives you:
- Authentication (who you are): username/password, social logins and multi-factor authentication, with ready-made login pages.
- Authorization (what you can do): roles, permissions and access policies. An admin can edit users; a regular user cannot.
- Single Sign-On: log in once, use many apps, the way one Google login opens Gmail, Drive and Calendar. Logout works the same way: log out once and you are out everywhere.
- Federation: plug into identity systems you already have, such as LDAP, Active Directory, Google or SAML providers.
- Token-based access: secure JWTs that APIs can verify without keeping sessions. This is the part our Spring Boot API uses.
- Standards: OAuth 2.0, OpenID Connect and SAML, plus an admin console and REST APIs.
It is also widely used in production. Red Hat, Siemens, CERN, Deutsche Bank, EU institutions, the Brazilian government, the UK's NHS and Novartis all run it. The reasons are usually the same: it is free and open source, SSO/MFA/OAuth2 work out of the box, and it scales well with microservices and Kubernetes.
Tokens vs Federation
These two often get mixed up:
- A token is a passport. After login, Keycloak gives your app a JWT that proves who the user is and what they can do. Your app can verify it on its own, without calling Keycloak on every request.
- Federation is a trust agreement. Keycloak trusts another system (Google, Microsoft, your company's LDAP) to do the authentication, so users sign in with credentials they already have instead of creating new accounts in Keycloak.
In a typical enterprise setup, you sign in once through Keycloak (which may delegate to Azure AD or LDAP behind the scenes). Keycloak then issues a token, and every downstream application reads it to decide what you are allowed to access.
The Four Building Blocks
| Concept | What it is | In our project |
|---|---|---|
| Realm | An isolated container for users, credentials, roles and groups. Realms share nothing with each other. Think of separate castles in a kingdom. | codingcenter |
| Client | An application or service that asks Keycloak to authenticate users (web app, mobile app or API). | Our Spring Boot app |
| Role | A responsibility or permission you assign to users. | employee, manager |
| User | A person (or account) who logs in. | John Doe, Jane Doe |
Step 1: Install Keycloak and Start It in Dev Mode
Keycloak needs OpenJDK 21, so install that first. Then go to the official Keycloak site, click Download, and grab the zip or tar.gz for your OS (the contents are the same either way). Extract it and you'll see these folders:
bin: holdskc.sh(Linux/macOS) andkc.bat(Windows), the scripts that run Keycloak.conf: holdskeycloak.conf, which we will edit when we move to PostgreSQL.lib,providers,themes: we won't touch these in this course.
From inside the Keycloak folder, start it in development mode:
cd keycloak
bin/kc.sh start-dev # Windows: bin\kc.bat start-devIn the logs you'll see Profile dev activated and Keycloak listening on port 8080. Open http://localhost:8080, create the initial admin user (I used admin / admin for local work), then click Open Administration Console and sign in.
A Quick Tour of the Admin Console
- Top-right menu: Manage account (profile, signing-in options, device activity, connected applications), realm info, clear caches, sign out.
- Manage realms: you start in the built-in
masterrealm. - Clients, Client scopes, Realm roles, Users, Groups: the building blocks from the table above.
- Sessions and Events: active sessions, and an audit trail of user and admin events.
- Realm settings: login options, email, themes, and the Endpoints link we use later.
- Authentication, Identity providers, User federation: auth flows, social/OpenID/SAML logins, and LDAP/Active Directory integration.
Step 2: Create the Realm, Client, Roles and Users
Realm
Go to Manage realms → Create realm, name it codingcenter and click Create. The current realm switches from master to codingcenter. Do everything after this inside this realm.
Client
Go to Clients → Create client, give it an ID for the Spring Boot app (the repo uses springboot-app), click Next and set the capability config:
| Setting | Value | Why |
|---|---|---|
| Client authentication | ON | Makes it a confidential client with a secret. With this OFF the client is public. |
| Authorization | ON | Enables fine-grained authorization. It also switches on service accounts, which the guest login (client credentials) relies on. |
| Standard flow | ON (default) | OpenID Connect redirect-based login with an authorization code. |
| Direct access grants | ON | Lets us exchange a username and password directly for tokens. This is what our Postman login request uses. |
Leave the URLs on the next screen empty for now and click Save. Then open the client's Credentials tab. That is where your client secret is, and you'll need it in Postman and in application.properties.
Realm Roles
Under Realm roles → Create role, create employee, then create manager. Keep them lowercase, because the Spring code matches them exactly.
Users
Under Users → Add user, create John Doe (first name John, last name Doe, an email). Leave Email verified off, since this is for learning. Then:
- Credentials tab → Set password (I used
john). Turn Temporary OFF, or the user is forced to change it on first login. - Role mapping tab → Assign role → switch the filter from client roles to realm roles → pick
manager.
Repeat for Jane Doe with password jane and role employee. To double-check, open a role and look at the Users in role tab.
Step 3: Move Keycloak from H2 to PostgreSQL
In dev mode, the startup log lists jdbc-h2 under installed features. Keycloak is using an embedded H2 database. That is why start-dev works with no setup at all.
H2 is like instant noodles: great for a quick bite, terrible as a daily diet. A crash or shutdown can corrupt the data, other applications can't share it, and upgrades can break it. PostgreSQL is the proper home-cooked meal. It takes a bit of setup, but it handles lots of users and data, accepts connections from other services, and comes with backups, restores, monitoring and GUI tooling. PostgreSQL 17 is also a version Keycloak officially lists as tested.
Install PostgreSQL and Create the Database
- Download the installer for your OS from the official PostgreSQL site. On macOS you can also use Homebrew.
- Select the PostgreSQL Server, pgAdmin 4 (the GUI) and Stack Builder components. Command Line Tools are optional.
- Set a password for the superuser
postgresand remember it. Keep the default port5432. - Open pgAdmin 4 and create an empty database called
keycloak.
Point Keycloak at PostgreSQL
Open conf/keycloak.conf, uncomment the db line, and add the connection settings:
# Database
db=postgres
# PostgreSQL connection settings
db-url=jdbc:postgresql://localhost:5432/keycloak
db-username=postgres
db-password=<your-postgres-password>
# Optional: log level
log-level=info
# HTTP (local only - see the note below)
http-enabled=true
hostname-strict=falseNow start Keycloak in production mode, using start instead of start-dev:
bin/kc.sh start
# Review what Keycloak picked up from keycloak.conf
bin/kc.sh show-config
# After the first build, Keycloak suggests this for later runs
bin/kc.sh start --optimizedOn the first run, Keycloak detects the configuration change, rebuilds its server image and persists the new configuration. I hit two errors along the way in the video, and you may too:
| Error | Cause | Fix |
|---|---|---|
Key material not provided to setup HTTPS | Production mode expects HTTPS and disables HTTP by default. | Add http-enabled=true and hostname-strict=false to keycloak.conf (for local use only). |
password authentication failed for user ... | Wrong value in db-username or db-password. | Use the real PostgreSQL superuser (postgres) and its password. |
http-enabled=true with hostname-strict=false is fine on your laptop. On a real server, configure TLS certificates and a proper hostname instead.Once Keycloak is listening, refresh the keycloak database in pgAdmin. You'll find around 88 tables that Keycloak created on its own. Because this is a fresh database, Keycloak asks you to create the admin user again, and you need to recreate the realm, client, roles and users as well. After that, query user_entity and John Doe and Jane Doe are there, now stored in PostgreSQL.
Step 4: Test the OpenID Connect Endpoints in Postman
Go to Realm settings, scroll down to Endpoints, and open OpenID Endpoint Configuration. Paste the JSON into any formatter and you'll see the endpoints we care about: token_endpoint, authorization_endpoint, introspection_endpoint, userinfo_endpoint and end_session_endpoint.
Collection and Environment Variables
In Postman, create a collection (mine is Spring Boot Keycloak Demo) and an environment (Spring Boot Keycloak Env) with these variables, so no URL or secret is hard-coded:
| Variable | Value |
|---|---|
keycloak_url | http://localhost:8080 |
realm_name | codingcenter |
client_id | Your client ID from Keycloak |
client_secret | From the client's Credentials tab |
access_token | Empty: filled automatically by the login request (Step 6) |
refresh_token | Empty: filled automatically by the login request (Step 6) |
Reference a variable as {{keycloak_url}}. When it turns blue/green, Postman has resolved it from the selected environment.
1. Login (Token Endpoint)
POST {{keycloak_url}}/realms/{{realm_name}}/protocol/openid-connect/token| Body key (x-www-form-urlencoded) | Value |
|---|---|
username | The username you created for John Doe |
password | john |
grant_type | password |
client_id | {{client_id}} |
client_secret | {{client_secret}} |
Send it and you get 200 OK with both tokens:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
"expires_in": 300,
"refresh_expires_in": 1800,
"refresh_token": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6...",
"token_type": "Bearer",
"scope": "profile email"
}Access Token vs Refresh Token
| Access token | Refresh token | |
|---|---|---|
| Lifetime | Short: 300 s (5 min) | Longer: 1800 s (30 min) |
| Used for | Calling protected APIs, sent as Authorization: Bearer ... | Getting a new access token without logging in again |
| Contains | User info, roles and permissions (JWT) | A handle to the user's Keycloak session |
| Why | If stolen, it is only useful for a few minutes | Keeps background sessions alive |
2. Logout (End Session Endpoint)
POST {{keycloak_url}}/realms/{{realm_name}}/protocol/openid-connect/logout- Authorization: Bearer Token →
{{access_token}} - Body:
client_id,client_secret,refresh_token(the latest, unexpired one)
A successful logout returns 204 No Content with an empty body.
3. Logout Validation
How do you prove the logout actually worked? Duplicate the login request and try to use the refresh token you just logged out with:
| Body key | Value |
|---|---|
grant_type | refresh_token |
refresh_token | The refresh token from before the logout |
client_id | {{client_id}} |
client_secret | {{client_secret}} |
Before logout, this returns a fresh access token. After logout, it returns 400 Bad Request with invalid_grant / Session not active. The session is gone, so the old refresh token is useless.
4. Guest Login (Client Credentials)
What about a caller who has no user account in Keycloak? There is no username or password, and no token to send. Here the client itself authenticates:
- Authorization: Basic Auth → username = client ID, password = client secret
- Body:
grant_type=client_credentials
You get 200 OK with an access token but no refresh token. When it expires, the caller simply requests a new one.
Step 5: Build the Spring Boot Resource Server
Generate the Project
On Spring Initializr, pick:
| Option | Value |
|---|---|
| Project / Language | Maven / Java |
| Spring Boot | 4.x |
| Group / Artifact | com.example / KeycloakSpringBootApp |
| Java | 21 (25 works too) |
| Dependencies | Spring Web, Spring Security, OAuth2 Resource Server |
Why Resource Server and not the other two OAuth2 options?
- Authorization Server issues tokens. Keycloak already does that, so we don't need it.
- OAuth2 Client calls other APIs using a token it obtained. That isn't our job here.
- Resource Server hosts APIs that require tokens. That is exactly what we are building. It validates JWTs automatically using Keycloak's public keys and plugs straight into Spring Security, so you never parse or verify a JWT by hand.
application.properties
Spring Boot and Keycloak both default to port 8080, so the app moves to 8082. The repo uses realm codingcenter:
# ===============================
# Application Name and Port
# ===============================
spring.application.name=keycloakdemo
server.port=8082
# ===============================
# Keycloak OAuth2 Client (if you want to support login flows)
# ===============================
spring.security.oauth2.client.registration.keycloak.client-id=springboot-app
spring.security.oauth2.client.registration.keycloak.client-secret=<your-client-secret>
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri=http://localhost:8082/login/oauth2/code/keycloak
spring.security.oauth2.client.registration.keycloak.provider=keycloak
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/codingcenter
# ===============================
# OAuth2 Resource Server (API Token Validation)
# ===============================
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/codingcenter
# Alternatively:
# spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/realms/codingcenter/protocol/openid-connect/certs
# ===============================
# Logging (for Debugging)
# ===============================
logging.level.org.springframework.security=DEBUG
logging.level.org.springframework.web=INFOWhat each block does:
- OAuth2 Client: client ID and secret (your app's identity and password in Keycloak), scopes (openid, profile, email), the authorization-code browser flow, the redirect URI Keycloak returns to after login, and
issuer-uri, which tells the app where Keycloak lives. - Resource Server: the property that actually protects the API. Every incoming
Authorization: Bearer <jwt>is validated against this issuer. Spring checks the token'sissclaim and auto-discovers the JWKS endpoint for the signing keys. The commented-outjwk-set-uriis the alternative: it skips discovery and points straight at the keys. - Logging:
DEBUGfor Spring Security, so you can watch every auth decision while learning.
spring-boot-starter-oauth2-client for browser-based login. Token validation only needs the resource-server line.client-secret=${KEYCLOAK_CLIENT_SECRET}.JwtConverter: Teaching Spring Security About Keycloak Roles
Keycloak puts realm roles inside a nested claim in the JWT:
"realm_access": {
"roles": [
"manager",
"offline_access",
"uma_authorization",
"default-roles-codingcenter"
]
}Spring Security doesn't read realm_access.roles by default. Its hasRole(...) checks look for GrantedAuthority objects with a ROLE_ prefix. The converter bridges the two:
package com.example.KeycloakSpringBootApp.config;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class JwtConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public @Nullable Collection<GrantedAuthority> convert(Jwt source) {
Map<String, Object> realmAccess = source.getClaim("realm_access");
if (realmAccess == null || realmAccess.isEmpty()) {
return Collections.emptyList();
}
Collection<String> roles = (Collection<String>) realmAccess.get("roles");
return roles.stream()
.map(role -> "ROLE_" + role)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}@Componentregisters it as a Spring bean so the security config can inject it.- It reads the
realm_accessclaim as aMap. If the claim is missing or empty, it returns an empty list (a defensive check: no roles means no authorities). - It pulls the
rolesarray, prefixes each role withROLE_, wraps it in aSimpleGrantedAuthorityand collects the results into a list. SomanagerbecomesROLE_manager.
SecurityConfig: The Bouncer at Your API's Door
package com.example.KeycloakSpringBootApp.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.beans.factory.annotation.Autowired;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtConverter jwtConverter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // disable CSRF for REST APIs
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // public APIs
.requestMatchers("/api/manager/**").hasRole("manager") // only manager
.requestMatchers("/api/employee/**").hasRole("employee") // only employee
.anyRequest().authenticated() // everything else requires auth
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwtConverter); // plug in custom Keycloak role converter
return converter;
}
}@Configuration+@EnableWebSecurityturn on Spring Security for web requests;@EnableMethodSecurityenables@PreAuthorizeon controller methods.- CSRF is disabled because this is a stateless REST API that uses bearer tokens. CSRF protection matters for browser forms and cookie sessions, not here.
authorizeHttpRequestsdefines URL rules: public paths are open, manager and employee paths need their role, andanyRequest().authenticated()means everything else needs a valid token.oauth2ResourceServer(...jwt...)tells Spring that this app is a resource server and that it should validate incoming tokens as JWTs, running each one through ourJwtConvertervia aJwtAuthenticationConverter.
/api/v1/..., so the /api/manager/** and /api/employee/** URL rules don't match them. Those requests fall through to anyRequest().authenticated(). The role check that produces the 403 in the demo is the method-level @PreAuthorize in the controller. If you prefer URL-based rules, change the matchers to /api/v1/manager/** and /api/v1/employee/**. Keeping both layers is a reasonable defense-in-depth choice.Controller
package com.example.KeycloakSpringBootApp.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1")
public class Controller {
@GetMapping
public ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello!");
}
@GetMapping("/employee")
@PreAuthorize("hasRole('employee')")
public ResponseEntity<String> helloUser() {
return ResponseEntity.ok("Hello from User!!");
}
@GetMapping("/manager")
@PreAuthorize("hasRole('manager')")
public ResponseEntity<String> helloAdmin() {
return ResponseEntity.ok("Hello from Admin!!");
}
}@RequestMapping("/api/v1")gives every endpoint a common versioned prefix.GET /api/v1has no role check, so any valid token from any user works.GET /api/v1/employeeandGET /api/v1/manageruse@PreAuthorize("hasRole(...)"). Spring adds theROLE_prefix itself and compares the result with the authorities our converter built.
Step 6: End-to-End Validation
Start the Spring Boot app, log in as a user in Postman, then call each endpoint with Authorization → Bearer Token:
| Logged in as | Endpoint | Result |
|---|---|---|
| John (manager) | GET /api/v1 | 200 – Hello! |
| Jane (employee) | GET /api/v1 | 200 – Hello! |
| Jane (employee) | GET /api/v1/manager | 403 Forbidden |
| Jane (employee) | GET /api/v1/employee | 200 – Hello from User!! |
| John (manager) | GET /api/v1/employee | 403 Forbidden |
| John (manager) | GET /api/v1/manager | 200 – Hello from Admin!! |
/v1 in the URL. Got a connection error? The Spring Boot app isn't running yet.Stop Copy-Pasting Tokens: Postman Post-Response Script
Open the Login request → Scripts → Post-response and add:
var jsonData = pm.response.json();
pm.environment.set("access_token", jsonData.access_token);
pm.environment.set("refresh_token", jsonData.refresh_token);Now every login writes fresh tokens into access_token and refresh_token. Set every other request to use {{access_token}} or {{refresh_token}}. Switching between John and Jane then takes one click on Send, with no copy-pasting.
Recap
- Installed Keycloak and ran it in dev mode on the embedded H2 database.
- Created the
codingcenterrealm, a confidential client,employee/managerroles and two users. - Moved Keycloak to PostgreSQL and ran it in production mode.
- Tested login, logout, logout validation and guest login against the OpenID Connect endpoints in Postman.
- Built a Spring Boot 4 resource server that validates Keycloak JWTs, maps realm roles to Spring authorities and enforces role-based access per endpoint.
From here, the natural next steps are refresh-token rotation in a real client, client roles instead of realm roles, and running Keycloak in Docker or Kubernetes behind TLS.
Resources
- Full course on Udemy: Master Keycloak with Spring Boot
- Source code: github.com/sprashantofficial/KeycloakSpringBootApp
- More backend development: YouTube @PrashantSharmaOfficial
If this helped, subscribe on YouTube and drop your questions in the comments. I read all of them.
Comments
Post a Comment