A Keycloak authentication plugin for Vendure.
It adds Keycloak OAuth 2.0 / OpenID Connect sign-in to both the Admin API
(back-office users) and the Shop API (storefront customers), links Keycloak
identities to Vendure Users by email, and ships a dashboard extension with a
“Login with Keycloak” button, an OAuth callback page and identity-management
blocks on the Administrator and Customer detail pages.
The plugin declares the following peerDependencies, which must be present on
your Vendure project:
| Package | Version | Notes |
|---|---|---|
@vendure/core | ^3.6.0 | ships with any standard Vendure project |
@vendure/dashboard | ^3.6.0 | ships with any standard Vendure project |
jose | ^6.1.3 | must be installed explicitly — see below |
jose is used to verify the Keycloak id_token against the realm's JWKS. It
is not a Vendure dependency, so install it on the host project:
Without jose, token verification throws at authentication time.
Add KeycloakPlugin to the plugins array of your Vendure config:
The plugin appends its strategies to
authOptions.shopAuthenticationStrategy and
authOptions.adminAuthenticationStrategy rather than replacing them, so
Vendure's native email/password login keeps working alongside Keycloak.
For each client (admin and, if separate, shop) you need a confidential
client in the realm — the plugin performs a server-side authorization_code
exchange with client_secret:
https://<your-host>/dashboard/keycloak-callback
(the dashboard builds this from window.location.origin); for the storefront
it is whichever URI you pass to the Shop keycloakAuthConfig query.openid email profile. The realm must return
an id_token with an email claim — authentication is rejected without it.There is nothing extra to set up for the dashboard. The plugin ships a
dashboard extension declared through its dashboard slot, and the Vendure
dashboard Vite plugin discovers it automatically — simply registering
KeycloakPlugin.init({...}) in vendure-config.ts (step 1) is enough.
Rebuild the dashboard and you get:
/keycloak-callback route (unauthenticated) that completes the flow;Bundled translations: en, fr, de.
The UI honours Vendure permissions: the Administrator block requires
ReadAdministrator+UpdateAdministrator, the Customer block requiresReadCustomer+UpdateCustomer, and the Admin API is independently guarded.
None required. The plugin adds no entities and no custom fields — links are
stored in Vendure's built-in ExternalAuthenticationMethod table.
init() takes a KeycloakPluginOptions object. serverUrl, realm and
adminClient are required; everything else has a default.
| Option | Type | Default | Description |
|---|---|---|---|
serverUrl | string | — (required) | Keycloak server base URL, e.g. https://keycloak.example.com. A trailing slash is stripped; realm paths are derived from it. |
realm | string | — (required) | Keycloak realm name. All OIDC endpoints are built as {serverUrl}/realms/{realm}/protocol/openid-connect/…. |
adminClient | KeycloakClientConfig | — (required) | OAuth client used for Admin API authentication. |
shopClient | KeycloakClientConfig | falls back to adminClient | OAuth client used for Shop API authentication. Omit it to share a single client between both APIs. |
autoRegisterShopCustomers | boolean | true | On Shop login with an email unknown to Vendure, create a verified Customer + User and link the Keycloak identity. Set to false to require pre-registration instead. |
KeycloakClientConfig| Field | Type | Description |
|---|---|---|
clientId | string | OAuth client ID. Also used as the expected audience when verifying the id_token. |
clientSecret | string | Client secret of the confidential client, sent during the code→token exchange. |
redirectUri | string | Default redirect URI. Used as-is for the Admin flow; the Shop flow always sends its own (see Shop API). |
Full example:
Both strategies implement Vendure's AuthenticationStrategy and follow the
same five steps; they differ only in who is allowed in (see step 4).
Front-end (dashboard or storefront) Vendure Keycloak
│ │ │
1. ask for the │ query keycloakAuthConfig │ │
authorize URL ├─────────────────────────────────►│ │
│◄─── authorizationUrl ────────────┤ │
│ │ │
2. redirect user │──────────────────────────────────────────────────────────► │
(+ state) │ user signs in on Keycloak │
│◄── redirect back to redirectUri?code=…&state=… ─────────────┤
│ │ │
3. send the code │ mutation authenticate( │ │
│ input: { keycloak_shop: } ) │ |
├─────────────────────────────────►│ POST /token │
│ ├─────────────────────────►│
│ │◄── id_token ─────────────┤
│ │ │
4. server side │ │ verify id_token against │
│ │ the realm JWKS │
│ │ (issuer + audience) │
│ │ │
5. resolve user │◄── CurrentUser / error ──────────┤ link or create the │
│ │ Vendure User │
Step by step, server side:
POST {realm}/protocol/openid-connect/token with
grant_type=authorization_code, the client credentials and the redirect URI.
The redirect URI sent here must be byte-identical to the one used in step 2.id_token is verified with jose against the
realm JWKS (/protocol/openid-connect/certs), checking issuer (the realm
URL) and audience (the client ID). The JWKS keyset is fetched lazily and
cached for the process lifetime. No userinfo round-trip is needed.
Authentication is rejected if Keycloak returns no id_token (missing
openid scope) or if the token carries no email claim.sub claim is looked up in
ExternalAuthenticationMethod for this strategy. On a hit, that User is
returned immediately.Administrator.
Pre-registration is mandatory: an unknown email is refused with
No administrator found with email: …. Keycloak never creates
administrators, so realm membership alone grants no back-office access.Customer. If none matches,
the behaviour depends on autoRegisterShopCustomers: true (default)
creates a verified Customer + User from the given_name /
family_name claims; false refuses with
No customer found with email: ….ExternalAuthenticationMethod row is written with the
strategy name, externalIdentifier = the Keycloak sub, and
metadata.clientID = the client used. Subsequent logins short-circuit at
step 3.Any thrown error is logged under the KeycloakPlugin logger context and
surfaced to the caller as an InvalidCredentialsError message.
The strategy name is the key you pass inside the authenticate mutation input:
| API | Strategy name | Input type |
|---|---|---|
| Admin | keycloak_admin | KeycloakAuthInput { code: String!, redirectUri: String } |
| Shop | keycloak_shop | KeycloakAuthInput { code: String!, redirectUri: String! } |
On the Admin API redirectUri is optional and falls back to
adminClient.redirectUri. On the Shop API it is required: the storefront
chooses its own redirect URI at runtime, so it must send back the exact value
it used.
The returned URL already carries client_id, redirect_uri, response_type=code
and scope=openid email profile. Append your own state parameter before
redirecting, and verify it on the way back.
A minimal storefront callback handler:
No argument here — the Admin flow uses the server-configured
adminClient.redirectUri. Authentication then goes through
authenticate(input: { keycloak_admin: { code, redirectUri } }).
The bundled dashboard extension already implements this end to end (login
button → /dashboard/keycloak-callback → authenticate), including a
state value stored in sessionStorage for CSRF protection.
| Operation | Permission | Description |
|---|---|---|
keycloakExternalAuthMethods(administratorId: ID) | Authenticated | Keycloak identities linked to an administrator. Returns [] when administratorId is omitted. |
keycloakCustomerExternalAuthMethods(customerId: ID!) | ReadCustomer | Keycloak identities linked to a customer. |
deleteKeycloakExternalAuthMethod(administratorId: ID!, externalIdentifier: String!) | UpdateAdministrator | Unlink one identity from an administrator. Returns true when a row was deleted. |
deleteKeycloakCustomerExternalAuthMethod(customerId: ID!, externalIdentifier: String!) | UpdateCustomer | Unlink one identity from a customer. |
Each entry exposes externalIdentifier (the Keycloak sub) and clientID
(the OAuth client that performed the link, from the record's metadata).
Unlinking only removes the ExternalAuthenticationMethod row — the Vendure
Administrator / Customer and the Keycloak account are both left untouched.
The next Keycloak login re-links by email (subject to the step 4 rules above),
so unlinking is a re-binding tool, not a way to revoke access; to revoke, delete
or disable the Vendure user or the Keycloak account.
The package root exports the plugin and its types:
KeycloakService (OIDC endpoints, authorization URL, code exchange, token
verification) and KeycloakAdmin (read/delete external auth methods) are
exported from the plugin's Nest module, so they can be injected into your own
providers. They are not re-exported from the package root — import them from
their module path:
client_secret; it happens server-side in Vendure, and the secret never
reaches a browser. Keep it in an environment variable.Administrator with a matching email
must already exist, with its roles and permissions assigned in Vendure.state is enforced by the caller. The plugin returns a bare
authorization URL; the shipped dashboard adds a random state and validates
it on callback. If you build your own storefront flow, do the same.autoRegisterShopCustomers: false if you would rather gate
storefront access on pre-existing customer records.| Plugin | Vendure |
|---|---|
0.x.x | ^3.6.0 (dashboard extension), declared compatibility: ^3.0.0 |
Source code and contribution guidelines live at
gitlab.datasolution.fr/datasolution/vendure/plugins/keycloak.
See DEVELOPMENT.md for the contributor workflow, including
the per-developer “dev mode” that syncs this plugin's src/ into a host Vendure
project.