Minimal, reproducible example for Keycloak + React.
- Screenshots
- Why
- Setup
- Config
- Links
- Scenarios
- High-level summary of common flows
- Seeded data
- Sharing
- Disclaimers
- History
- Contributing
I often see questions in the Keycloak forums on how to use it with React. On the other hand, I often see questions in the react-oidc-context repo on how to use it with Keycloak.
So, I thought it'd be cool to make a little project that glues these tools together, in hopes that others may play with it and get ideas for their own implementations.
The noblest pleasure is the joy of understanding. - Leonardo da Vinci
In one terminal, run the Postgres database, Keycloak server, Mailhog server, Express API, and React app via Docker Compose.
OPTIONALLY, in another terminal, you may run the React app outside of Docker Compose.
-
Install Docker
-
Copy file
.env.sample
to file.env
cp .env.sample .env
-
Build images
docker compose build
-
Run containers
docker compose up
-
Wait until you see a message from the Keycloak server like this: Running the server in development mode. DO NOT use this configuration in production.
-
See links for username and password
Optional steps
-
Stop the React app container
docker compose stop react
-
Install Node.js
-
Change to
react
foldercd react
-
Create file
.env.local
with contentsVITE_PORT=5173 VITE_AUTHORITY=http://localhost:8080/realms/master VITE_CLIENT_ID=react VITE_API_BASE_URL=http://localhost:5174
-
Install packages
npm install
-
Start dev server
npm run dev
-
See links for username and password
The Docker Compose config should work as-is. If you need to customize it, then edit file .env
-
Link: http://localhost:5173
-
Username:
-
Password:
juggle-prance-shallot-wireless-outlet
- Link: http://localhost:8080/realms/master/account/
- Credentials: same as React app
- Link: http://localhost:8080/admin/master/console/
- Credentials: same as React app
- Link: http://localhost:8025
Here are some scenarios you can play with:
- Login to the React app
- Copy-paste the link into another browser tab
- Notice how you're automatically logged in
- Logout in one of the browser tabs
- Notice how you're automatically logged out of both browser tabs
- Login to the React app
- Open your browser DevTools, go to the Network tab, and filter requests by Fetch/XHR
- Go to the Playground page in the React app
- Notice how the request without a Bearer token gets a 401, but the request with a Bearer token gets a 200
- Go to the login page
- Click Register
- Fill out the fields for a fake user
- Click Register
- Open the Mailhog UI
- Click the email from
[email protected]
with subject Verify email - Click the Link to e-mail address verification
- Notice how you're automatically logged into the React app with your newly created user
Thankfully the react-oidc-context and oidc-client-ts libraries do the heavy lifting for us.
This flow is known as Authorization Code Grant with Proof Key for Code Exchange (PKCE).
You can see this happen for yourself by doing: Open your browser DevTools, go to the Network tab, check Preserve log, and filter requests by Doc and Fetch/XHR.
I've copied the request query params and form data directly from DevTools for education purposes. Keep in mind that these values will likely be different for you.
-
Go to the React app
-
Request URL:
GET http://localhost:5173/
-
-
The OpenID config is fetched
-
Request URL:
GET http://localhost:8080/realms/master/.well-known/openid-configuration
-
Response body: Omitted for brevity. Go to the request URL to see it
-
-
You're not logged in, so you're redirected to the Keycloak login page
-
Request URL:
GET http://localhost:8080/realms/master/protocol/openid-connect/auth
-
Request query params:
client_id: react redirect_uri: http://localhost:5173/ response_type: code scope: openid state: 391fb773c982414baed2250583113efc code_challenge: K-Hp16LIAH8r6QSjOmfYh7zJtySFjWFRsVN-4s72st0 code_challenge_method: S256
-
-
Submit your username and password
-
Request URL:
POST http://localhost:8080/realms/master/login-actions/authenticate
-
Request query params:
session_code: NSBRN5i4WCHlUNM-Fr_7sVGv_luCqlcuj-dYvRgPGbg execution: 0c75cc68-1058-49a7-bdaa-d0f8ec69c1b8 client_id: react tab_id: 1x0hcfo9RVk
-
Request form data:
username: [email protected] password: juggle-prance-shallot-wireless-outlet credentialId:
-
-
On success, you're redirected to the React app
-
Request URL:
GET http://localhost:5173/
-
Request query params:
state: 391fb773c982414baed2250583113efc session_state: 683043bb-2209-47ff-b0a5-2c0197ab2507 iss: http://localhost:8080/realms/master code: 44891f5f-be56-4fc4-b970-8f1dd0d88fbe.683043bb-2209-47ff-b0a5-2c0197ab2507.acc4f3dc-25c9-4716-bfa5-cde9f19c8c32
-
-
The token is fetched
-
Request URL:
GET http://localhost:8080/realms/master/protocol/openid-connect/token
-
Request form data:
grant_type: authorization_code redirect_uri: http://localhost:5173/ code: 44891f5f-be56-4fc4-b970-8f1dd0d88fbe.683043bb-2209-47ff-b0a5-2c0197ab2507.acc4f3dc-25c9-4716-bfa5-cde9f19c8c32 code_verifier: d8df056c73d6437fa810e3a85f1784761ac3a8001b734dc593fef211ffdba501c0c82b6abeab4b989eb03fae34ad36c4 client_id: react
-
Response body:
access_token
,refresh_token
, andid_token
values truncated for brevity{ "access_token": "eyJ...", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJ...", "token_type": "Bearer", "id_token": "eyJ...", "not-before-policy": 0, "session_state": "683043bb-2209-47ff-b0a5-2c0197ab2507", "scope": "openid email profile" }
-
-
Go to the React app, login, then click the Playground page
-
An API request is made, which the Vite dev server proxies to the correct location on the API server
-
Request URL:
GET http://localhost:5173/api/payload
-
Request headers: Only relevant headers are included. The access token value is truncated for brevity
authorization: Bearer eyJ...
-
-
The API server receives the request. The token is parsed from the
authorization
header -
The API server requests the JSON web key set
-
Request URL:
GET http://localhost:8080/realms/master/protocol/openid-connect/certs
-
Response body: Omitted for brevity. Go to the request URL to see it
-
-
The jose library consumes the JSON web key set, then uses it to verify the token
-
The API server responds to the React app
The db/init
folder contains SQL which is copied into the Postgres image and runs on container startup.
I didn't write this SQL by hand. Instead, I customized the master
realm a tad, then dumped the data. See compose.jobs.yml
Create a public client with:
- General settings > set Client ID to
react
- Access settings > set Valid redirect URIs to
http://localhost:5173/*
- Access settings > set Valid post logout redirect URIs to
http://localhost:5173/*
- Access settings > set Web origins to
*
- Authentication flow > check Standard flow
- Authentication flow > check Direct access grants
- Logout settings > check Front channel logout
- Logout settings > check Backchannel logout session required
- Realm settings > Login > check User registration
- Realm settings > Login > check Forgot password
- Realm settings > Login > check Remember me
- Realm settings > Login > check Email as username
- Realm settings > Login > check Login with email
- Realm settings > Login > check Verify email
- Realm settings > Tokens > set Access Token Lifespan to
5 mins
- Realm settings > Email > set From to
[email protected]
- Realm settings > Email > set Host to
mailhog
- Realm settings > Email > set Port to
1025
Places where this project has been shared.
- The
Dockerfile
for each service is optimized for local development mode. DO NOT use this configuration in production - For convenience, the admin user is also used for regular app logins. In production, the admin account would be locked down, and you'd have regular, non-admin users
This repo originally lived at zach-betz-hln/mre-keycloak-react. For context, see this issue.
-
Create a branch from
main
, or a fork of this repo -
Make your changes
-
Run through the Setup steps in this doc from scratch and confirm everything works
-
Run the dump job
docker compose -f compose.yml -f compose.jobs.yml run dump
-
Increment the semantic version.
<semver>
should be one of:major
|minor
|patch
npm --no-git-tag-version version <semver>
-
Update
CHANGELOG.md
with a new section -
PR your changes to be reviewed and merged