alepha@docs:~/docs/packages/alepha/api$
cat oauth.md | pretty
3 min read
Last commit:

#Alepha - Api Oauth

#Installation

Part of the alepha package. Import from alepha/api/oauth.

npm install alepha

#Overview

OAuth 2.1 authorization server module for MCP.

Features:

  • OAuth 2.1 authorization code flow with PKCE (RFC 7636)
  • Dynamic Client Registration (RFC 7591), deduplicated
  • Authorization server metadata discovery (RFC 8414)
  • Stateless authorization codes (short-lived signed JWTs)
  • Single-use code enforcement
  • Refresh tokens bound to the client they were issued to
  • Device authorization grant (RFC 8628), with the page a human approves a device on

The device grant ships both halves. POST /oauth/device_authorization and the device_code token grant are the device's; /oauth/device is the human's, and it is the verification_uri a device is told to print. It is server-rendered HTML like the consent screen, sends a signed-out visitor to loginPath?redirect_uri= and back, and refuses an answer posted from another origin - see OAuthController.deviceDecision for why that check matters more here than on the consent POST.

Registration is deduplicated, and that is what makes a "connected app" a thing. A client that registers again with the same name, the same redirect_uris and no secret is handed the row it already has instead of a new one. Some clients - claude.ai among them - run DCR on every connect and never reuse an id, which grew a table of near-identical rows and, worse, made one application look like four to anything grouping by client_id. Reuse is refused for a confidential client, a revoked one, another realm, a different redirect_uri set, and for any registration that named its own client_id - see OAuthClientService.register.

oauth_clients.lastUsedAt is written on every successful grant, and OAuthJobs.purgeAbandonedClients collects DCR rows older than a day that never received a token (lastUsedAt null) and no session references. A client used once is never collected: some clients (ChatGPT) register once and reuse that client_id for the life of the connector. Register it the way $realm does; a job that mounted itself would run in every application that imports this module.

The refresh_token grant requires client_id. The client is looked up and - when confidential - must present its secret, exactly as on the authorization_code grant; the refresh token must then belong to a session minted for that same client. A session with no recorded client (an ordinary password login) is not an OAuth grant and cannot be refreshed here.

This makes the id_token aud trustworthy: it is the authenticated client, not an unvalidated request field. Without the binding, any refresh-token holder could name any client_id and receive an id_token minted for it, which a relying party that forwards id_tokens as its Bearer would accept.

Which redirect URIs a client may register. https:// to any host, with at most one * standing for a single host label (https://*.example.com/cb), or plain http:// to the loopback interface only: 127.0.0.1, [::1] or localhost, compared on the parsed host. A loopback redirect is matched on any port (RFC 8252 §7.3), since a native app listens wherever the OS lets it; everything else is matched exactly. A refused registration answers 400 with an RFC 7591 body, error being invalid_redirect_uri or invalid_client_metadata.

Integration: Register the module and configure the realm + protected resource path:

ts
1const app = Alepha.create()2  .with(AlephaOAuth)3  .set(oauthOptions, { realm: "users", resource: "/mcp" });