Skip to main content

Get Keycloak Auth Access Token

Understanding Keycloak Auth Access Token – A Deep Dive
$ keycloak / auth-deep-dive

🔐 Identity & Access Management

Getting a Keycloak Access Token
— and Actually Understanding It

June 2025 Keycloak OAuth 2.0 JWT OpenID Connect Service Account

You hit Keycloak's token endpoint, you get back a fat JSON blob — but what is all that stuff? This post dismantles a real token response piece by piece so you know exactly what you have, why the JWT is structured the way it is, and what to do with it next.

📡 Step 1 — How to Get the Token

Keycloak speaks OAuth 2.0. The endpoint that issues tokens lives under your realm:

HTTP Token Endpoint
POST http://127.0.0.1:7080/realms/master/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=eazybank-callcenter-cc
&client_secret=<YOUR_CLIENT_SECRET>
    

This uses the Client Credentials Grant — the right flow when a machine (service account) authenticates on its own behalf, with no human user involved. The client_id here is eazybank-callcenter-cc, and Keycloak issues a service account token for it.

ℹ️
Client Credentials vs Password Grant Use grant_type=client_credentials for backend services. Use grant_type=password only for user-facing flows (and even then, prefer Authorization Code + PKCE).

📦 Step 2 — The Full Token Response, Decoded

Keycloak returns a JSON object. Here's what each field actually means:

access_token
A signed JWT Bearer token. This is what you send to your APIs in the Authorization: Bearer … header. It encodes roles, scopes, and expiry directly inside it — no database lookup needed.
expires_in
60 seconds. The access token is valid for only 1 minute. Short-lived by design — if it leaks, damage is limited. Your client must refresh or re-fetch before it expires.
refresh_expires_in
0 — no refresh token issued. Client Credentials grants don't get refresh tokens in Keycloak. Re-authenticate when the token expires.
token_type
Bearer — the standard OAuth 2.0 token type. "Bearer" means whoever holds the token can use it.
id_token
An OpenID Connect identity token, issued because the scope included openid. Contains identity info about the authenticated principal (the service account here). Not for API authorization — for identity assertion only.
not-before-policy
0 — no minimum-issued-at policy set. Keycloak won't reject tokens issued before a certain time.
scope
openid email profile — the set of claims the token is allowed to carry. Governed by the client's configured scopes in Keycloak.

🧬 Step 3 — Inside the JWT (Three Parts)

A JWT is three Base64URL-encoded segments joined by dots: Header . Payload . Signature.

HEADER (red) eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJubm5pQjRyeEwwcWhHNzlwa0YzY2NRal9sMGpEbXhLcllZeTJZTGxFZTFzIn0
.
PAYLOAD (purple) eyJleHAiOjE3ODEzNDkwMDAsImlhdCI6MTc4MTM0ODk0MCwianRpIjoidH…
.
SIGNATURE (green) GJxSWj3QLAYgFEJ1PFZhjz21-8QluiE1Kpe…

Decoded Header

JSONHeader (algorithm + key id)
{
  "alg": "RS256",          // RSA + SHA-256 signing
  "typ": "JWT",
  "kid": "nnniBrxL0qhG79pkF3ccQj_l0jDmxKrYYy2YLlEe1s"  // key ID — used to look up the public key
}

Decoded Access Token Payload — All Claims Explained

JSONPayload (decoded)
{
  "exp":  1781349000,      // Expiry (Unix timestamp) — token dies here
  "iat":  1781348940,      // Issued-at — 60s before exp (matches expires_in: 60)
  "jti":  "trtcc:312ee6b5-...",  // JWT ID — unique per token, for replay prevention
  "iss":  "http://127.0.0.1:7080/realms/master",  // Issuer — your Keycloak realm URL
  "aud":  "account",       // Audience — intended recipient service
  "sub":  "47ea3794-558d-423c-934e-ea8c4d341805",  // Subject — Keycloak UUID of the service account
  "typ":  "Bearer",
  "azp":  "eazybank-callcenter-cc",  // Authorized party — the client that requested the token
  "acr":  "1",             // Authentication context class — level 1 (password equiv)
  "allowed-origins": ["/*"],  // CORS: all origins allowed (tighten in prod!)
  "realm_access": {
    "roles": [
      "default-roles-master",
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "account": {
      "roles": ["manage-account", "manage-account-links", "view-profile"]
    }
  },
  "scope": "openid email profile",
  "clientHost":    "172.17.0.1",   // Docker gateway IP of the requesting client
  "email_verified": false,
  "preferred_username": "service-account-eazybank-callcenter-cc",
  "clientAddress":  "172.17.0.1",
  "client_id":      "eazybank-callcenter-cc"
}

📋 Step 4 — Claims Quick Reference

Claim Value (this response) What it means
exp 1781349000 Token expiry as Unix epoch. Always validate this server-side.
iat 1781348940 Issued-at time. exp - iat = 60s matches expires_in.
iss …/realms/master Your API must verify this matches the expected Keycloak realm.
sub 47ea3794-… Keycloak's UUID for this service account principal.
azp eazybank-callcenter-cc The OAuth client that requested this token.
realm_access.roles ["default-roles-master", …] Realm-wide roles assigned to this client/user. Used for authorization decisions.
resource_access account → manage-account, … Per-client roles. Lets fine-grained API permissions live on specific resources.
preferred_username service-account-eazybank-… Auto-generated service account username. Confirms this is machine-to-machine auth.
alg (header) RS256 Asymmetric signing. Verify using Keycloak's public key — never the secret.

🚀 Step 5 — Using the Token in API Calls

Place the access_token in the HTTP Authorization header:

HTTPCalling a protected API
GET https://api.eazybank.internal/v1/accounts
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
    
JAVASpring Boot — auto-validate with Keycloak adapter
// application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:7080/realms/master

# Spring fetches Keycloak's JWKS, validates RS256 signature,
# checks exp/iss automatically — zero extra code.
    

🔄 Step 6 — The Full Auth Flow

1
Service sends Client Credentials request
POST to Keycloak with client_id + client_secret (or mTLS in production).
POST /realms/master/protocol/openid-connect/token
2
Keycloak validates credentials
Checks client secret, looks up service account, evaluates realm/client roles.
Signs JWT with RS256 private key (kid: nnniBrx…)
3
Returns token response (60s window)
access_token + id_token + metadata. Cache the token; re-fetch ~5s before expiry.
expires_in: 60
4
Service calls downstream API with Bearer token
Downstream validates JWT locally — no Keycloak call needed per request.
Authorization: Bearer eyJhbG…
5
Resource server verifies signature via JWKS
Fetches Keycloak's public key once (cached). Checks sig, exp, iss, aud claims.
GET /realms/master/protocol/openid-connect/certs

🛡️ Step 7 — Security Checklist

⚠️
Tighten allowed-origins in production The token shows "allowed-origins": ["/*"]. That's fine for local dev, but restrict to specific domains before going live.
⏱️
Short expiry (60s)
Already set tightly. Leaked tokens expire fast. Cache and refresh proactively.
🔑
RS256 (asymmetric)
Private key signs; public key verifies. Resource servers never need the secret.
🚫
No refresh token
Client Credentials grants can't rotate silently. Re-authenticate on expiry.
Validate on every request
Check exp, iss, aud, and signature. Don't trust a token just because it arrived.
🔒
Least privilege roles
Remove unused realm roles. This client has offline_access but probably doesn't need it.
🌐
Use HTTPS in prod
127.0.0.1:7080 is dev only. TLS everywhere when exposed externally.
Summary: what to remember The access_token is a self-contained RS256-signed JWT. It carries roles, expiry, and identity directly in its payload. Your resource server validates it offline using Keycloak's public JWKS — fast, scalable, no per-request auth calls. The 60-second window keeps things tight; just cache and re-fetch on expiry.

Written for developers building with Keycloak + Spring Boot / microservices. Token values shown are from a real local dev environment and contain no sensitive data.

Verify tokens yourself at jwt.io  ·  Keycloak docs at keycloak.org/docs

Comments

Popular posts from this blog

Deloitte Interview Problem Reverse run-length encoding & decoding of a given string

Observability & Monitoring through Loki,Promtail (Alloy),Prometheus,Micrometer in Grafana

🔧 What this demo covers End-to-end observability setup using Prometheus + Loki + Grafana Integration of Micrometer with Spring Boot for real-time metrics Log collection using Promtail / Alloy from application containers 📊 Metrics Monitoring (Prometheus) Scraping metrics from /actuator/prometheus endpoint JVM metrics: memory, threads, GC activity HTTP metrics: request count, latency, error rates Custom metrics via Micrometer 📜 Centralized Logging (Loki + Promtail) Aggregates logs from multiple microservices Label-based log filtering (fast & efficient) No heavy indexing → lightweight compared to ELK 📈 Visualization (Grafana Dashboards) Real-time dashboards for metrics & logs Correlate logs with metrics for faster debugging Pre-built + custom dashboards ⚙️ Architecture Flow Spring Boot app → exposes metrics via Micrometer Prometheus → scrapes & stores metrics Promtail/Alloy → collects logs → pushes to Loki...