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 BaseAuthProvider, 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).
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 BaseAuthProvider 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. |
| 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.