Adding Auth0 Authentication to Backstage: A Complete Guide — hero banner

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

  1. Log into Auth0 Dashboard
  2. Navigate to Applications > Applications > Create Application
  3. Select Regular Web Application
  4. 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/frame

Allowed Logout URLs:

http://localhost:3000
https://backstage.yourdomain.com

Allowed Web Origins:

http://localhost:3000
https://backstage.yourdomain.com

Note 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-provider

Register 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: emailLocalPartMatchingUserEntityName

The 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-here

Backstage doesn't auto-load .env files. Either:

  1. Use dotenv-cli: yarn add -D dotenv-cli and run dotenv yarn start-backend
  2. 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_SECRET

Update 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_SECRET

Production 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

  1. Start Backstage:

    yarn start-backend  # Terminal 1
    yarn start          # Terminal 2
  2. Verify User entity loaded:

    http://localhost:7007/api/catalog/entities?filter=kind=user
  3. Click Auth0 login and sign in with a user whose email matches a User entity

  4. 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:

  1. User entity exists in catalog (check /api/catalog/entities?filter=kind=user)
  2. Email matches exactly between Auth0 profile and User entity spec.profile.email
  3. Catalog rules include User and Group types
  4. Wait for catalog sync after adding new User entities

"Auth0 misconfigured"

Check:

  1. Environment variables are set (not just in .env file)
  2. Callback URL in Auth0 matches your Backstage URL
  3. 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 allowed

You need to add raw.githubusercontent.com to the backend reading allow list:

backend:
  # ... other config
  reading:
    allow:
      - host: raw.githubusercontent.com

This 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:

  1. Backend module - handles the OAuth flow and token exchange
  2. Frontend API reference - connects the sign-in page to Auth0
  3. Sign-in resolvers - map Auth0 users to catalog User entities
  4. 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