Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx skills add bendaamerahmed/backstage-idp-plugin --skill backstage-authgit clone --depth 1 https://github.com/bendaamerahmed/backstage-idp-pluginWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/skills/bendaamerahmed/backstage-idp-plugin/backstage-auth)<a href="https://agentmods.dev/skills/bendaamerahmed/backstage-idp-plugin/backstage-auth"><img src="https://agentmods.dev/badge/skills/bendaamerahmed/backstage-idp-plugin/backstage-auth/github.svg" alt="Measured on agentmods" height="20"></a>Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.
<a href="https://agentmods.dev/skills/bendaamerahmed/backstage-idp-plugin/backstage-auth"><img src="https://agentmods.dev/badge/skills/bendaamerahmed/backstage-idp-plugin/backstage-auth.svg" alt="Reviewed on agentmods" width="80" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.00043 | $0.03809 |
| Opus 5 | $0.00022 | $0.01904 |
| Sonnet 5 | $0.00009 | $0.00762 |
| Haiku 4.5 | $0.00004 | $0.00381 |
Grade A, and why
backstage-auth scanned grade A with 1 finding against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 12d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Makes network callslowCapability
Not a fault in itself. Listed so you know the mod talks to something, and to what.
- For external access, `curl -H "Authorization: Bearer <token>" <backend>/api/<plugin>/...` — a 401 means the token or its `accessRestrictions` is wrong, not the plugin. How it starts
The opening of the file, as written. The whole thing — 103 lines — stays where its author put it; the contents beside it link to each section on GitHub.
Backstage Authentication and Identity
Wire an identity provider into Backstage, map its users onto catalog entities, and tell apart the five ways sign-in fails.
Preconditions
- Release line from
backstage.json. Backend generation:createBackend()+backend.add(import('@backstage/plugin-auth-backend'))is the new backend system; acreateRouterinpackages/backend/src/plugins/auth.tsis legacy — migrate it before adding auth modules. - Frontend generation:
SignInPageBlueprintfrom@backstage/plugin-app-react+createFrontendModuleis NFS; acomponents: { SignInPage }option oncreateAppfrom@backstage/app-defaultsis legacy. - Which provider is meant for sign-in and which only delegates access to third-party APIs. These are different jobs on the same config tree and most misconfigurations start here.
- Exact factory and resolver signatures (
createOAuthProviderFactory,createProxyAuthProviderFactory,authProvidersExtensionPoint, thectxhelpers) read from the installed@backstage/plugin-auth-nodetypes, not from memory. - Anything requiring a new OAuth app registration, a client-secret rotation, or an IdP-side change is external mutation: stop and return a BLOCKED report with the exact redirect URI and scopes needed.
Procedure
- Inventory before changing. Read every
auth:block acrossapp-config.yaml,app-config.production.yamland local overrides, the auth imports inpackages/backend/src/index.ts, and the app's sign-in component. Print the effective merge withyarn backstage-cli config:print --lax. Noteauth.environment— only the provider sub-block matching it is loaded. - Classify each provider. A provider is a sign-in provider only if it has
signIn.resolversin config or asignInResolverin code. Providers used purely for API delegation (SCM tokens, Google APIs) are configured underauth.providers.<id>.<env>with client credentials and no resolver, and are consumed in the frontend through their*AuthApiRef. Adding a resolver to a delegation-only provider silently creates a second sign-in path. - Configure the provider block and register its module.
- Credentials live at
auth.providers.<id>.<env>.clientId/clientSecret, sourced from env vars. callbackUrlis optional and defaults to<backend.baseUrl>/api/auth/<id>/handler/frame. Set it explicitly only when an ingress rewrites the path.- Add
backend.add(import('@backstage/plugin-auth-backend-module-<id>-provider'))after@backstage/plugin-auth-backend. Confirm the package name and its exports from the installed package; provider ids and module names do not always match the vendor's name.
- Credentials live at
- Ingest users and groups before wiring a resolver. Every built-in resolver is a catalog lookup and cannot work against an empty catalog. Use an org provider —
@backstage/plugin-catalog-backend-module-github-org(config undercatalog.providers.githubOrg), or the MS Graph / GitLab / LDAP equivalents — or a customEntityProvider(backstage-catalog). Verify theUserentities exist before touching sign-in config. - Choose exactly one sign-in resolver, in the provider's
signIn.resolverslist.- Provider-agnostic built-ins:
emailMatchingUserEntityProfileEmail,emailLocalPartMatchingUserEntityName. Provider-specific ones such as GitHub'susernameMatchingUserEntityNameare listed in that provider's doc page. - Always set
allowedDomainsonemailLocalPartMatchingUserEntityName; without it any account whose email local part matches an entity name can sign in. - More than one resolver, or more than one sign-in provider, is acceptable only when they provably resolve the same human to the same entity ref. Otherwise it is an account-takeover path.
- Provider-agnostic built-ins:
- Write a custom resolver only when no built-in fits.
- Backend module with
pluginId: 'auth'(exact) and a uniquemoduleId, registering throughauthProvidersExtensionPointwithproviderIdmatching the config key exactly. - Pass
signInResolvertocreateOAuthProviderFactory({ authenticator, ... }); proxy-based providers use the proxy factory instead. Read both signatures from the installed@backstage/plugin-auth-node. - Remove
signIn.resolversfrom config first: config resolvers take priority over code resolvers, so leaving them in makes the module dead code. - Prefer
ctx.signInWithCatalogUser. Drop toctx.findCatalogUser+ctx.resolveOwnershipEntityRefs+ctx.issueToken({ claims: { sub, ent } })only for custom ownership.resolveOwnershipEntityRefsincludes only groups with a directMEMBER_OFrelation by default — the usual cause of missing ownership in nested group trees (backstage-permissions).
- Backend module with
- Use
profileTransformon the same factory for authorization and display fields. That is where you validate the upstream response, shapedisplayName/email/picture, and throw to reject a user outright — not the resolver. - Wire the app-side sign-in page.
- NFS:
SignInPageBlueprint.make({ params: { loader: async () => props => <SignInPage {...props} provider={{ id, title, message, apiRef }} /> } }), registered viacreateFrontendModule({ pluginId: 'app', extensions: [signInPage] }). SignInPageandProxiedSignInPagecome from@backstage/core-components; theprovidersarray form accepts'guest'alongside provider configs, and can be selected conditionally offconfigApi.getString('auth.environment').- Behind an auth proxy (AWS ALB, Azure EasyAuth, Cloudflare Access, Google IAP, OAuth2 Proxy) use
ProxiedSignInPage provider="<id>"; it only calls/refresh. Extra provider headers go through itsheadersprop, which may be sync or async. enableExperimentalRedirectFlow: trueat the config root replaces the popup with a full redirect.
- NFS:
- Keep guest access out of production.
auth.providers.guest: {}is development-only;userEntityRefandownershipEntityRefscustomise the identity.dangerouslyAllowOutsideDevelopment: trueis required anywhere else — never set it without explicit authorization. - Attach tokens correctly in frontend plugin clients.
- Calls to Backstage backends: use
fetchApiReffrom@backstage/core-plugin-api, which adds the Backstage token itself. Do not readidentityApiRef.getCredentials()and build the header by hand. Resolve base URLs throughdiscoveryApiRef. - Calls to third-party services: take a short-lived token from that provider's
*AuthApiRef(githubAuthApiRefand friends) with the scopes actually needed, or fromscmAuthApiRefwhen the target host varies. Extend SCM coverage withScmAuth.merge(ScmAuth.forGithub(...))in the app's API factories.
- Calls to Backstage backends: use
- Use the auth core services for backend-to-backend calls.
httpAuth.credentials(req)turns an incoming request into credentials;auth.getPluginRequestToken({ onBehalfOf, targetPluginId })mints the outgoing token;await auth.getOwnServiceCredentials()suppliesonBehalfOffor self-initiated work such as scheduled tasks.- Mint a token immediately before each request. Never store or reuse one.
- Narrow principals with
auth.isPrincipal(credentials, 'user' | 'service'). - In the new backend system ownership refs are not read off the token's
entclaim; get them from theuserInfoservice.
- Grant non-browser callers access through
backend.auth.externalAccess.type: staticwithoptions.tokenandoptions.subject; generate the token withnode -p 'require("crypto").randomBytes(24).toString("base64")'.type: jwkswithurl,issuer,algorithm,audience,subjectPrefix; verified subjects get anexternal:prefix.- Always add
accessRestrictions(plugin, permission, permission attribute). An entry without them has unlimited access to every plugin. backend.auth.pluginKeyStorestatic ES256 keys are for multi-replica deployments needing stable plugin-to-plugin signing.
- Treat the OAuth allowlists as hardened. These apply where Backstage acts as an OAuth server for MCP clients.
- CIMD is stable:
auth.clientIdMetadataDocuments.enabled: true, with optionalallowedClientIdPatternsandallowedRedirectUriPatterns.auth.experimentalClientIdMetadataDocumentssurvives as a deprecated alias. - The block belongs under the top-level
auth:, neverbackend.auth:. - Patterns are matched per URL component, not against the whole URL string. Wildcards do not cross host or path boundaries; a pattern without an explicit protocol is rejected; redirect URIs with embedded credentials are always rejected; a wildcard port no longer implies any path, so write
http://localhost:*/*,http://127.0.0.1:*/*,http://[::1]:*/*. - Setting
allowedClientIdPatternsreplaces the built-in Claude and VS Code defaults entirely; the Backstage CLI client stays allowed regardless. - CIMD requires the new frontend system plus the
@backstage/plugin-authfrontend plugin inpackages/app. On a legacy app it cannot work at all. auth.experimentalDynamicClientRegistration(DCR) is deprecated, logs a startup warning, and must not be used for new setups. Migrate existing ones to CIMD.
- CIMD is stable:
- Pin signing keys when tokens must survive restarts.
auth.keyStore.provider: staticwith ES256 keys (privateKeyFilein PKCS#8,publicKeyFilein SPKI; the first key signs, later keys only validate, which is what makes rotation safe). The default is ephemeral in-memory keys.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 12d ago First seen · 103 lines · 43 tokens per session scan A 0a3b611702f4
backstage-auth is a skill published in the GitHub repository bendaamerahmed/backstage-idp-plugin (1 stars, last pushed 1mo ago), licensed MIT. It adds 43 tokens to every session and 3,809 once invoked, about $0.0002 per session on Opus 5. A static security scan graded it A with 1 finding (makes network calls). No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.
Other skills, from other repositories
ash-framework
Ash Framework — resources, actions, policies, aggregates, calculations, AshPhoenix.Form, LiveView, migrations. Use when generating resources via mix ash.codegen, editing changes, checks, types, validations, or domain code interfaces.
deploy
Elixir/Phoenix deployment patterns — Dockerfile, fly.toml, runtime.exs, mix release, rel/ overlays. Use when configuring Fly.io, Docker, CI/CD, health checks, or production migrations.
liveview-patterns
Build LiveView: async data (assignasync), PubSub (check connected?), phx-change events, form components/modals/uploads, streams for lists, livepatch. Use when handling interactions, debugging events, or tracking Presence.
tidewave-integration
Tidewave MCP runtime tools — debugging, smoke testing, live state inspection, SQL queries, hex docs. Use when evaluating code in a running Phoenix app.
oban
Oban job processing — workers, perform/1 (OSS) and process/1 (Pro), queues, cron, retries, unique jobs, idempotency, Oban Pro (Workflow, Batch, Chunk, Smart Engine), Testing. Use when writing Oban workers, queue config, or debugging jobs.
perf
Analyze Elixir/Phoenix performance — N+1 queries, assign bloat, ecto optimization, genserver bottlenecks. Use when slowness, timeouts, or high memory reported.