/
Contact usSee pricingStart building
Node
​

    About Stytch

    Introduction
    Integration Approaches
      Full-stack overview
      Frontend (pre-built UI)
      Frontend (headless)
      Backend
    Migrations
      Migration overview
      Migrating users statically
      Migrating users dynamically
      Additional migration considerations
      Zero-downtime deployment
      Defining external IDs for users
      Migrating from Stytch Consumer to B2B
      Exporting from Stytch
    Custom Domains
      Overview

    Authentication

    DFP Protected Auth
      Overview
      Setting up DFP Protected Auth
      Handling challenges
    Magic Links
    • Email Magic Links

      • Getting started with the API
        Getting started with the SDK
        Replacing your password reset flow
        Building an invite user flow
        Add magic links to an existing auth flow
        Adding PKCE to a Magic Link flow
        Magic Link redirect routing
    • Embeddable Magic Links

      • Getting started with the API
    MFA
      Overview
      Backend integration
      Frontend integration
      Remembered device flow
    Mobile Biometrics
      Overview
    M2M Authentication
      Authenticate an M2M Client
      Rotate client secrets
      Import M2M Clients from Auth0
    OAuth
    • Identity providers

      • Overview
        Provider setup
      Getting started with the API (Google)
      Add Google One Tap via the SDK
      Email address behavior
      Adding PKCE to an OAuth flow
    Connected Apps
      Overview
      Getting started with the SDK
      Getting started with the API
      Client types
      OAuth scopes
    • Integration Guides

      • Integrate with an Existing Auth System
        MCP Authorization Overview
        Integrate with MCP servers deployed on Cloudflare
        Integrate with MCP servers on Vercel
        Integrate with CLI Apps
        Integrate with AI agents
    • Resources

      • Consent Management
    Passcodes
      Getting started with the API
      Getting started with the SDK
    • Toll fraud

      • What is SMS toll fraud?
        How you can prevent toll fraud
      Unsupported countries
    Passkeys & WebAuthn
    • Passkeys

      • Passkeys overview
        Set up Passkeys with the frontend SDK
    • WebAuthn

      • Getting started with the API
        Getting started with the SDK
    Passwords
      Getting started with the API
      Getting started with the SDK
      Password strength policy
    • Email verification

      • Overview
        Email verification before password creation
        Email verification after password creation
    Sessions
      How to use sessions
      Backend integrations
      Frontend integrations
      Custom claims
      Custom claim templates
      Session tokens vs JWTs
      How to use Stytch JWTs
    TOTP
      Getting started with the API
      Getting started with the SDK
    Web3
      Getting started with the API
      Getting started with the SDK
    Trusted Auth Tokens
      Overview
      Getting Started with External IDPs
      Getting Started with Custom Auth Factors
    Device History
      New Device Notifications

    RBAC

    Resources
      Overview
      Role assignment
    Integration Guides
      Start here
      Backend integration
      Headless frontend integration
      (Legacy) Implement RBAC with metadata

    3rd Party Integrations

    Planetscale
    Supabase
    Feathery
    Unit

    Testing

    E2E testing
    Sandbox values
Get support on SlackVisit our developer forum

Contact us

Consumer Authentication

/

Guides

/

Authentication

/

Connected Apps

/

Getting started with the API

Allow apps to log in with your Stytch-powered app Using the API

In this guide, you'll learn how to configure your suite of OIDC applications to log in and perform actions on behalf of your users with your Stytch-powered app utilizing our API. This enables users of your Stytch app to use their in-house tools, external integrations, desktop apps, AI agents, any OIDC-compatible app to use your Stytch application as an Authorization Server, reducing user information duplication and simplifying the process of integrating Connected Apps with your app.

Fundamentals

OIDC is an industry standard for allowing applications to ask an Authorization Server—your Stytch-powered application—to verify the identity of a user. By logging in a user in this way we can consolidate information about a user’s authentication and authorization in Stytch and manage user data in one centralized location across multiple apps.

To ground our discussion about Connected Apps we will discuss the OIDC Authorization Code Flow (in the Connected App configuration, this is a "confidential app", described below). At the end of the article we will cover modification of this flow for "public apps" - Authorization Code Flow with PKCE.

The general flow for the Authorization Code Flow for a Connected App proceeds as follows:

  1. A user using the Connected App wishes to authenticate this app with your Stytch-powered Authorization Server in order to access user information (what we're implementing here). They click a button or follow a link to an Authorization URL (configured below) owned by your app.
  2. The Stytch API is called.
  3. The user may be prompted to consent to giving the Connected App permission to access their data (depending on whether the Connected App is "First Party" or "Third Party", described below).
  4. Upon success, Stytch returns both a redirect_uri, owned by the Connected App, and a code parameter.
  5. The Connected App uses this code parameter, along with its client_id and client_secret, to request an access_token (and optionally an id_token or refresh_token) from Stytch.

Once these steps are complete, the Connected App is logged in and can use the access_token to access user data.

Flow diagram of Connected Apps

Before you start

In order to complete this guide, you'll need:

  • A Stytch project. If you don't have one already, or would like to create a new one, in the Dashboard, click on your existing project name in the top left corner of the Dashboard, click Create Project, and then select Consumer Authentication.
  • An application using either the Stytch Frontend SDKs, or the Stytch Backend APIs.
  • A Relying Party app (the Connected App) that wishes to receive user information from your application.

Integration steps

1
Implement an Entry Point

In OIDC Authorization Code Flow, to begin the login flow the Connected App will make a request to an Authorization URL in your Stytch-powered application. This page will need to implement a mechanism which assists carrying out the flow. The exact form of this may be something as simple as a backend endpoint which handles the calls, or as complex as a dedicated frontend component. The entry point should parse out some URL parameters to supply to the Stytch OAuth Authorization flow.

The redirect to this page should include these URL parameters as defined in the OIDC specification:

NameMeaning
response_typeThe response type of the OIDC request. For Authorization Code Flow this is always code
scopeThe scope of access of the user information. Typically one or more of openid profile email phone
client_idThe Stytch client app id, which you will receive when configuring a Client App (below)
redirect_uriThe URI owned by the Client App to call back when Stytch verifies access is granted. After the initial steps of the login process, Stytch redirects to this URI. You will configure this in Stytch when configuring a Client App (below)
state (optional)An opaque value (not used by the flow) which will be passed back and forth throughout the flow. This can be used in some enhanced security scenarios or to store information about user state that should be preserved throughout the flow.
code_challengeRequired for public clients, see below
code_challenge_methodRequired for public clients, see below

This page will need to initiate a call to Start OAuth Authorization. This will return information about which scopes the user should be prompted to consent to.

The result of this call should be used to prompt a user for consent to access these pieces of data. Upon confirmation by a user, the authorization flow can be finalized by calling the Submit OAuth Authorize endpoint.

Upon a successful submission Stytch will respond with a redirect_uri and a code for the Connected App to complete the flow on its backend.

For this flow to succeed, the user must have an active session, provided via either a session_token, session_jwt, or session_id. Prior to rendering the component, you'll want to check for a valid session and redirect to the login page with a return_to parameter if the user is not authenticated.

A sample of how to call these endpoints can be seen here, broken into three focused steps.

First, start authorization and check if consent is required. If consent is not required, we can simply complete an authorization call and continue on. If consent is required, we should move on to informing the user what scopes are requested in step 2.

import os
from urllib.parse import urlencode
from flask import Flask, request, make_response
from stytch import Client

app = Flask(__name__)

client = Client(
    project_id=os.getenv("STYTCH_PROJECT_ID"),
    secret=os.getenv("STYTCH_SECRET"),
)

def perform_authorize(
    consent_granted,
    client_id,
    redirect_uri,
    response_type,
    scopes,
    state,
    code_challenge,
    code_challenge_method,
    session_jwt,
):
    return client.idp.oauth.authorize(
        consent_granted=consent_granted,
        client_id=client_id,
        redirect_uri=redirect_uri,
        response_type=response_type,
        scopes=scopes,
        state=state,
        code_challenge=code_challenge,
        code_challenge_method=code_challenge_method,
        session_jwt=session_jwt,
    )

@app.get("/oauth/authorize")
def oauth_authorize():
    response_type = request.args.get("response_type", "code")
    scope_str = request.args.get("scope", "")
    client_id = request.args.get("client_id")
    redirect_uri = request.args.get("redirect_uri")
    state = request.args.get("state")
    code_challenge = request.args.get("code_challenge")
    code_challenge_method = request.args.get("code_challenge_method")

    scopes = [s for s in scope_str.split(" ") if s]
    session_jwt = request.cookies.get("stytch_session_jwt")

    base_params = {
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "response_type": response_type,
        "scopes": scopes,
        "state": state,
        "code_challenge": code_challenge,
        "code_challenge_method": code_challenge_method,
        "session_jwt": session_jwt,
    }

    start_resp = client.idp.oauth.authorize_start(**base_params)

    if start_resp.consent_required:
        # See Step 2: Render consent screen
        return make_response("Consent required", 200)

    auth_resp = perform_authorize(consent_granted=True, **base_params)

    resp = make_response("", 307)
    resp.headers["Location"] = auth_resp.redirect_uri
    return resp

if __name__ == "__main__":
    app.run(port=3000)
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/stytchauth/stytch-go/v16/stytch/consumer/idp/oauth"
	"github.com/stytchauth/stytch-go/v16/stytch/consumer/stytchapi"
)

func performAuthorize(ctx context.Context, client *stytchapi.Client, params *oauth.AuthorizeParams) (*oauth.AuthorizeResponse, error) {
	return client.IDP.OAuth.Authorize(ctx, params)
}

func main() {
	client, err := stytchapi.NewClient(os.Getenv("STYTCH_PROJECT_ID"), os.Getenv("STYTCH_SECRET"))
	if err != nil { log.Fatal(err) }

	http.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		responseType := firstOrDefault(q["response_type"], "code")
		scopeStr := firstOrDefault(q["scope"], "")
		clientID := firstOrDefault(q["client_id"], "")
		redirectURI := firstOrDefault(q["redirect_uri"], "")
		state := firstOrDefault(q["state"], "")
		codeChallenge := firstOrDefault(q["code_challenge"], "")
		codeChallengeMethod := firstOrDefault(q["code_challenge_method"], "")
		scopes := []string{}
		if scopeStr != "" { scopes = strings.Fields(scopeStr) }
		var sessionJWT string
		if c, err := r.Cookie("stytch_session_jwt"); err == nil { sessionJWT = c.Value }

		baseParams := &oauth.AuthorizeStartParams{
			ClientID:            clientID,
			RedirectURI:         redirectURI,
			ResponseType:        responseType,
			Scopes:              scopes,
			State:               state,
			CodeChallenge:       codeChallenge,
			CodeChallengeMethod: codeChallengeMethod,
			SessionJWT:          sessionJWT,
		}

		startResp, err := client.IDP.OAuth.AuthorizeStart(r.Context(), baseParams)
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}

		if startResp.ConsentRequired {
			// See Step 2: Render consent screen
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("Consent required"))
			return
		}

		authResp, err := performAuthorize(r.Context(), client, &oauth.AuthorizeParams{
			ConsentGranted:      true,
			ClientID:            baseParams.ClientID,
			RedirectURI:         baseParams.RedirectURI,
			ResponseType:        baseParams.ResponseType,
			Scopes:              baseParams.Scopes,
			State:               baseParams.State,
			CodeChallenge:       baseParams.CodeChallenge,
			CodeChallengeMethod: baseParams.CodeChallengeMethod,
			SessionJWT:          baseParams.SessionJWT,
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			return
		}
		w.Header().Set("Location", authResp.RedirectURI)
		w.WriteHeader(http.StatusTemporaryRedirect)
	})

	log.Println("Server listening on :3000")
	log.Fatal(http.ListenAndServe(":3000", nil))
}

func firstOrDefault(values []string, def string) string {
	if len(values) > 0 && values[0] != "" { return values[0] }
	return def
}
require 'sinatra'
require 'stytch'

client = Stytch::Client.new(
  project_id: ENV['STYTCH_PROJECT_ID'],
  secret: ENV['STYTCH_SECRET']
)

def perform_authorize(client:, consent_granted:, client_id:, redirect_uri:, response_type:, scopes:, state:, code_challenge:, code_challenge_method:, session_jwt:)
  client.idp.oauth.authorize(
    consent_granted: consent_granted,
    client_id: client_id,
    redirect_uri: redirect_uri,
    response_type: response_type,
    scopes: scopes,
    state: state,
    code_challenge: code_challenge,
    code_challenge_method: code_challenge_method,
    session_jwt: session_jwt,
  )
end

get '/oauth/authorize' do
  response_type = params['response_type'] || 'code'
  scope_str = params['scope'].to_s
  client_id = params['client_id']
  redirect_uri = params['redirect_uri']
  state = params['state']
  code_challenge = params['code_challenge']
  code_challenge_method = params['code_challenge_method']

  scopes = scope_str.split(/\s+/).reject(&:empty?)
  session_jwt = request.cookies['stytch_session_jwt']

  base_params = {
    client_id: client_id,
    redirect_uri: redirect_uri,
    response_type: response_type,
    scopes: scopes,
    state: state,
    code_challenge: code_challenge,
    code_challenge_method: code_challenge_method,
    session_jwt: session_jwt,
  }

  start_resp = client.idp.oauth.authorize_start(**base_params)

  if start_resp.consent_required
    # See Step 2: Render consent screen
    status 200
    return 'Consent required'
  end

  auth_resp = perform_authorize(client: client, consent_granted: true, **base_params)

  status 307
  headers 'Location' => auth_resp.redirect_uri
  body ''
end
const express = require('express');
const cookieParser = require('cookie-parser');
const stytch = require('stytch');

const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));

const client = new stytch.Client({
  project_id: process.env.STYTCH_PROJECT_ID,
  secret: process.env.STYTCH_SECRET,
});

async function performAuthorize(params) {
  return client.idp.oauth.authorize(params);
}

app.get('/oauth/authorize', async (req, res) => {
  try {
    const {
      response_type = 'code',
      scope = '',
      client_id,
      redirect_uri,
      state,
      code_challenge,
      code_challenge_method,
    } = req.query;

    const scopes = String(scope).split(' ').filter(Boolean);
    const session_jwt = req.cookies?.stytch_session_jwt || undefined;

    const baseParams = {
      client_id,
      redirect_uri,
      response_type,
      scopes,
      state,
      code_challenge,
      code_challenge_method,
      session_jwt,
    };

    const startResp = await client.idp.oauth.authorizeStart(baseParams);

    if (startResp.consent_required) {
      // See Step 2: Render consent screen
      return res.status(200).send('Consent required');
    }

    const authResp = await performAuthorize({
      consent_granted: true,
      ...baseParams,
    });

    res.status(307).set('Location', authResp.redirect_uri).send();
  } catch (err) {
    console.error(err);
    res.status(400).send('Authorization failed');
  }
});

app.listen(3000, () => console.log('Server listening on :3000'));

If consent is required, we need to render a page informing the user what scopes are requested. If consent is granted, the authorization flow may continue seen in step 3.

def render_consent_html(scopes, hidden):
    return f"""
<html>
  <body>
    <h1>Consent required</h1>
    <p>This app is requesting access to:</p>
    <ul>
      {''.join([f'<li>{s}</li>' for s in scopes])}
    </ul>
    <form method=\"post\" action=\"/oauth/authorize/submit\">{''.join([f'<input type=\"hidden\" name=\"{k}\" value=\"{v}\" />' for k, v in hidden.items()])}<button type=\"submit\">Continue</button></form>
  </body>
</html>
"""
import (
	"fmt"
	"net/http"
)

func renderConsentPage(w http.ResponseWriter, scopes []string, hidden map[string]string) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, "<html><body><h1>Consent required</h1><p>This app is requesting access to:</p><ul>")
	for _, s := range scopes { fmt.Fprintf(w, "<li>%s</li>", s) }
	fmt.Fprintf(w, "</ul><form method=\"post\" action=\"/oauth/authorize/submit\">")
	for k, v := range hidden { fmt.Fprintf(w, "<input type=hidden name=\"%s\" value=\"%s\" />", k, v) }
	fmt.Fprintf(w, "<button type=submit>Continue</button></form></body></html>")
}
def render_consent_html(scopes:, hidden: {})
  <<~HTML
    <html>
      <body>
        <h1>Consent required</h1>
        <p>This app is requesting access to:</p>
        <ul>
          #{scopes.map { |s| "<li>#{s}</li>" }.join}
        </ul>
        <form method="post" action="/oauth/authorize/submit">
          #{hidden.map { |k, v| "<input type=\"hidden\" name=\"#{k}\" value=\"#{v}\" />" }.join}
          <button type="submit">Continue</button>
        </form>
      </body>
    </html>
  HTML
end
function renderConsentPage(scopes, hidden) {
  const hiddenInputs = Object.entries(hidden)
    .map(([k, v]) => `<input type="hidden" name="${k}" value="${String(v)}" />`)
    .join('');
  return `
<html>
  <body>
    <h1>Consent required</h1>
    <p>This app is requesting access to:</p>
    <ul>
      ${scopes.map((s) => `<li>${s}</li>`).join('')}
    </ul>
    <form method="post" action="/oauth/authorize/submit">
      ${hiddenInputs}
      <button type="submit">Continue</button>
    </form>
  </body>
</html>`;
}

Finally, the authorization request may then be submitted, completing authorization.

from flask import request, make_response

@app.route("/oauth/authorize/submit", methods=["POST", "GET"])
def oauth_authorize_submit():
    response_type = request.values.get("response_type", "code")
    scope_str = request.values.get("scope", "")
    client_id = request.values.get("client_id")
    redirect_uri = request.values.get("redirect_uri")
    state = request.values.get("state")
    code_challenge = request.values.get("code_challenge")
    code_challenge_method = request.values.get("code_challenge_method")
    consent_granted = (request.values.get("consent_granted", "false").lower() == "true")

    scopes = [s for s in scope_str.split(" ") if s]
    session_jwt = request.cookies.get("stytch_session_jwt")

    auth_resp = perform_authorize(
        consent_granted=consent_granted,
        client_id=client_id,
        redirect_uri=redirect_uri,
        response_type=response_type,
        scopes=scopes,
        state=state,
        code_challenge=code_challenge,
        code_challenge_method=code_challenge_method,
        session_jwt=session_jwt,
    )

    resp = make_response("", 307)
    resp.headers["Location"] = auth_resp.redirect_uri
    return resp
import (
	"net/http"
	"strings"
	"github.com/stytchauth/stytch-go/v16/stytch/consumer/idp/oauth"
)

http.HandleFunc("/oauth/authorize/submit", func(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	responseType := r.Form.Get("response_type")
	if responseType == "" { responseType = "code" }
	scopeStr := r.Form.Get("scope")
	clientID := r.Form.Get("client_id")
	redirectURI := r.Form.Get("redirect_uri")
	state := r.Form.Get("state")
	codeChallenge := r.Form.Get("code_challenge")
	codeChallengeMethod := r.Form.Get("code_challenge_method")
	consentGranted := strings.ToLower(r.Form.Get("consent_granted")) == "true"

	scopes := []string{}
	if scopeStr != "" { scopes = strings.Fields(scopeStr) }
	var sessionJWT string
	if c, err := r.Cookie("stytch_session_jwt"); err == nil { sessionJWT = c.Value }

	authResp, err := performAuthorize(r.Context(), client, &oauth.AuthorizeParams{
		ConsentGranted:      consentGranted,
		ClientID:            clientID,
		RedirectURI:         redirectURI,
		ResponseType:        responseType,
		Scopes:              scopes,
		State:               state,
		CodeChallenge:       codeChallenge,
		CodeChallengeMethod: codeChallengeMethod,
		SessionJWT:          sessionJWT,
	})
	if err != nil { http.Error(w, err.Error(), http.StatusBadRequest); return }
	w.Header().Set("Location", authResp.RedirectURI)
	w.WriteHeader(http.StatusTemporaryRedirect)
})
post '/oauth/authorize/submit' do
  response_type = params['response_type'] || 'code'
  scope_str = params['scope'].to_s
  client_id = params['client_id']
  redirect_uri = params['redirect_uri']
  state = params['state']
  code_challenge = params['code_challenge']
  code_challenge_method = params['code_challenge_method']
  consent_granted = (params['consent_granted'].to_s.downcase == 'true')

  scopes = scope_str.split(/\s+/).reject(&:empty?)
  session_jwt = request.cookies['stytch_session_jwt']

  auth_resp = perform_authorize(
    client: client,
    consent_granted: consent_granted,
    client_id: client_id,
    redirect_uri: redirect_uri,
    response_type: response_type,
    scopes: scopes,
    state: state,
    code_challenge: code_challenge,
    code_challenge_method: code_challenge_method,
    session_jwt: session_jwt,
  )

  status 307
  headers 'Location' => auth_resp.redirect_uri
  body ''
end
app.post('/oauth/authorize/submit', async (req, res) => {
  try {
    const {
      response_type = 'code',
      scope = '',
      client_id,
      redirect_uri,
      state,
      code_challenge,
      code_challenge_method,
      consent_granted,
    } = req.body;

    const scopes = String(scope).split(' ').filter(Boolean);
    const session_jwt = req.cookies?.stytch_session_jwt || undefined;

    const authResp = await performAuthorize({
      consent_granted: String(consent_granted).toLowerCase() === 'true',
      client_id,
      redirect_uri,
      response_type,
      scopes,
      state,
      code_challenge,
      code_challenge_method,
      session_jwt,
    });

    res.status(307).set('Location', authResp.redirect_uri).send();
  } catch (err) {
    console.error(err);
    res.status(400).send('Authorization failed');
  }
});

2
Configure Your Stytch Project

We will configure your Stytch app in the Connected Apps section of the Stytch Dashboard. Enter the URL where the component is mounted into the Authorization URL field.

Entering in the Authorization URL

3
Configure the Connected App(s) information

After Stytch handles verification of the user and the application’s ability to access the user’s information, Stytch will issue a redirect. This redirect is intended to go to a URL which should be supplied by the Connected App and provides some short-lived credentials that allow the Connected App to complete the authorization flow.

To set this up, each Connected App is also configured in the Connected Apps section of the Dashboard:

Create a new client app

Upon creating a new Connected App we specify what type of user consent and OIDC flow the Connected App expects. Stytch supports connecting a few types of client apps which vary based on how much user consent is appropriate and specifics of the OIDC flow:

Specify the type of client app

Continue to configure the Connected App on the next screen. Make sure to take a note of the “Client ID” and the “Client secret” (for confidential apps); these will be needed by the Connecting App to complete the Authorization Code Flow.

Configure the client app

The Connected App will also supply a Redirect URL for receiving the login information from Stytch after the initial login is complete. This should be entered into the “Redirect URLs” field.

Create a redirect URL for the client app

At this point, off-the-shelf software should be prepared to authorize as a Connected App with Stytch.

Fundamentals

Before you start

Integration steps

1.

Implement an Entry Point

2.

Configure Your Stytch Project

3.

Configure the Connected App(s) information