Backstage authenticates against Keycloak using its generic OIDC auth provider (auth.providers.oidc). You register Backstage as a confidential OIDC client in your Keycloak realm, point Backstage’s app-config.yaml at the realm’s metadata URL, and configure a sign-in resolver that maps the incoming Keycloak token to a Backstage catalog User entity. That last step, the resolver, is what most tutorials skip, and it is the reason most teams hit a “user not found” error on first login and spend hours confused about why Keycloak succeeded while Backstage rejected the session.
Backstage has become the default internal developer portal for platform engineering teams, but its authentication story is still one of the rougher edges. The Backstage authentication documentation covers the available providers, yet the specific mechanics of wiring Keycloak as an OIDC provider, and especially of making the sign-in resolver actually work, are scattered across GitHub issues and community posts. This guide consolidates everything into one linear walkthrough you can follow from a fresh Keycloak realm to a working Backstage login.
If you want a broader grounding in OIDC concepts before diving in, the post OpenID Connect explained for developers covers the protocol fundamentals. For general SSO implementation patterns, see the SSO implementation guide for developers.
The walkthrough assumes Keycloak 26.x and a recent Backstage release (v1.30+). Configuration syntax has evolved across Backstage major versions, notes call out places where you should verify against your installed version.
Step 1: Create the Keycloak Client
Log in to the Keycloak Admin Console and navigate to your realm. If you are building this for the first time, create a dedicated realm (for example, platform) rather than reusing the master realm.
Inside the realm, go to Clients and click Create client.
General settings
- Client type:
OpenID Connect - Client ID:
backstage(or a name that matches your org’s convention; you will reference this inapp-config.yaml)
Capability config
- Client authentication: On (this makes the client confidential, required because Backstage’s OIDC provider uses the authorization code flow with a client secret)
- Standard flow: On
- Direct access grants: Off (Backstage does not use the resource owner password flow)
Login settings
Set the redirect URI to:
http://localhost:7007/api/auth/oidc/handler/frame
For staging or production, add the corresponding URI, for example:
https://backstage.example.com/api/auth/oidc/handler/frame
Keycloak validates the redirect_uri on every authorization request. If Backstage sends a URI that does not exactly match one of the entries here, including trailing slashes, the authorization request will be rejected with invalid_redirect_uri. Add both the local development URI and all environment URIs upfront.
Web origins: set to http://localhost:7007 for development (and your production origin alongside it). This controls the Keycloak-side CORS header on token endpoint responses.
For a detailed explanation of CORS configuration in the context of OIDC clients, see configuring CORS with your Keycloak OIDC client.
Client secret
After saving, go to the Credentials tab and copy the client secret. You will need it in the next step.
Optional: Add a dedicated mapper for preferred_username
Backstage’s sign-in resolver often matches on preferred_username or email. By default Keycloak includes both in the ID token. If you want to map a custom attribute, go to Client scopes > backstage-dedicated > Add mapper > By configuration and select User Attribute.
Step 2: Configure app-config.yaml
Open your Backstage repository and locate app-config.yaml (or app-config.local.yaml for local overrides not committed to version control).
Add the following block under auth:
auth:
environment: development
providers:
oidc:
development:
metadataUrl: https://${KEYCLOAK_HOST}/realms/${KEYCLOAK_REALM}/.well-known/openid-configuration
clientId: backstage
clientSecret: ${KEYCLOAK_CLIENT_SECRET}
scope: 'openid profile email'
prompt: auto
Key points about each field:
metadataUrl: Keycloak’s OIDC discovery endpoint. The path is always/.well-known/openid-configurationunder the realm URL. For a realm namedplatformonkeycloak.example.com, this ishttps://keycloak.example.com/realms/platform/.well-known/openid-configuration. Backstage fetches this URL at startup to discover the authorization, token, and userinfo endpoints automatically.clientId: Must match the Client ID you set in Keycloak exactly.clientSecret: Never commit secrets to version control. Use environment variable substitution (${VAR}) or a secrets manager. In production, inject via Kubernetes secrets or a tool like HashiCorp Vault, see Keycloak and HashiCorp Vault OIDC SSO for patterns that apply to secrets management alongside Keycloak.scope:openid profile emailis the minimum required.openidtriggers the ID token;profileincludespreferred_username,given_name, andfamily_name;emailincludes the email address. If you want group membership claims, add a custom scope after configuring it in Keycloak (covered in the group mapping section below).prompt:autolets Keycloak decide whether to show the login page or use an existing session. Useloginif you want to force a fresh authentication on every session.
Wiring the Sign-In Page
In your Backstage packages/app/src/App.tsx, add the OIDC provider to the sign-in page configuration. The exact API has changed across Backstage versions. In current releases, you import from @backstage/core-components and use the SignInPage component with a providers list. Consult the Backstage sign-in page documentation for the current API shape, the principle is that you declare oidc as a provider and Backstage renders a “Sign in with SSO” button that redirects to Keycloak.
Step 3: Configure the Sign-In Resolver (The Critical Step)
This is where most integrations break down. Keycloak can successfully authenticate the user, issue a token, and return the user to Backstage, and then Backstage throws a “user not found” error. Understanding why requires understanding what Backstage does after it receives the OIDC token.
What the resolver does: After Backstage validates the ID token from Keycloak, it needs to link the identity in that token to a User entity in the Backstage software catalog. The software catalog is Backstage’s database of components, APIs, and people. If there is no User entity matching the logged-in user, Backstage considers the authentication incomplete and shows an error.
The resolver is the function that performs this mapping. It receives claims from the OIDC token (such as email or preferred_username) and returns a Backstage EntityRef pointing to the corresponding User entity.
Where to Configure the Resolver
In current Backstage (roughly v1.28 and later), authentication is handled by the new backend system. The resolver is configured in the auth backend module, consult the @backstage/plugin-auth-backend-module-oidc package for the current exported factory name and resolver options. Do not rely on examples from 2022-era Backstage posts; they use the old backend system.
The resolver options typically let you choose a built-in strategy:
emailMatchingUserEntityProfileEmail: Looks up a User entity whosespec.profile.emailmatches theemailclaim in the OIDC token. This is the most common choice when your catalog is populated from an HR system or directory that uses the same email addresses as your Keycloak users.emailLocalPartMatchingUserEntityName: Uses the local part of the email (before@) as the entity name.preferredUsernameMatchingUserEntityName: Matches thepreferred_usernameclaim to the entitymetadata.name. Useful when usernames rather than email addresses are the canonical identifier.
The “User Not Found in Catalog” Pitfall
When you enable a catalog-matching resolver, every user who logs in with Keycloak must have a corresponding User entity in the Backstage catalog. If the entity does not exist, login fails.
There are two approaches:
Option A: Populate the catalog with User entities (recommended). Ingest users from your directory using a catalog provider (for example, Microsoft Graph for Azure AD users, or a custom provider reading from the Keycloak Admin API). Each ingested user produces a kind: User entity with the matching email in spec.profile.email.
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: alice
namespace: default
spec:
profile:
email: alice@example.com
displayName: Alice Smith
memberOf: []
Option B: Use a permissive resolver. Backstage provides a built-in option (check current docs for the exact name) that creates a transient identity from token claims without a catalog entity. Useful during initial setup, but ownership features and permissions that depend on catalog identity will not work.
Start with Option B to unblock login, then migrate to Option A as the catalog fills in.
Step 4: Map Keycloak Groups to Backstage (Optional)
Backstage’s permissions framework can evaluate group membership. If you want Backstage to know which Keycloak groups a user belongs to, for example, to gate access to certain catalog entities or plugins, you need to surface group claims in the OIDC token and map them in the resolver.
Add a Group Membership Mapper in Keycloak
In Keycloak, navigate to your backstage client, then to Client scopes > backstage-dedicated > Add mapper > By configuration > Group Membership.
Configure the mapper:
- Name:
groups - Token claim name:
groups - Full group path: Off (so the claim contains
engineeringrather than/platform/engineering) - Add to ID token: On
- Add to access token: On
After saving, a user who is a member of a Keycloak group named platform-admins will have groups: ["platform-admins"] in their ID token.
Consume Groups in Backstage
In your sign-in resolver, you can read the groups claim from the token result and use it to populate the ownershipEntityRefs returned alongside the user reference. This tells Backstage which Group entities the user is a member of, enabling ownership-based permissions and catalog filtering.
For deep control over Keycloak client scopes and role claims, the post Keycloak client scopes vs roles explained covers when to use scopes, realm roles, and client roles and how the claims differ in tokens.
Backstage Group Entities
For group membership to function in Backstage’s permissions system, the groups must also exist as kind: Group entities in the catalog, and User entities must reference them via spec.memberOf. The Keycloak groups claim gives you the runtime membership; the catalog entities give Backstage the graph it uses for ownership resolution.
Step 5: Production Configuration
The development setup uses http://localhost:7007. For production, apply these changes:
Redirect URI: Add your production URL to the Keycloak client’s valid redirect URIs:
https://backstage.example.com/api/auth/oidc/handler/frame
Web origins: Add https://backstage.example.com.
Secrets: Do not put the client secret in app-config.yaml in plain text. Use environment variable substitution (${KEYCLOAK_CLIENT_SECRET}) and inject the value at runtime via Kubernetes secrets, AWS Secrets Manager, or Vault.
TLS: Keycloak must be served over HTTPS in production. Backstage’s OIDC provider will refuse to fetch a metadata URL that begins with http:// in non-development environments.
app-config.production.yaml override:
auth:
environment: production
providers:
oidc:
production:
metadataUrl: https://keycloak.example.com/realms/platform/.well-known/openid-configuration
clientId: backstage
clientSecret: ${KEYCLOAK_CLIENT_SECRET}
scope: 'openid profile email'
prompt: auto
The production key under oidc must match auth.environment: production. Backstage selects the config block whose key matches the current environment.
Troubleshooting
“User not found in catalog” after successful Keycloak login: The resolver ran but found no matching User entity. Verify that (a) the email or username in the Keycloak token exactly matches the value in the catalog User entity’s spec.profile.email or metadata.name, (b) the User entity is ingested into the catalog, search the Backstage catalog UI to confirm, and (c) you are using the correct resolver strategy for your data.
invalid_redirect_uri from Keycloak: The redirect URI Backstage sent does not match any entry in the client’s valid redirect URIs list. Copy the exact URI from the browser network tab and add it to Keycloak.
Backstage cannot fetch the metadata URL at startup: The Keycloak host must be reachable from the Backstage backend process, not just the browser. In containerized environments, localhost inside the container is not the same as localhost on your workstation, use the internal service DNS name.
invalid_client from Keycloak token endpoint: The client secret is wrong or the client is not configured as confidential. Verify on the Keycloak Credentials tab.
Groups claim is missing from the token: Verify the Group Membership mapper is on the backstage-dedicated client scope with “Add to ID token” enabled.
Session expires too quickly: Keycloak’s default access token lifetime is short (often 5 minutes). Go to Realm settings > Tokens and increase SSO Session Idle and SSO Session Max to values appropriate for a developer portal (8-12 hours is common).
Frequently Asked Questions
How do I set up Backstage SSO with Keycloak?
Register Backstage as a confidential OIDC client in Keycloak with the redirect URI https://{backstage-host}/api/auth/oidc/handler/frame. In app-config.yaml, add an auth.providers.oidc block with metadataUrl pointing to https://{keycloak-host}/realms/{realm}/.well-known/openid-configuration. Then configure a sign-in resolver in the auth backend module that maps token claims to a catalog User entity.
Why does Backstage say my user is not found after Keycloak login?
Backstage’s sign-in resolver requires a matching User entity in the software catalog. If none exists, authentication fails with “user not found”, even though Keycloak authenticated successfully. The fix is to populate the catalog with User entities whose email or name matches the Keycloak token claims, or use a permissive resolver that allows sign-in without a catalog entry during initial setup.
How do I map Keycloak groups to Backstage?
Add a Group Membership mapper to the Backstage client’s dedicated scope in Keycloak with token claim name groups. In the sign-in resolver, read the claim and populate ownershipEntityRefs with the corresponding Group entity refs. You also need matching kind: Group entities in the Backstage catalog with User entities referencing them via spec.memberOf.
Can I use Keycloak roles instead of groups for Backstage permissions?
Yes. Add a Realm Role or Client Role mapper to the Backstage client scope. In the resolver, consume the roles claim and map it to Group entity refs, or implement a custom permission policy that reads the raw roles directly. For a detailed breakdown of roles versus scopes in Keycloak tokens, see Keycloak client scopes vs roles explained.
Summary
Integrating Keycloak SSO into Backstage involves three distinct layers that must all be configured correctly:
- Keycloak client: Confidential, standard flow, with the exact Backstage redirect URI registered and appropriate mappers for email, username, and optionally groups.
app-config.yamlOIDC provider block:metadataUrlpointing to the realm discovery endpoint,clientId,clientSecretfrom the environment, and the right scope.- Sign-in resolver: The bridge between the Keycloak token and the Backstage catalog. Most failures happen here, not in steps 1 or 2.
The resolver requirement is not a quirk, it reflects Backstage’s design philosophy that authentication and catalog identity are coupled. Once your catalog is populated with User entities that match your Keycloak users, the integration is stable and requires minimal maintenance.
If managing your own Keycloak server is adding overhead to this kind of platform engineering work, Skycloak provides managed Keycloak hosting with production-ready configuration, automatic upgrades, and the reliability SLA your internal tools deserve.