Skip to content

Creating an identity provider

A hosted, multi-tenant deployment authenticates through an identity provider: a separate installed distribution that answers whether a credential grants access to a group, and as whom. The platform ships none, and single-tenant mode needs none. The two modes are described under Deployment.

A provider spans both services. On the backend it is an entry point the server loads. On the frontend it is an auth adapter compiled into the UI image.

The backend contract

A provider declares one entry point:

[project.entry-points."glossogen.identity_provider.v1"]
my_provider = "my_auth.identity_provider:MyIdentityProvider"

It implements IdentityProvider (src/glossogen/server/identity/identity_provider.py), whose members are:

Member Returns
provider_name() A short name for the startup logs
unauthenticated_path_prefixes() The path prefixes its own webhooks and callbacks are served on. They bypass identity resolution, so cover exactly what routers() adds
routers() The routers it contributes to the app
deferred_consent_url(request_id) Where to send a browser that must choose a group before an MCP token is minted
resolve_identity(credential, group) The Identity a credential grants for a group

resolve_identity is called only after the platform has extracted a bearer credential and resolved the URL's slug to a groups row. So a provider answers one question, whether this credential grants access to this group and as whom, and never queries the groups table itself. It raises IdentityRejected with 401 for a credential that does not verify and 403 for one that verifies but does not cover the group.

Ambiguity refuses to boot: more than one declared provider, or one declared under an entry-point version the platform does not read, stops the server, because running unauthenticated while an operator believes a provider is installed is worse than not starting.

What the platform offers a provider

The contract above is one direction. A provider also needs things from the platform, and those live in identity/provider_services.py:

Need Call
Finish a deferred MCP authorization approve_parked_consent(request, request_id, group_id)
Build the consent URL for deferred_consent_url frontend_base_url()
Create or rename a group from an organization event glossogen.db.queries.upsert_group
Delete one glossogen.db.queries.soft_delete_group_by_external_org_id

The soft delete clears the external id and keeps the row, so runs.group_id foreign keys stay valid. The retained row continues to reserve its slug, so a new external organization cannot reuse that slug.

The frontend contract

frontend/src/features/auth/auth-adapter.ts is the contract; frontend/src/features/auth/adapter/ is the implementation, and the copy in this repository answers "no provider configured" to every slot. A deployment replaces that directory. Four modules, one per runtime that imports them:

Module Runs in Supplies
adapter/proxy.ts the Next.js proxy a delegate seeing every request, or null for pass-through
adapter/server.ts Server Components readSession(): the request's user and active group
adapter/browser.ts the browser, no React getSessionToken(): the bearer for API calls
adapter/client.tsx client components the provider wrapper, the group switcher, and the bodies of /sign-in, /sign-up, /select-org, /mcp-consent

Those four routes stay in this repository as shells, because the App Router resolves pages by file path. Each renders one adapter component and owns nothing else.

An adapter's public configuration reaches the browser the same way API_URL does: every AUTH_PUBLIC_* variable is read on the server, its prefix stripped, and published as RuntimeConfig.auth. Those values are visible in page source. Secrets stay server-side and never enter that object.

Users in several groups

A provider whose service tracks one active organization per session can activate the URL's group from its proxy delegate, for the current request, before the page renders or the API client mints a token. That is what lets a user in several groups reach any of them by URL without picking one first. If they are not a member, the backend sees a session whose active group does not match the slug and answers 403.