Docs

Add social sign-in

Let users sign in with Google or any OpenID Connect provider, set up with the zitadel CLI.

A social sign-in provider, such as "Sign in with Google", is one more sign-in method on a user schema. auth-method sso enable writes the connection, turns it on in the schema and login flow, and stores the provider's credentials on the project. deploy then publishes it.

Register the app with the provider

Create an OAuth client (for Google: a "Web application" client in the Google Cloud console) and keep its client id and client secret. The provider asks for a redirect URI. sso enable prints the right one as callback_uri; locally, for Google, it is:

http://localhost:3000/__nextgen/idp/google/callback

Each connection has its own callback URI, on your app's origin, ending in the connection's slug. Register it with the provider exactly as printed. A URI from another connection doesn't work.

Enable Google

npx @zitadel/cli@latest auth-method sso enable --provider google

The command prompts for the client id and the client secret. In a script or an agent, pass the client id as a flag and pipe the secret in on stdin; the secret never goes in a flag:

npx @zitadel/cli@latest auth-method sso enable --provider google \
  --client-id 1234-abc.apps.googleusercontent.com \
  --non-interactive --json < secret.txt

Pass --schema <name> when the project has more than one user schema. The JSON result names what it did:

{
  "provider": "google",
  "schema": "default-human-user",
  "connection": { "action": "create", "slug": "google", "file": ".zitadel/idps/google.json" },
  "callback_uri": "http://localhost:3000/__nextgen/idp/google/callback",
  "changed": [".zitadel/schemas/default-human-user.json", ".zitadel/flows/default-login.json"],
  "client_id": { "variable": "GOOGLE_CLIENT_ID", "published": "stored" },
  "secret": { "variable": "GOOGLE_CLIENT_SECRET", "published": "stored" },
  "next_commands": ["npx @zitadel/cli@latest deploy --dry-run", "npx @zitadel/cli@latest deploy"]
}

Then publish the change:

npx @zitadel/cli@latest deploy --dry-run
npx @zitadel/cli@latest deploy

Reload the app: the sign-in, registration and password steps show a "Continue with Google" button.

Any other OpenID Connect provider

--provider custom connects any provider that publishes OIDC discovery (<issuer>/.well-known/openid-configuration):

npx @zitadel/cli@latest auth-method sso enable --provider custom \
  --slug acme-sso --display-name 'Acme SSO' \
  --issuer https://sso.acme.example \
  --scopes 'openid profile email groups' \
  --client-id abc123 --non-interactive < secret.txt
  • --slug names the connection. Schemas and flows refer to it by this name, and it is part of the callback URI (/__nextgen/idp/acme-sso/callback).
  • --display-name is the text on the sign-in button.
  • --issuer is the provider's issuer URL; the server finds its endpoints by discovery.
  • --scopes is optional, separated by spaces or commas, and must include openid. The default is openid profile email.

The credentials are stored as <SLUG>_CLIENT_ID and <SLUG>_CLIENT_SECRET, for example ACME_SSO_CLIENT_ID.

During setup

setup takes the same provider while it scaffolds the app, so a new app starts with social sign-in. The OAuth client must already exist; pipe its secret in on stdin:

npx @zitadel/cli@latest setup --server local --framework next \
  --sso google --sso-client-id 1234-abc.apps.googleusercontent.com \
  --non-interactive --json < secret.txt

For a custom provider, use --sso custom with --sso-slug, --sso-display-name, --sso-issuer and optionally --sso-scopes. Setup reports each connection under data.sso[], with its callback_uri to register.

What the command writes

  • .zitadel/idps/<slug>.json, the connection: protocol, issuer, scopes, claim mapping, and the credentials as references, ${{ GOOGLE_CLIENT_ID }} and ${{ GOOGLE_CLIENT_SECRET }}. The file holds no secret, so commit it.
  • The user schema, .zitadel/schemas/<schema>.json: the slug is added to x-auth-methods.sso.providers, and x-auth-methods.sso.enabled is set to true.
  • The login flow, .zitadel/flows/<flow>.json: the slug is added to sso_providers on the steps that offer sign-in, and two steps are added. register-sso creates the user on a first sign-in with the provider. sso-conflict handles a provider account whose email already belongs to a user (see Limits).
  • Two project variables, GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET (the secret encrypted). They live on the project, not in .env.local: the server exchanges the code with the provider, so the app never needs the secret.

The variables are the one thing sso enable publishes before deploy. When a credential did not reach the project, for example because the secret was not piped in or the server refused it, published is deferred or failed and next_commands starts with the variables set command that stores it:

npx @zitadel/cli@latest variables set GOOGLE_CLIENT_SECRET --project-level --secret < secret.txt

To remove a provider, run auth-method sso disable --provider google; a custom one is named by its slug, as in --provider acme-sso. Then deploy --dry-run and deploy.

Fields fixed after the first apply

Once a connection has been applied, these fields identify it and the users who signed in through it, and can't change:

  • slug: a file with another slug is another connection
  • protocol
  • oidc.issuer
  • subject_claim

deploy refuses an edit to one of them:

Error E_VALIDATION: .zitadel/idps/google.json: subject_claim of the connection "google" cannot be edited

Restore the stored value, or add a new connection file with its own slug. Everything else in the file, such as display_name or scopes, can be edited and applied.

Test against a local OIDC provider

A provider running on your machine, such as a Keycloak or Dex container on localhost, works with the server zitadel start runs. A server refuses outgoing requests to loopback addresses unless NEXTGEN_HTTPCLIENT_ALLOW_LIST allows them, and start sets it to localhost,127.0.0.0/8,::1/128 unless .env.local, .env or your shell already sets it. data.runtime.env.injected in start --json lists the variable.

Two cases still can't reach the provider, and sso enable warns about both:

  • The Docker runtime. Inside the container, localhost is the container itself. Start the server with zitadel start --runtime binary instead.
  • Any other server, or a local one with NEXTGEN_HTTPCLIENT_ALLOW_LIST= set empty, which turns the default off.

setup --sso custom doesn't know yet which server will run, so it warns about any localhost issuer.

When the server can't reach the provider, the widget only says "This sign-in provider is not available right now", and zitadel logs --level warn shows a sso provider unavailable warning with idp.discovery_failed and address is denied by 'localhost'. After changing the variable in .env.local, stop the server and start it again: start alone keeps a running server as it is.

npx @zitadel/cli@latest stop
npx @zitadel/cli@latest start

Only allow loopback on your machine. A deployed server must not reach its own network on a URL that a project can configure.

Limits

  • No account linking yet. A provider account is never linked to an existing user. When someone who registered with a password signs in with Google using the same email, the flow lands on the sso-conflict step, which asks them to sign in with their password or passkey instead. That happens on every Google sign-in, because the two stay separate. A user who started with Google signs in with Google.
  • OpenID Connect only. The CLI knows Google by name; any other provider needs --provider custom and an issuer with OIDC discovery. A plain OAuth 2.0 provider, such as GitHub, can't be connected yet.

Was this helpful?