A from-scratch, copy-paste guide that matches the new Keycloak UI (v25+). Uses port 8099 like the screenshots. If you prefer 8080, replace the port everywhere below.
- Docker installed
- Accounts for Google Cloud, GitHub, and Azure (Microsoft Entra)
- Decide your realm name (examples use
myrealm
)
docker run --name keycloak -p 8099:8080 \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.3.2 \
start-dev
Open admin console: http://localhost:8099/admin
- Top-left realm selector → Create realm
- Name:
myrealm
→ Create
- Clients → Create client
- Client ID:
node-api-client
- Client type: OpenID Connect → Save
- Capability config
- Client authentication: On (confidential)
- Standard flow: On
- Direct access grants: On (for quick curl tests)
- Save
- Credentials tab → copy Client secret
-
Google Cloud Console → APIs & Services → OAuth consent screen
- User type: External → fill minimum details → Save/Publish
-
Credentials → Create Credentials → OAuth client ID
-
Application type: Web
-
Authorized redirect URI:
http://localhost:8099/realms/myrealm/broker/google/endpoint
-
-
Create → copy Client ID and Client Secret
- Identity providers → Add provider → OpenID Connect v1.0
- Fill:
-
Redirect URI: (auto; verify it matches the Google redirect you registered)
-
Alias:
google
-
Display name:
Google
-
Use discovery endpoint: On
-
Discovery endpoint:
https://accounts.google.com/.well-known/openid-configuration
-
Client authentication: Client secret sent as post
-
Client ID: (from Google)
-
Client Secret: (from Google)
-
Save
-
-
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
-
Fill:
-
Homepage URL:
http://localhost:8099
-
Authorization callback URL:
http://localhost:8099/realms/myrealm/broker/github/endpoint
-
-
Create → copy Client ID and Client Secret
- Identity providers → Add provider → GitHub
- Fill:
- Redirect URI: (auto; verify it matches the GitHub callback)
- Alias:
github
- Display name:
GitHub
- Client ID / Client Secret: (from GitHub)
- Base URL:
https://github.com
- API URL:
https://api.github.com
- Save
- After save, ensure default scopes include:
read:user
user:email
-
Azure Portal → Microsoft Entra ID → App registrations → New registration
-
Supported account types: Accounts in any organizational directory and personal Microsoft accounts
-
Redirect URI (Web):
http://localhost:8099/realms/myrealm/broker/microsoft/endpoint
-
Register → copy Application (client) ID
-
Certificates & secrets → New client secret → copy the secret value
- Identity providers → Add provider → OpenID Connect v1.0
- Fill:
-
Alias:
microsoft
-
Display name:
Microsoft
-
Use discovery endpoint: On
-
Discovery endpoint:
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
-
Client authentication: Client secret sent as post
-
Client ID / Client Secret: (from Azure)
-
Save
-
Note: Microsoft photos are not in ID token by default; skip avatar for now or fetch via Graph API later.
Identity providers → select provider (e.g., Google) → Mappers → Create
- Email
- Mapper type: Attribute Importer
- Claim:
email
→ User attribute:email
- First name
- Mapper type: Attribute Importer
- Claim:
given_name
→ User attribute:firstName
- Last name
- Mapper type: Attribute Importer
- Claim:
family_name
→ User attribute:lastName
- Avatar
- Mapper type: Attribute Importer
- Claim:
picture
→ User attribute:avatar
- For each: Sync mode: Force
- Username
- Mapper type: Username Template Importer
- Template:
${CLAIM:login}
- Email
- Mapper type: Attribute Importer
- Claim:
email
→ User attribute:email
- Avatar
- Mapper type: Attribute Importer
- Claim:
avatar_url
→ User attribute:avatar
- Ensure default scopes include:
read:user
user:email
- For each Attribute Importer: Sync mode: Force
- Email → Attribute Importer: Claim
email
→ User attributeemail
- First name → Attribute Importer: Claim
given_name
→ User attributefirstName
- Last name → Attribute Importer: Claim
family_name
→ User attributelastName
- Set Sync mode: Force
Keycloak v25+ only shows attributes that exist in User profile.
- Realm settings → User profile → Attributes → Create attribute
- Name:
avatar
- Display name:
Avatar
- Type: String
- Permissions: Viewable by Admin and User
- Save
- Name:
After a social login, go to Users → (user) → Attributes to see the avatar URL.
- Realm settings → Login
- Identity Provider Redirect (optional) → On → Save
Test page:
- Open:
http://localhost:8099/realms/myrealm/account
- You should see Google / GitHub / Microsoft buttons
- Log in → check Users → (your user) → Attributes for
avatar
Install deps:
npm i express express-jwt jwks-rsa
Create app.js
:
const express = require('express');
const { expressjwt: jwt } = require('express-jwt');
const jwksRsa = require('jwks-rsa');
const app = express();
const port = 3000;
const checkJwt = jwt({
secret: jwksRsa.expressJwtSecret({
jwksUri: 'http://localhost:8099/realms/myrealm/protocol/openid-connect/certs',
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
}),
audience: 'node-api-client',
issuer: 'http://localhost:8099/realms/myrealm',
algorithms: ['RS256'],
});
app.get('/public', (_, res) => res.json({ ok: true }));
app.get('/protected', checkJwt, (req, res) => res.json({ ok: true, sub: req.auth.sub }));
app.listen(port, () => console.log(`http://localhost:${port}`));
Run and test protected endpoint with a bearer token from Keycloak.
Export (no users):
docker exec -it keycloak \
/opt/keycloak/bin/kc.sh export \
--dir /opt/keycloak/data/import \
--realm myrealm --users skip
docker cp keycloak:/opt/keycloak/data/import ./exports
# => ./exports/myrealm-realm.json
Import on startup:
docker run --name keycloak-prod -p 8080:8080 \
-v $(pwd)/exports:/opt/keycloak/data/import \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.3.2 \
start-dev --import-realm
- No “Attributes” tab: create the attribute in User profile (step 8) and re-login
invalid redirect_uri
: callback must match exactly (host, port, path)- GitHub email empty: add
user:email
scope and verify the email on the GitHub account - Google fields don’t auto-fill: ensure “Use discovery endpoint” is On and the discovery URL is exact
- Different port: if you change Keycloak port, update all IdP redirect URIs