auth.oauth
Your project as an OAuth 2.1 and OpenID Connect provider.
Two different things live under this namespace, and conflating them is the main source of confusion.
Consumer side — the applications your users authorized, which they can review and disconnect. One screen in your settings.
Provider side — your project acting as the issuer, so other applications can "sign in with you". A whole flow you host, walked through in OAuth Server — this page is the method reference for it.
Neither is auth.authorize(provider), which sends
your user out to Google. That is the opposite direction: you as the consumer
of someone else's identity.
grants.list
oauth.grants.list() → OAuthUserGrant[]
The applications the current user has authorized.
const { data: apps } = await nulvless.auth.oauth.grants.list();client{ id, name?, logo_uri?, uri? }RequiredEnough to render the app with its own name and logo rather than a bare id.
scopesstring[]RequiredWhat this application was granted. Show them — "has access to your profile and email" is the whole point of the screen.
granted_atstringRequiredThese live under oauth.grants, not directly on oauth.
grants.revoke
oauth.grants.revoke(clientId) → null
Disconnects one application. Its tokens stop working; the others are untouched.
await nulvless.auth.oauth.grants.revoke(clientId);clientIdstringRequiredFrom client.id above.
discovery
discovery() → OAuthDiscovery
The OIDC discovery document — what any conforming client reads to configure itself.
const { data } = await nulvless.auth.oauth.discovery();jwks
jwks() → OAuthJWKS
The public keys your tokens are verified against.
const { data: keys } = await nulvless.auth.oauth.jwks();Both are published from the moment the OAuth server is enabled and stay in sync with your settings. There is nothing to regenerate when you change them, and nothing to host yourself.
authorize
authorize(params) → { url }
Begins the authorization code flow, with PKCE.
await nulvless.auth.oauth.authorize({
client_id,
redirect_uri: 'https://client.example.com/callback',
code_challenge,
code_challenge_method: 'S256',
scope: 'openid profile',
state,
});client_idstringRequiredredirect_uristringRequiredMust be on the application's allowlist. Checked before anything is issued.
code_challengestringRequiredPKCE is not optional here.
code_challenge_method'S256'RequiredThe only accepted method. plain is refused.
response_type'code'OptionalscopestringOptionalSpace-separated.
statestringOptionalRound-tripped back to your callback. Use it, and check it.
resourcestringOptionalnoncestringOptionalThe OIDC nonce, tying an ID token to this request. Unrelated to the sudo nonce used elsewhere in this SDK.
exchangeCodeForToken
exchangeCodeForToken(params) → OAuthTokenResponse
Trades the authorization code for tokens.
const { data } = await nulvless.auth.oauth.exchangeCodeForToken({
code,
code_verifier,
});codestringRequiredcode_verifierstringRequiredThe PKCE counterpart of code_challenge.
redirect_uristringOptionalclient_idstringOptionalclient_secretstringOptionalConfidential clients only. Never from a browser — a public client has no secret precisely because it cannot keep one.
resourcestringOptionalrefreshToken
refreshToken(params) → OAuthTokenResponse
const { data } = await nulvless.auth.oauth.refreshToken({ refresh_token });refresh_tokenstringRequiredclient_idstringOptionalclient_secretstringOptionalgetAuthorization
getAuthorization(authorizationId) → OAuthAuthorization
Loads a pending authorization so you can render the consent screen — who is asking, and for what.
const { data: pending } = await nulvless.auth.oauth.getAuthorization(
authorizationId,
);consent
consent(authorizationId, action) → OAuthConsentResult
Records the user's answer.
await nulvless.auth.oauth.consent(authorizationId, 'approve');
await nulvless.auth.oauth.consent(authorizationId, 'deny');authorizationIdstringRequiredactionOAuthConsentActionRequiredapproveIssue the code and continue the flow.denyRefuse. The client is redirected back with an error.The screen is entirely yours — your name, your styling, your copy. What the
platform guarantees is that nothing is issued until consent says so, so
there is no way to accidentally grant access by rendering the page.
userinfo
userinfo() → OAuthUserInfo
The OIDC UserInfo claims for the current access token. Called by the client application with the token it was issued, not by you.
const { data: claims } = await nulvless.auth.oauth.userinfo();register
register(params) → OAuthApp
Dynamic client registration, per RFC 7591 — applications registering themselves rather than being created by you.
const { data: app } = await nulvless.auth.oauth.register(params);Off unless you enable it. With it off, applications are created through
auth.admin.oauthClients, which is the
right default: dynamic registration lets anyone who can reach the endpoint
create a client.
What a third party can ask for
An application never receives more than the user themselves holds. A token
issued to a third party carries a subset of the user's own scopes — so granting
an integration projects:write does nothing if that user could not write
projects to begin with.
This is worth stating on your consent screen. "This app is requesting X" is misleading if the user cannot do X either.