← All @molecule/* packages · App templates
@molecule/api-oauthCore interface · auth · API (Node) · v1.1.0 · Apache-2.0
OAuth provider interface
npm install @molecule/api-oauth@molecule/api-oauth is the auth core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 6 providers: @molecule/api-oauth-apple, @molecule/api-oauth-github, @molecule/api-oauth-gitlab, @molecule/api-oauth-google, @molecule/api-oauth-microsoft, @molecule/api-oauth-twitter.
// Initiation: GET /users/oauth/:provider → 302 to the provider.
router.get('/oauth/:provider', (req, res) => {
const state = randomToken()
const { challenge, verifier } = pkce() // S256
res.cookie('oauth_state', state, { httpOnly: true, sameSite: 'lax' })
res.cookie('oauth_verifier', verifier, { httpOnly: true, sameSite: 'lax' })
const url = buildAuthorizeUrl({ state, codeChallenge: challenge, codeChallengeMethod: 'S256' })
if (!url) return res.status(404).json({ error: 'Provider not configured.' })
res.redirect(url)
})
// Callback: verify state FIRST, then exchange the code SERVER-SIDE.
router.get('/oauth/:provider/callback', async (req, res) => {
if (!req.query.state || req.query.state !== req.cookies.oauth_state) {
return res.status(403).json({ error: 'Invalid state.' }) // CSRF guard
}
const props = await verifyOAuthCode(String(req.query.code), req.cookies.oauth_verifier)
if (!props) return res.status(401).json({ error: 'OAuth verification failed.' })
// props.emailVerified must be === true before trusting props.email over a local account.
await logInOrLink(res, props)
})Providers (6): @molecule/api-oauth-apple, @molecule/api-oauth-github, @molecule/api-oauth-gitlab, @molecule/api-oauth-google, @molecule/api-oauth-microsoft, @molecule/api-oauth-twitter
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
OAuth core interface for molecule.dev.
Defines the standard interface for OAuth providers.
// Initiation: GET /users/oauth/:provider → 302 to the provider.
router.get('/oauth/:provider', (req, res) => {
const state = randomToken()
const { challenge, verifier } = pkce() // S256
res.cookie('oauth_state', state, { httpOnly: true, sameSite: 'lax' })
res.cookie('oauth_verifier', verifier, { httpOnly: true, sameSite: 'lax' })
const url = buildAuthorizeUrl({ state, codeChallenge: challenge, codeChallengeMethod: 'S256' })
if (!url) return res.status(404).json({ error: 'Provider not configured.' })
res.redirect(url)
})
// Callback: verify state FIRST, then exchange the code SERVER-SIDE.
router.get('/oauth/:provider/callback', async (req, res) => {
if (!req.query.state || req.query.state !== req.cookies.oauth_state) {
return res.status(403).json({ error: 'Invalid state.' }) // CSRF guard
}
const props = await verifyOAuthCode(String(req.query.code), req.cookies.oauth_verifier)
if (!props) return res.status(401).json({ error: 'OAuth verification failed.' })
// props.emailVerified must be === true before trusting props.email over a local account.
await logInOrLink(res, props)
})
core
npm install @molecule/api-oauth
OAuthAuthorizeUrlParamsParameters for building a provider authorization (initiation) URL — the URL the user's browser is redirected to so the provider can authenticate them and send back an authorization code.
interface OAuthAuthorizeUrlParams {
/**
* Absolute URI the provider should redirect the user back to after
* authorization (the app origin, optionally with a path). When omitted,
* the builder leaves `redirect_uri` off the URL so the provider falls
* back to its registered callback URL.
*/
redirectUri?: string
/**
* The CSRF `state` parameter bound to the initiating session (stored in
* an httpOnly cookie by the initiation endpoint and validated by the
* login handler on callback).
*/
state: string
/**
* PKCE code challenge derived (S256) from the per-session code verifier.
* Omit only for providers that do not support PKCE.
*/
codeChallenge?: string
/**
* PKCE challenge method. Always prefer `'S256'`; `'plain'` exists only
* for providers that cannot hash.
*/
codeChallengeMethod?: 'S256' | 'plain'
}
OAuthProviderConfigConfiguration for an OAuth provider.
interface OAuthProviderConfig {
/**
* The OAuth server identifier.
*/
serverName: string
/**
* The OAuth verification function.
*/
verify: OAuthVerifier
/**
* Builds the provider authorization URL for the initiation redirect.
* Optional for backward compatibility: bonds without it can still verify
* codes (the app initiates some other way), but the standard
* `GET /users/oauth/:provider` initiation endpoint requires it and
* responds 404 for providers that lack it.
*/
getAuthorizeUrl?: OAuthAuthorizeUrlBuilder
}
OAuthUserPropsThe properties returned when verifying an OAuth code.
interface OAuthUserProps {
/**
* An alphanumeric username derived from the OAuth provider.
*
* Format: `{provider_username}@{provider_name}`
*/
username: string
/**
* The user's display name from the OAuth provider.
*/
name?: string
/**
* The user's short biography / description from the OAuth provider
* (e.g. GitHub's `bio`, GitLab's `bio`, X's `description`). Omitted when
* the provider exposes no such field.
*/
bio?: string
/**
* URL of the user's profile image from the OAuth provider (e.g. Google's
* `picture`, GitHub/GitLab's `avatar_url`, X's `profile_image_url`).
* Omitted when the provider exposes none (Apple never does; Microsoft
* Graph only serves photos as a binary endpoint behind an extra scope).
*/
avatar?: string
/**
* The user's email address from the OAuth provider.
*/
email?: string
/**
* Whether the OAuth provider has affirmatively verified that the user
* controls this `email` mailbox.
*
* `true` MUST mean the provider proved mailbox ownership (e.g. Google's
* `email_verified`, Apple's `email_verified` ID-token claim). When the
* provider exposes no trustworthy verification signal in the profile data
* the verifier fetched, this MUST be `false`/`undefined` (never optimistically
* `true`) — consumers treat only an explicit `true` as verified.
*
* Consumers (e.g. the user resource's `logInOAuth` handler) use this to
* decide whether a provider-supplied email may be trusted over an existing,
* unverified local account — preventing an unverified squatter from blocking
* the verified mailbox owner.
*/
emailVerified?: boolean
/**
* The OAuth server identifier (e.g., 'github', 'google', 'twitter').
*/
oauthServer: string
/**
* Unique identifier for the user from the OAuth provider.
*/
oauthId: string
/**
* Raw user data from the OAuth provider.
*/
oauthData: Record<string, unknown>
}
OAuthAuthorizeUrlBuilderBuilds the provider's authorization URL for OAuth initiation
(GET /users/oauth/:provider → 302 to this URL). Implementations embed
their own client id, scopes, and authorize endpoint so no consumer ever
hardcodes provider knowledge.
type OAuthAuthorizeUrlBuilder = (params: OAuthAuthorizeUrlParams) => string | null
OAuthVerifierExchanges an OAuth authorization code for user profile information.
Implementations call the provider's token and user-info endpoints,
then return normalized OAuthUserProps for account creation or login.
Returning null means the provider AFFIRMATIVELY rejected the code
(e.g. GitHub's bad_verification_code, an expired/forged code) — the
consumer (logInOAuth) surfaces that as a clean 403 "verification
failed". A thrown error means an infrastructure failure (network,
provider outage) and surfaces as a 500. Implementations MUST NOT throw
for a rejected code — that would misreport a client mistake (or an
attack) as a server fault.
type OAuthVerifier = (
code: string,
codeVerifier?: string,
redirectUri?: string,
) => Promise<OAuthUserProps | null>
| Provider | Package |
|---|---|
| Apple OAuth | @molecule/api-oauth-apple |
| GitHub OAuth | @molecule/api-oauth-github |
| GitLab OAuth | @molecule/api-oauth-gitlab |
| Google OAuth | @molecule/api-oauth-google |
| Microsoft OAuth | @molecule/api-oauth-microsoft |
| Twitter OAuth | @molecule/api-oauth-twitter |
The OAuth authorization-code flow has security steps a weak integration skips — these are MANDATORY (the per-type docs carry the details):
state is CSRF protection, not optional. On initiation, generate a random
state, store it in an httpOnly cookie, and put it on the authorize URL
({@link OAuthAuthorizeUrlParams.state}). On callback, reject unless the returned
state matches the cookie — no state check is a login-CSRF / account-takeover hole.'S256'): derive a
per-session code verifier, send its challenge on initiation, pass the verifier to
{@link OAuthVerifier} on callback.(code, …) runs in your API
and returns {@link OAuthUserProps} (or null). The OAuth client secret lives only
in the API — never ship it to the browser; the browser only gets the authorize URL and
returns the code.emailVerified === true. A provider email whose
{@link OAuthUserProps.emailVerified} is not explicitly true MUST NOT take over an
existing local account (squatter protection).@molecule/api-resource-user's logInOAuth already implements this flow correctly —
prefer wiring a provider bond into it over hand-rolling the endpoints.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
state parameter is generated on initiation and
verified on callback (CSRF protection): a mismatched or absent state is
rejected (403); the redirect_uri is validated against an allowlist so an
attacker cannot redirect the code elsewhere; and the client secret + tokens
stay server-side — grep the browser bundle and network tab to confirm the
secret never reaches the client (only the authorize URL and returned code
cross the boundary).