Custom OAuth / SSO Provider
Plug in any provider not built in — Microsoft, Okta, Auth0, Discord, an internal SSO — with a full, end-to-end example.
Need Microsoft, Discord, Slack, Okta, Auth0, an internal enterprise SSO? Extend SocialAuthProvider, implement validate(), and pass an instance to the module. Nest Auth handles everything else — identity lookup, user creation, account linking, session, and events (see how SSO works).
Extend
SocialAuthProvider, notBaseAuthProvider, for any provider whosevalidate()returns the provider's own user id (an OAuthsub, Discordid, …).SocialAuthProviderresolves the linked identity by that external subject (providerId).BaseAuthProviderresolves by our internaluserId— auuidcolumn — so a non-UUID subject makes every login 500 on Postgres withinvalid input syntax for type uuid(it silently returnsINVALID_CREDENTIALSon SQLite). UseBaseAuthProvideronly when yourvalidate()returns ourNestAuthUser.id.
Full example: a Discord provider, end to end
Five steps take you from nothing to a working "Sign in with Discord". Swap the URLs/fields for Microsoft, Okta, Auth0, or your internal IdP — the shape is identical.
1. Register the OAuth app with the provider
In the Discord Developer Portal → New Application → OAuth2. Add your redirect URI, select the identify + email scopes, and copy the Client ID and Client Secret. (Every provider has an equivalent console step — this is where you get the credentials.)
2. Write the provider
A SocialAuthProvider subclass needs exactly three things: providerName, getRequiredFields(), and validate(). validate() verifies the credential with the provider and returns a normalized user.
3. Register it in the module
Pass a plain instance in customAuthProviders. That's it — the repositories are injected for you, so no forRootAsync or DI wiring is needed.
POST /auth/login with { providerName: 'discord', … } now works.
4. Call it from the frontend
Get a Discord access token with your OAuth flow, then log in. A first-time user needs createUserIfNotExists: true — the client SDK's socialLogin() sets it for you:
React (useNestAuth().login) — pass the flag yourself:
5. Verify with curl
A 2xx with a session/token body means it works. On the first call a NestAuthUser + identity row are created; subsequent logins with the same Discord id resolve to the same user.
The AuthProviderUser you return
| Field | Meaning |
|---|---|
userId (required) | The provider's stable user id (an OAuth sub, Discord id, …). Identities are keyed on this — not your local user id. |
email / phone | Used to find/create the local user and to link to an existing account. |
emailVerified / phoneVerified | Set true only when the provider proves it. Controls the linking gate and whether emailVerifiedAt is stamped. |
metadata | Anything else (username, avatar, raw profile) — stored on the identity and available to your hooks. |
Return null (or throw) when the credential is invalid.
What you get for free
Once validate() returns, the library:
- Looks up the identity (
nest_auth_identitiesforprovider='discord'+ the returneduserId) → logs the existing user in. - Creates a new
NestAuthUser+ identity when the identity is new andcreateUserIfNotExistsis set — emittingUserRegisteredEvent. - Links to an existing account when the email matches — but only when
emailVerified: true(the verified-email gate;social.requireVerifiedEmailForLinking). - Issues the session / JWT and emits
UserLoggedInEvent.
When the client sends an auth code instead of a token
If your provider hands the frontend a one-time authorization code, exchange it for a token inside validate() and declare getRequiredFields() as ['code']:
Enterprise SSO (SAML / OIDC / IdP groups)
The same shape covers enterprise SSO — verify the assertion/id-token in validate() (for OIDC, validate the id_token signature against the IdP's JWKS and return its sub/email) and return the user. To map IdP groups → Nest Auth roles, see the external role resolver recipe.
Common mistakes
| Symptom | Cause |
|---|---|
providerName unknown on login | You set name instead of providerName, or forgot to add the instance to customAuthProviders. |
First social login returns 401 INVALID_CREDENTIALS | New user without createUserIfNotExists: true — use socialLogin() or pass the flag. |
Login 500s on Postgres: invalid input syntax for type uuid (or existing users get INVALID_CREDENTIALS on SQLite) | Your provider extends BaseAuthProvider but returns the provider's subject as userId. Extend SocialAuthProvider so the identity lookup uses providerId, not the uuid userId column. |
| Social identity didn't link to the existing email account | emailVerified wasn't true, and social.requireVerifiedEmailForLinking is on (default). Set emailVerified when the provider proves it. |
MISSING_REQUIRED_FIELDS | The client didn't send a field named in getRequiredFields(). |
Related
- Social / SSO Login — how the universal flow works.
- Google OAuth — a built-in, by example.
- Custom OAuth provider recipe — a short copy-paste version.
- Link multiple providers to one account.