Skip to content
v3.0.1 — MCP OAuth, organizations, passkeys, SSR adaptersChangelog
Convex Auth
Esc
↑↓navigate↵open⌘Jpreview
On this page

Generic OAuth

Add arbitrary OAuth 2.0 providers to convex-auth.

convex-auth supports built-in OAuth providers (Google, GitHub, Discord) and arbitrary generic providers that follow the standard OAuth 2.0 Authorization Code flow with PKCE.

Enable

import { components } from "./_generated/api";
import { convexAuth } from "@vortex-api/convex-auth/convex";

export const auth = convexAuth({
  component: components.convexAuth,
  oauth: {
    providers: {
      keycloak: {
        clientId: process.env.OAUTH_KEYCLOAK_CLIENT_ID!,
        clientSecret: process.env.OAUTH_KEYCLOAK_CLIENT_SECRET!,
        authorizationEndpoint:
          "https://keycloak.example.com/realms/master/protocol/openid-connect/auth",
        tokenEndpoint: "https://keycloak.example.com/realms/master/protocol/openid-connect/token",
        userInfoEndpoint:
          "https://keycloak.example.com/realms/master/protocol/openid-connect/userinfo",
        scopes: ["openid", "email", "profile"],
      },
    },
  },
});

providers is keyed by the provider ID. The same ID is used in the sign-in call and in the callback route (/api/auth/callback/{provider}).

Configuration

Each generic provider accepts:

  • clientId
  • clientSecret
  • issuer — issuer string; required for OIDC discovery and ID-token validation
  • discovery — when true, fetch endpoints from /.well-known/openid-configuration
  • authorizationEndpoint — explicit or discovered authorization endpoint
  • tokenEndpoint — explicit or discovered token endpoint
  • userInfoEndpoint — explicit or discovered userinfo endpoint
  • jwksUri — explicit or discovered JWKS URI
  • useIdToken — verify and extract the user from the ID token instead of calling userinfo
  • scopes (defaults to [])
  • userInfo — optional mapping of standard userinfo or ID-token fields to response keys
  • profile — optional function for full control over profile extraction

Field mapping

When a userinfo response uses non-standard field names, map them instead of providing a profile function:

providers: {
  authentik: {
    clientId: process.env.OAUTH_AUTHENTIK_CLIENT_ID!,
    clientSecret: process.env.OAUTH_AUTHENTIK_CLIENT_SECRET!,
    authorizationEndpoint: "https://authentik.example.com/application/o/authorize/",
    tokenEndpoint: "https://authentik.example.com/application/o/token/",
    userInfoEndpoint: "https://authentik.example.com/application/o/userinfo/",
    scopes: ["openid", "email", "profile"],
    userInfo: {
      id: "sub",
      name: "name",
      email: "email",
      image: "picture",
      emailVerified: "email_verified",
    },
  },
}

If userInfo is omitted, the provider tries the most common field names (id, sub, user_id, name, username, login, email, image, picture, avatar, emailVerified, email_verified, verified).

Custom profile function

For providers that return nested or unusual data, provide a profile function:

providers: {
  custom: {
    clientId: process.env.OAUTH_CUSTOM_CLIENT_ID!,
    clientSecret: process.env.OAUTH_CUSTOM_CLIENT_SECRET!,
    authorizationEndpoint: "https://custom.example.com/oauth/authorize",
    tokenEndpoint: "https://custom.example.com/oauth/token",
    userInfoEndpoint: "https://custom.example.com/api/me",
    scopes: ["read_profile"],
    profile: (data) => {
      const me = data as { account: { id: string; email: string } };
      return {
        id: me.account.id,
        email: me.account.email,
        emailVerified: true,
      };
    },
  },
}

OIDC discovery

Enable discovery and provide an issuer. The provider fetches /.well-known/openid-configuration and uses the returned endpoints:

providers: {
  authentik: {
    clientId: process.env.OAUTH_AUTHENTIK_CLIENT_ID!,
    clientSecret: process.env.OAUTH_AUTHENTIK_CLIENT_SECRET!,
    issuer: "https://authentik.example.com",
    discovery: true,
    scopes: ["openid", "email", "profile"],
  },
}

Explicit authorizationEndpoint, tokenEndpoint, userInfoEndpoint, or jwksUri override the discovered values.

ID-token verification

For OIDC providers that return an ID token and no userinfo endpoint, set useIdToken: true:

providers: {
  keycloak: {
    clientId: process.env.OAUTH_KEYCLOAK_CLIENT_ID!,
    clientSecret: process.env.OAUTH_KEYCLOAK_CLIENT_SECRET!,
    issuer: "https://keycloak.example.com/realms/master",
    discovery: true,
    useIdToken: true,
    scopes: ["openid", "email", "profile"],
  },
}

The ID token is verified against the JWKS, checked for the correct iss and aud, and then mapped through userInfo or profile.

React sign-in

Use the same authClient.signIn.social call as the built-in providers:

await authClient.signIn.social({
  provider: "keycloak",
  callbackURL: window.location.origin,
});

The redirect route is /api/auth/callback/{provider}. When providers.keycloak is configured, convex-auth routes the callback through the generic provider and mints a session.

Runtime actions

When oauth.providers is configured, auth exposes:

  • auth.signInWithRedirect
  • auth.callback

These are the same actions used by the built-in providers; the configured provider string selects the right metadata at runtime.

Was this page helpful?