
Adding Auth0 Authentication to Backstage: A Complete Guide
December 26, 2025·8 min read
Backstage's default guest authentication works well for development, but it's not designed for production use. The dangerouslyAllowOutsideDevelopment: true flag exists as a workaround, but as the name suggests, it's not ideal for real deployments. Auth0's free tier offers a straightforward way to add proper identity management without the complexity of enterprise SSO solutions.
What we're building:
- Auth0 OAuth integration with Backstage
- User entity resolution (matching Auth0 emails to catalog users)
- Production-ready Kubernetes deployment with Azure Key Vault secrets
- Guest fallback only in development mode
Prerequisites
- Backstage app (new backend system)
- Auth0 account (free tier works)
- For production: Kubernetes cluster with CSI Secret Store Driver
Part 1: Auth0 Setup
Create an Auth0 Application
- Log into Auth0 Dashboard
- Navigate to Applications > Applications > Create Application
- Select Regular Web Application
- Name it something like "Backstage"
Configure Callback URLs
In your Auth0 application settings, add:
Allowed Callback URLs:
http://localhost:7007/api/auth/auth0/handler/frame
https://backstage.yourdomain.com/api/auth/auth0/handler/frameAllowed Logout URLs:
http://localhost:3000
https://backstage.yourdomain.comAllowed Web Origins:
http://localhost:3000
https://backstage.yourdomain.comNote Your Credentials
From the Auth0 application settings, grab:
- Domain (e.g.,
dev-xxxxx.us.auth0.com) - Client ID
- Client Secret
Part 2: Backend Configuration
Install the Auth0 Provider Module
cd packages/backend
yarn add @backstage/plugin-auth-backend-module-auth0-providerRegister the Module
In packages/backend/src/index.ts, add the Auth0 module:
import { createBackend } from '@backstage/backend-defaults';
const backend = createBackend();
// Core plugins
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend'));
// Auth plugins
backend.add(import('@backstage/plugin-auth-backend'));
// Auth0 provider - production authentication
backend.add(import('@backstage/plugin-auth-backend-module-auth0-provider'));
// Guest provider - development convenience only
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
// ... other plugins
backend.start();Configure app-config.yaml
Add the Auth0 configuration:
auth:
environment: development
session:
secret: ${AUTH_SESSION_SECRET}
providers:
# Guest provider for local development only
guest: {}
# Auth0 OAuth provider
auth0:
development:
clientId: ${AUTH_AUTH0_CLIENT_ID}
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
domain: ${AUTH_AUTH0_DOMAIN}
signIn:
resolvers:
# Match Auth0 email with Backstage User entity profile email
- resolver: emailMatchingUserEntityProfileEmail
# Fallback: Match email local part (before @) with User entity name
- resolver: emailLocalPartMatchingUserEntityNameThe sign-in resolvers determine how Auth0 users get mapped to Backstage User entities. The first resolver that finds a match is used.
Part 3: Frontend Configuration
Create the Auth0 API Reference
Backstage doesn't export an auth0AuthApiRef from the core packages, so you'll need to create one yourself. This API reference connects the frontend to the Auth0 OAuth flow. In packages/app/src/apis.ts:
import {
createApiRef,
ApiRef,
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionApi,
configApiRef,
discoveryApiRef,
oauthRequestApiRef,
createApiFactory,
} from '@backstage/core-plugin-api';
import { OAuth2 } from '@backstage/core-app-api';
// Create Auth0 API reference (not exported from core-plugin-api)
export const auth0AuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: 'internal.auth.auth0',
});
export const apis: AnyApiFactory[] = [
// ... existing APIs
// Auth0 OAuth API factory
createApiFactory({
api: auth0AuthApiRef,
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
configApi: configApiRef,
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OAuth2.create({
discoveryApi,
oauthRequestApi,
provider: {
id: 'auth0',
title: 'Auth0',
icon: () => null,
},
environment: configApi.getOptionalString('auth.environment'),
defaultScopes: ['openid', 'profile', 'email'],
}),
}),
];Configure the SignInPage
In packages/app/src/App.tsx, configure the sign-in page to use the Auth0 provider:
import { SignInPage } from '@backstage/core-components';
import { auth0AuthApiRef } from './apis';
const app = createApp({
// ... other config
components: {
SignInPage: props => (
<SignInPage
{...props}
providers={[
{
id: 'auth0',
title: 'Auth0',
message: 'Sign in with Auth0',
apiRef: auth0AuthApiRef,
},
]}
title="Your App Name"
/>
),
},
});The built-in SignInPage component handles the OAuth popup flow automatically. When users click the sign-in button, a popup window opens for Auth0 authentication, then redirects back to your app once complete.
Part 4: User Entity Setup
Auth0 users must match Backstage User entities for sign-in to work. Create or update examples/org.yaml:
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
name: chris
spec:
profile:
displayName: Chris House
email: chris@example.com # Must match Auth0 user email
memberOf: [platform-team]
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: platform-team
description: Platform engineering team
spec:
type: team
children: []Configure Catalog to Load Users
In app-config.yaml, ensure User entities are allowed:
catalog:
rules:
# Include User and Group in global rules
- allow: [Component, System, API, Resource, Location, Template, User, Group]
locations:
- type: file
target: ./examples/org.yaml
rules:
- allow: [User, Group]Part 5: Environment Variables
Local Development
Create a .env file (add to .gitignore):
AUTH_AUTH0_DOMAIN=dev-xxxxx.us.auth0.com
AUTH_AUTH0_CLIENT_ID=your-client-id
AUTH_AUTH0_CLIENT_SECRET=your-client-secret
AUTH_SESSION_SECRET=a-random-32-character-string-hereBackstage doesn't auto-load .env files. Either:
- Use
dotenv-cli:yarn add -D dotenv-cliand rundotenv yarn start-backend - Set environment variables directly in your shell
For PowerShell, add to your profile:
$env:AUTH_AUTH0_DOMAIN="dev-xxxxx.us.auth0.com"
$env:AUTH_AUTH0_CLIENT_ID="your-client-id"
$env:AUTH_AUTH0_CLIENT_SECRET="your-client-secret"
$env:AUTH_SESSION_SECRET="your-session-secret"Part 6: Production Deployment
Add Secrets to Azure Key Vault
az keyvault secret set --vault-name your-vault --name AUTH-AUTH0-DOMAIN --value "dev-xxxxx.us.auth0.com"
az keyvault secret set --vault-name your-vault --name AUTH-AUTH0-CLIENT-ID --value "your-client-id"
az keyvault secret set --vault-name your-vault --name AUTH-AUTH0-CLIENT-SECRET --value "your-client-secret"
az keyvault secret set --vault-name your-vault --name AUTH-SESSION-SECRET --value "$(openssl rand -hex 32)"Configure SecretProviderClass
Update your CSI Secret Store configuration:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-backstage-secrets
namespace: backstage
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "false"
clientID: "your-managed-identity-client-id"
keyvaultName: "your-vault"
tenantId: "your-tenant-id"
objects: |
array:
# ... existing secrets
# Auth0 authentication secrets
- |
objectName: AUTH-AUTH0-DOMAIN
objectType: secret
- |
objectName: AUTH-AUTH0-CLIENT-ID
objectType: secret
- |
objectName: AUTH-AUTH0-CLIENT-SECRET
objectType: secret
- |
objectName: AUTH-SESSION-SECRET
objectType: secret
secretObjects:
- secretName: backstage-secrets
type: Opaque
data:
# ... existing mappings
# Auth0 secrets
- objectName: AUTH-AUTH0-DOMAIN
key: AUTH_AUTH0_DOMAIN
- objectName: AUTH-AUTH0-CLIENT-ID
key: AUTH_AUTH0_CLIENT_ID
- objectName: AUTH-AUTH0-CLIENT-SECRET
key: AUTH_AUTH0_CLIENT_SECRET
- objectName: AUTH-SESSION-SECRET
key: AUTH_SESSION_SECRETUpdate Deployment/Rollout
Add environment variables to your pod spec:
env:
# Auth0 authentication secrets
- name: AUTH_AUTH0_DOMAIN
valueFrom:
secretKeyRef:
name: backstage-secrets
key: AUTH_AUTH0_DOMAIN
- name: AUTH_AUTH0_CLIENT_ID
valueFrom:
secretKeyRef:
name: backstage-secrets
key: AUTH_AUTH0_CLIENT_ID
- name: AUTH_AUTH0_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: backstage-secrets
key: AUTH_AUTH0_CLIENT_SECRET
- name: AUTH_SESSION_SECRET
valueFrom:
secretKeyRef:
name: backstage-secrets
key: AUTH_SESSION_SECRETProduction Config
In app-config.production.yaml:
auth:
environment: production
session:
secret: ${AUTH_SESSION_SECRET}
providers:
# No guest provider in production
auth0:
production:
clientId: ${AUTH_AUTH0_CLIENT_ID}
clientSecret: ${AUTH_AUTH0_CLIENT_SECRET}
domain: ${AUTH_AUTH0_DOMAIN}
sessionDuration: { hours: 24 }
signIn:
resolvers:
- resolver: emailMatchingUserEntityProfileEmail
- resolver: emailLocalPartMatchingUserEntityName
catalog:
locations:
# Load users from GitHub in production
- type: url
target: https://github.com/your-org/your-repo/blob/main/backstage/examples/org.yaml
rules:
- allow: [User, Group]Testing the Integration
-
Start Backstage:
yarn start-backend # Terminal 1 yarn start # Terminal 2 -
Verify User entity loaded:
http://localhost:7007/api/catalog/entities?filter=kind=user -
Click Auth0 login and sign in with a user whose email matches a User entity
-
Check the catalog to verify you're signed in as the correct user
Troubleshooting
There are several moving parts involved in getting Auth0 working with Backstage. Here are some common issues and their solutions.
"Failed to sign-in, unable to resolve user identity"
This means Auth0 authenticated successfully, but no matching User entity was found. Check:
- User entity exists in catalog (check
/api/catalog/entities?filter=kind=user) - Email matches exactly between Auth0 profile and User entity
spec.profile.email - Catalog rules include
UserandGrouptypes - Wait for catalog sync after adding new User entities
"Auth0 misconfigured"
Check:
- Environment variables are set (not just in
.envfile) - Callback URL in Auth0 matches your Backstage URL
- Domain doesn't include
https://prefix
Guest option still shows in production
Verify NODE_ENV=production is set in your deployment. The conditional spread only hides guest when this is set.
User entities not loading from GitHub URLs
If your catalog logs show:
Unable to read url, NotAllowedError: Reading from 'https://raw.githubusercontent.com/...' is not allowedYou need to add raw.githubusercontent.com to the backend reading allow list:
backend:
# ... other config
reading:
allow:
- host: raw.githubusercontent.comThis is separate from the GitHub integration. The catalog reader needs its own permission to fetch from raw GitHub URLs.
Local file paths not loading User entities
File-based catalog locations use paths relative to the config file location, which can sometimes be confusing. Using GitHub raw URLs tends to be more straightforward:
catalog:
locations:
# Use raw GitHub URLs instead of local file paths
- type: url
target: https://raw.githubusercontent.com/your-org/your-repo/main/backstage/examples/entities.yaml
rules:
- allow: [User, Group]GitHub rate limiting
If catalog processing fails with 403 errors, you're hitting GitHub's rate limit. Set GITHUB_TOKEN in your environment:
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}Even for public repos, an authenticated token gets 5000 requests/hour vs 60 for anonymous.
SQLite database errors in development
If you see errors about connection.filename not being supported, use the in-memory database:
backend:
database:
client: better-sqlite3
connection: ':memory:' # Not { filename: './file.db' }The new backend system uses a Knex wrapper that handles database connections differently than earlier versions.
Plugins blocking backend startup
Some plugins (like ArgoCD) will block startup if their external services aren't configured. For local development, comment them out:
// argocd plugin - requires argocd config, disabled for local dev
// backend.add(import('@roadiehq/backstage-plugin-argo-cd-backend'));Summary
Auth0's free tier provides a good middle ground between guest authentication and enterprise SSO. The main components are:
- Backend module - handles the OAuth flow and token exchange
- Frontend API reference - connects the sign-in page to Auth0
- Sign-in resolvers - map Auth0 users to catalog User entities
- Environment configuration - keeps development and production settings separate
This approach works for both local development and production Kubernetes deployments, with secrets managed through Azure Key Vault.
Enjoyed this post? Give it a clap!
Comments