Keycloak for .NET
Key Points
- Keycloak is a free, Apache-licensed identity and access management (IAM) server, maintained by Red Hat. Self-hosted, full OIDC + OAuth2 + SAML, multi-tenant via realms, and stack-neutral — any OIDC client works against it.
- It's a serious peer to Entra ID, Auth0, Duende in 2026: feature-complete, battle-tested, mature admin UI, federation, fine-grained authorization, MFA, social logins, account self-service, brute-force protection.
- In .NET, integration is the standard
AddAuthentication().AddJwtBearer(...)pattern using Keycloak's OIDC discovery endpoint. No Keycloak-specific NuGet is required — Keycloak speaks the standards. - Realm = tenant boundary. Users, clients, roles, identity providers, and tokens are all scoped to a realm. Plan your realm strategy carefully — moving users between realms is painful.
- The "Keycloak vs Entra ID" question: pick Keycloak when you need self-hosting (regulated data, on-prem, sovereignty), cross-stack uniformity (mixed Java/Node/.NET), or want to escape per-MAU pricing. Pick Entra ID when you're already an M365 shop and Conditional Access matters.
- Keycloak is Java. That's an operational fact. You're running JVM containers, tuning heap settings, applying Quarkus configuration. Don't adopt Keycloak if you have no appetite for operating Java.
Concepts (deep dive)
What Keycloak is
Keycloak is a standalone server that issues tokens and verifies identities for your applications. It implements:
- OIDC (OpenID Connect) — the standard for browser-based login flows;
id_token,userinfo, discovery, JWKS. - OAuth 2.0 — Authorization Code + PKCE, Client Credentials, Device Code, Refresh Token, Token Revocation, Token Introspection — see OAuth2 & OIDC Flows.
- SAML 2.0 — for legacy enterprise SSO.
- Identity brokering — federate to Google, GitHub, Microsoft, LDAP/Active Directory, generic OIDC/SAML.
- User federation — keep users in an external LDAP/AD; Keycloak proxies authentication.
- Fine-grained authorization — UMA 2.0 / Authorization Services (role-based, attribute-based, policy-based).
┌─────────────────────────────────────────────────────────┐
│ Keycloak server │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ realm A │ │ realm B │ │ realm C (master) │ │
│ │ - users │ │ - users │ │ - admin only │ │
│ │ - clients│ │ - clients│ └──────────────────┘ │
│ │ - roles │ │ - roles │ │
│ │ - IdPs │ │ - IdPs │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
▲ ▲
│ OIDC │ admin REST API
│ │
┌──────────┐ ┌──────────────────┐
│ .NET app │ │ Terraform / IaC │
│ (client) │ │ (realm + client │
│ │ │ config as code) │
└──────────┘ └──────────────────┘
Realm: the tenant boundary
A realm is Keycloak's tenant boundary. Each realm has:
- Its own user database.
- Its own set of clients (registered applications).
- Its own roles, groups, identity providers, authentication flows.
- Its own signing keys (rotated independently).
The master realm is special — it manages Keycloak itself. Never put end-users in master. Production user-facing realms are always separate.
Realm strategy patterns:
| Pattern | When |
|---|---|
| One realm for the whole product | Single-tenant SaaS, internal apps. Simplest. |
| Realm per customer | Multi-tenant SaaS where each customer brings their own IdP federation, branding, and password policies. Heavyweight but flexible. |
| Realm per environment | Dev/staging/prod each on different Keycloak instances; usually paired with one of the above. |
| Single shared realm with custom claims | Multi-tenant via a tenant_id claim per user. Lighter than realm-per-customer; harder to give each tenant autonomy. |
Anti-pattern: starting with realm-per-customer "in case we need it" with no customers requesting that level of isolation. Migrate up when the need is real; collapse is much harder than split.
Client: the registered application
A client in Keycloak is an application registered to obtain tokens. Each client has:
- A client ID (public identifier).
- A client secret (for confidential clients).
- An access type:
public(SPA, mobile),confidential(server-side web app, service), orbearer-only(resource server that validates tokens but never initiates flows). - A list of valid redirect URIs and web origins (CORS).
- Optional service accounts (for the Client Credentials flow).
- A client scope mapping (which scopes/claims it can request).
- Optional fine-grained authorization (UMA-style policies).
Typical client layout for a single product:
realm: contoso-prod
├── client: contoso-web (confidential — ASP.NET Core BFF or MVC)
├── client: contoso-spa (public — Angular/React SPA)
├── client: contoso-mobile (public — iOS/Android)
└── client: contoso-api (bearer-only — Web API resource server)
The contoso-api is the audience; the others get tokens to call it.
Authentication flows
Keycloak ships with sensible default flows (Browser, Direct Grant, Reset Credentials, Registration), and they're customizable:
- Browser flow: cookie check → identity provider redirect → username/password (or alternative authenticator) → optional OTP/WebAuthn → set cookie.
- Direct grant (Resource Owner Password Credentials): deprecated; only for legacy CLI tools where there's no browser.
- Authenticators are pluggable: password, OTP, WebAuthn (passkeys), Recovery Code, conditional steps (e.g., MFA only from new IPs).
You can build flows in the admin UI, or define them as JSON and import via Terraform / kcadm.sh. Custom authenticators are written in Java as Keycloak SPIs — that's the dividing line where Keycloak stops being "configure a UI" and starts being "build a JVM extension."
How .NET integrates
Keycloak speaks standards, so .NET integration is identical to "any OIDC provider":
Resource server (Web API that validates JWTs)
// Program.cs in your ASP.NET Core Web API
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// OIDC discovery — Keycloak publishes this:
options.Authority = "https://keycloak.contoso.com/realms/contoso-prod";
// The token's `aud` claim must include this client ID:
options.Audience = "contoso-api";
// In dev only; production should always require HTTPS metadata:
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
options.TokenValidationParameters = new()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
// No need to set IssuerSigningKeys — JWKS is fetched from
// https://keycloak.contoso.com/realms/contoso-prod/protocol/openid-connect/certs
// and refreshed automatically.
ClockSkew = TimeSpan.FromSeconds(30),
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
That's the entire integration. There's no Keycloak SDK; the JWT middleware doesn't care who issued the token as long as the discovery document and JWKS resolve.
Confidential client (server-side web app with login)
builder.Services
.AddAuthentication(o =>
{
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://keycloak.contoso.com/realms/contoso-prod";
options.ClientId = "contoso-web";
options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"]; // from Key Vault / Secrets Manager
options.ResponseType = "code";
options.UsePkce = true; // always
options.SaveTokens = true; // keep the access + refresh tokens server-side
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
// Any custom Keycloak client scopes you need:
options.Scope.Add("orders.read");
options.TokenValidationParameters.NameClaimType = "preferred_username";
options.TokenValidationParameters.RoleClaimType = "realm_access.roles";
});
This is the standard ASP.NET Core OIDC pattern. The provider name in the configuration happens to be Keycloak, but AddOpenIdConnect doesn't know that.
Mapping Keycloak roles to ASP.NET Core claims
Keycloak embeds roles inside the access token under nested objects (realm_access.roles, resource_access.{client}.roles). ASP.NET Core's [Authorize(Roles = "...")] expects a flat role claim. The fix is a claims transformation:
public sealed class KeycloakRolesClaimsTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (principal.Identity is not ClaimsIdentity identity || !identity.IsAuthenticated)
return Task.FromResult(principal);
var realmAccess = identity.FindFirst("realm_access")?.Value;
if (!string.IsNullOrEmpty(realmAccess))
{
using var doc = JsonDocument.Parse(realmAccess);
if (doc.RootElement.TryGetProperty("roles", out var rolesEl) &&
rolesEl.ValueKind == JsonValueKind.Array)
{
foreach (var role in rolesEl.EnumerateArray())
identity.AddClaim(new Claim(ClaimTypes.Role, role.GetString()!));
}
}
return Task.FromResult(principal);
}
}
// Program.cs
builder.Services.AddTransient<IClaimsTransformation, KeycloakRolesClaimsTransformation>();
Now [Authorize(Roles = "admin")] works as expected.
Federation: bringing users from another IdP
Keycloak can federate to:
- Social logins (Google, GitHub, Microsoft personal, Facebook, Apple) — built-in.
- Enterprise OIDC/SAML providers (another Keycloak, Entra ID, Okta, Auth0) — for B2B customer-brings-own-IdP.
- LDAP / Active Directory — Keycloak proxies authentication; users stay in AD.
The federated user appears in Keycloak's realm as a normal user, with the external IdP as their identity source. Tokens issued to your app are Keycloak's tokens (signed by your realm's key); the external IdP is invisible to your app.
User federation vs identity brokering — the distinction
These are easy to confuse but different:
- User federation (LDAP/AD): Keycloak treats the external store as its user database. User credentials live in LDAP/AD; Keycloak validates against it at login. The user does not see a "log in with X" button — they log in to Keycloak normally; Keycloak verifies against LDAP.
- Identity brokering (OIDC/SAML/social): the user sees a "log in with X" button. Keycloak redirects to the external IdP; on return, Keycloak creates (or updates) a local user record linked to the external identity.
For a corporate environment with AD, user federation is usually right — users have a single login. For "users can sign in with their Google or Microsoft account," identity brokering is right.
Authorization Services (UMA 2.0)
Beyond simple roles, Keycloak supports fine-grained authorization via UMA 2.0 / Authorization Services:
- Resources (named objects in your app): "Order #123."
- Scopes on those resources:
read,update,cancel. - Policies: rule sets (role-based, attribute-based, time-based, script-based).
- Permissions: which scopes on which resources are granted to which policies.
You can either embed this evaluation server-side via Keycloak's Authorization Services API (the KeycloakPolicyEnforcer in Java, or hand-rolled REST calls in .NET), or pull the user's permissions into a custom claim at token issue time. Most teams don't need this; standard role-based authorization in [Authorize(Policy=...)] with ASP.NET Core's policy system is enough, and avoids tying authorization logic to the IdP.
Performance and ops realities
- Caching JWKS: Keycloak rotates signing keys. The .NET middleware caches the JWKS document and refreshes on
kidmiss. Don't disable this. A JWKS fetch on every request will saturate Keycloak. - Token lifetime tuning: Keycloak defaults are conservative — access tokens 5 min, refresh 30 min idle. For B2B APIs, 15–30 min access tokens are typical; longer if you accept the revocation latency.
- Database: Keycloak persists realms, users, and sessions in a relational DB (PostgreSQL preferred). Plan HA accordingly — the DB is a single point of failure for login.
- Sticky sessions: Keycloak supports stateless or stateful clustering. Stateless (Infinispan caches replicated) is the modern default.
- HTTPS termination: behind a reverse proxy (nginx, Application Gateway, Traefik). Configure
proxy=edgeand trust forwarded headers. - Heap: Keycloak's Quarkus-based distribution is leaner than the WildFly distribution; 1–2 GB heap covers most production realms.
- Backups: just the database. Keycloak itself is stateless once realms are configured (via Terraform).
Keycloak vs the alternatives
| Need | Best fit | Why |
|---|---|---|
| Microsoft shop, M365, Conditional Access matters | Entra ID | Native integration; risk-based policies |
| Best dev UX, social logins, ship in a week | Auth0 | Polished SDKs, Actions/Rules |
| Self-host, .NET-only stack, full control | Duende IdentityServer | .NET-native; commercial license |
| Self-host, free, .NET-only stack | OpenIddict | MIT; .NET API surface |
| Self-host, cross-stack (Java/Node/.NET), full features | Keycloak | Free; mature; battle-tested |
| User store + login pages only (no SSO across apps) | ASP.NET Core Identity | Not an IdP; user store + flows |
For the long-form comparison, see Identity Providers Comparison.
When Keycloak is the right answer
- Regulated / sovereignty: data must stay on-prem or in a specific region; no cloud IdP fits.
- Cross-stack: mix of Java, Node, Go, Python, .NET — one IdP serves all.
- Cost control at scale: per-MAU pricing of Auth0/Okta becomes a budget line item at scale; Keycloak's cost is operational, not per-user.
- Identity-provider escape: vendor lock-in is a stated concern.
- Federation-heavy B2B: customer-brings-own-IdP via OIDC/SAML; Keycloak handles brokering well.
When Keycloak is the wrong answer
- Your team has no Java operational experience and isn't willing to acquire it.
- You're a small team and a hosted IdP "just works." Operating Keycloak HA in production is a real job; weigh against per-MAU pricing.
- You're all-in on Microsoft and need Conditional Access / Verified ID / Entra External ID's branded sign-ups.
- You want full .NET control and licensing isn't a blocker — Duende fits better.
How it works under the hood
OIDC discovery — what Keycloak publishes
When ASP.NET Core's AddJwtBearer sets Authority = "https://keycloak.contoso.com/realms/contoso-prod", the middleware fetches:
That document returns endpoints (authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, end_session_endpoint, introspection_endpoint), supported algorithms, supported scopes, and supported grant types. The middleware caches it and uses jwks_uri to fetch the signing keys.
JWKS document shape:
{
"keys": [
{
"kid": "a1b2c3...",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "...base64-modulus...",
"e": "AQAB"
},
{
"kid": "previous-key-still-valid-during-rotation",
...
}
]
}
When a token arrives, JWT validation reads the kid from the JWT header, looks up the matching key in the cached JWKS, verifies the signature, and validates the standard claims (iss, aud, exp, nbf).
Key rotation
Keycloak rotates realm signing keys on a schedule (default: every 6 months) and exposes the new + previous keys via JWKS during the overlap window. The .NET middleware's automatic JWKS refresh handles this transparently — no app deploy needed.
If you ever need to force key rotation (suspected compromise), do it from the realm admin UI; clients pick up the new keys on the next JWKS refresh.
Realm vs client signing
Tokens are signed with the realm's key, not the client's. The kid resolves against the realm's JWKS. Multiple clients in the same realm share signing material — that's why two services in the same realm can validate each other's tokens trivially.
Sessions and refresh tokens
Keycloak tracks SSO sessions (browser-level) and client sessions (per registered app). When a user logs in via the browser flow, Keycloak sets a KEYCLOAK_SESSION cookie. Subsequent OIDC requests from other clients in the same realm find this cookie, skip the password prompt, and silently authenticate — that's SSO.
Refresh tokens are session-bound. Revoking the SSO session invalidates all refresh tokens issued under it. See Refresh Token Rotation & Revocation for rotation specifics that apply across IdPs including Keycloak.
Quarkus distribution (the 2026 default)
The modern Keycloak distribution (since 17.x) is built on Quarkus rather than WildFly. That means:
- Faster startup (seconds, not minutes).
- Smaller container images.
- Configuration via
kc.conf+ env vars (no XML). - Native AOT image build supported for even faster startup.
- Production mode via
kc.sh start— distinct fromstart-devwhich is unprotected.
The "WildFly distribution" still appears in older blog posts; don't follow those for new deployments.
Code: correct vs wrong
❌ Wrong: start-dev in production
start-dev disables HTTPS requirements, uses an in-memory dev DB, and runs the admin console without TLS. It exists for local development. Running it in production is a credential leak waiting to happen.
✅ Correct: production startup
KC_DB=postgres
KC_DB_URL=jdbc:postgresql://db.internal:5432/keycloak
KC_HOSTNAME=keycloak.contoso.com
KC_PROXY=edge
KC_HEALTH_ENABLED=true
KC_METRICS_ENABLED=true
kc.sh build # one-time, bakes in the providers/extensions
kc.sh start # production mode — requires HTTPS, real DB, real hostname
Run behind a TLS-terminating proxy; proxy=edge tells Keycloak the proxy handled TLS.
❌ Wrong: validating tokens against the master realm
master is for administering Keycloak. Production user-facing realms are separate. Validating against master mixes admin and end-user identities — a privilege escalation risk.
✅ Correct: validate against the product realm
options.Authority = "https://keycloak.contoso.com/realms/contoso-prod";
options.Audience = "contoso-api";
❌ Wrong: hard-coding signing keys
options.TokenValidationParameters.IssuerSigningKey = new RsaSecurityKey(...);
options.TokenValidationParameters.ValidateIssuerSigningKey = true;
// Now your app breaks the next time Keycloak rotates keys.
✅ Correct: let JWKS handle key rotation
options.Authority = "https://keycloak.contoso.com/realms/contoso-prod";
// Don't set IssuerSigningKey — let the middleware fetch JWKS automatically.
options.TokenValidationParameters.ValidateIssuerSigningKey = true;
❌ Wrong: realm-per-feature
Three realms means three sets of users, three signing keys, no shared SSO. You've made one product look like three.
✅ Correct: one realm, three clients
realm: contoso-prod
├── client: contoso-orders-api
├── client: contoso-customers-api
└── client: contoso-billing-api
One user, one session, three audiences.
❌ Wrong: configuring realms by clicking through the admin UI in prod
Configurations made by clicking aren't reproducible. The next person to set up a staging environment can't replicate your prod realm. Disasters look like "I'll just recreate it from memory."
✅ Correct: realms as code (Terraform)
resource "keycloak_realm" "prod" {
realm = "contoso-prod"
enabled = true
password_policy = "length(12) and upperCase(1) and digits(1) and notUsername"
ssl_required = "external"
}
resource "keycloak_openid_client" "api" {
realm_id = keycloak_realm.prod.id
client_id = "contoso-api"
access_type = "BEARER-ONLY"
enabled = true
}
resource "keycloak_openid_client" "web" {
realm_id = keycloak_realm.prod.id
client_id = "contoso-web"
access_type = "CONFIDENTIAL"
valid_redirect_uris = ["https://app.contoso.com/signin-oidc"]
pkce_code_challenge_method = "S256"
}
The mrparkers/keycloak Terraform provider covers realms, clients, roles, IdPs, authentication flows. Disasters become terraform apply.
Design patterns for this topic
Pattern 1 — "One realm per product; one client per service"
- Intent: SSO across the product without inflating realm count.
Pattern 2 — "Bearer-only client for resource servers"
- Intent: APIs validate tokens; never initiate flows.
Pattern 3 — "Realm-as-code via Terraform"
- Intent: reproducible environments; no admin-UI snowflakes.
Pattern 4 — "Federation for B2B; identity brokering for social"
- Intent: customer's IdP integrates without exposing internal users; social logins don't require a separate account.
Pattern 5 — "Claims transformation for nested roles"
- Intent: flatten Keycloak's
realm_access.rolesintoClaimTypes.Roleso ASP.NET Core authorization works without custom policies.
Pattern 6 — "Trust JWKS; never hard-code keys"
- Intent: survive key rotation without app redeploys.
Pros & cons / trade-offs
| Aspect | Pros | Cons |
|---|---|---|
| Keycloak | Free; full OIDC/OAuth2/SAML; mature; federation; multi-realm; standards-compliant | Java ops; DB dependency; you operate it |
| vs Entra ID | No per-user cost; self-host; full control | No Conditional Access; no Verified ID; you do MFA/security yourself |
| vs Auth0 | No per-MAU pricing; on-prem | Worse dev UX; you operate it |
| vs Duende | Free; cross-stack; richer admin UI | Java (not .NET); separate ops |
| vs OpenIddict | Full standalone server; admin UI; federation | Heavier; not embedded in your .NET app |
When to use / when to avoid
- Use Keycloak when self-hosting is a hard requirement (regulatory, sovereignty, on-prem).
- Use Keycloak for cross-stack environments where Duende/OpenIddict are awkward for the non-.NET services.
- Use Keycloak when Auth0/Okta MAU pricing becomes a budget problem at scale.
- Avoid Keycloak if the team has no Java operational appetite.
- Avoid Keycloak for small Microsoft-shop apps — Entra ID is cheaper to operate.
- Avoid hand-rolled OAuth/OIDC in general — Keycloak (or any of the listed alternatives) covers it.
Interview Q&A
Q1. What is Keycloak in one sentence? A free, self-hosted, Apache-licensed identity server providing OIDC, OAuth 2.0, and SAML 2.0, with multi-tenant realms, federation, MFA, and an admin UI — maintained by Red Hat.
Q2. How does a .NET app integrate with Keycloak? Standard AddAuthentication().AddJwtBearer() with Authority pointing at the realm's OIDC discovery URL. No Keycloak-specific SDK; the JWT middleware fetches the discovery document and JWKS automatically.
Q3. What's a realm? A tenant boundary inside Keycloak. Each realm has its own users, clients, roles, signing keys, and identity providers. The master realm administers Keycloak itself; production user-facing realms are separate.
Q4. What's the difference between a client and a realm? Realm = the tenant (users live here, signing keys live here). Client = a registered application within the realm that requests tokens.
Q5. What's "bearer-only" mean? A client access type for APIs that validate incoming JWTs but never initiate login flows. They have no client secret to use in code-grant flows; they just verify tokens.
Q6. How do Keycloak roles map to ASP.NET Core [Authorize(Roles = "...")]? Keycloak places roles inside nested objects (realm_access.roles, resource_access.{client}.roles). ASP.NET Core expects a flat role claim. Use an IClaimsTransformation to flatten them.
Q7. When pick Keycloak over Entra ID? When self-hosting is a hard requirement (regulated, on-prem, data sovereignty), when the stack is polyglot, or when avoiding per-user IdP pricing matters. Entra ID wins in Microsoft-centric shops with Conditional Access requirements.
Q8. How does Keycloak handle key rotation? The realm rotates signing keys on a schedule; both old and new keys are published in JWKS during the overlap window. JWT middleware in .NET refreshes JWKS automatically when an unknown kid arrives, so apps don't need a redeploy.
Q9. What's user federation vs identity brokering? Federation: external store (LDAP/AD) is the user database; Keycloak proxies authentication. Brokering: external IdP (Google, Okta, another Keycloak) is a "log in with X" option; Keycloak creates a local user record on first login.
Q10. How do you manage Keycloak realms in production? As code — Terraform with the mrparkers/keycloak provider, or import/export of realm JSON. Clicking through the admin UI in prod creates snowflakes that can't be reproduced in staging.
Q11. Is Keycloak suitable for a five-engineer .NET-only team? Usually not. The operational overhead of running a Java IdP plus its database in HA outweighs the cost savings vs Entra ID / Auth0 free tier / OpenIddict embedded in the app.
Q12. What's UMA 2.0 in Keycloak? User-Managed Access — Keycloak's fine-grained authorization layer (resources × scopes × policies × permissions). Most teams don't need it; standard ASP.NET Core authorization policies cover the typical case.
Gotchas / common mistakes
- ⚠️ Running
start-devin production — disables TLS, uses dev DB, exposes admin console. Usestart. - ⚠️ Putting end-users in the
masterrealm — admin and end-user identities mix; privilege escalation risk. - ⚠️ Validating against
masterinstead of the product realm — same hazard from the client side. - ⚠️ Hard-coding signing keys — break on the next key rotation. Trust JWKS.
- ⚠️ Disabling HTTPS metadata in production —
RequireHttpsMetadata = falseis a dev-only setting. - ⚠️ Realm-per-feature instead of client-per-feature — breaks SSO; inflates ops.
- ⚠️ Configuring realms via admin-UI clicks — not reproducible; use Terraform /
kcadm.sh. - ⚠️ No HA story for the Keycloak DB — login is now a single point of failure for the entire product.
- ⚠️ Missing claims transformation for nested roles —
[Authorize(Roles = "admin")]silently fails because the role claim isn't where ASP.NET Core looks. - ⚠️ Ignoring the WildFly → Quarkus migration — old blog posts assume WildFly; the modern distribution is Quarkus.
- ⚠️ Custom Java SPIs for things that could be a claims transformer — every Java extension is now part of your build/deploy.
Further reading
- Keycloak Documentation
- Keycloak Server Administration Guide
mrparkers/keycloakTerraform provider- ASP.NET Core JwtBearer authentication
- OAuth2 & OIDC Flows — the protocols Keycloak implements
- Identity Providers Comparison — Keycloak's place among alternatives
- Refresh Token Rotation & Revocation — rotation strategies that apply to Keycloak-issued refresh tokens