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:
- A user of 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.
- The Stytch API is called.
- 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).
- Upon success, Stytch returns both a redirect_uri, owned by the Connected App, and a code parameter.
- 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.

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 B2B 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
1Implement 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:
Name | Meaning |
---|---|
response_type | The response type of the OIDC request. For Authorization Code Flow this is always code |
scope | The scope of access of the user information. Typically one or more of openid profile email phone |
client_id | The Stytch client app id, which you will receive when configuring a Client App (below) |
redirect_uri | The 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_challenge | Required for public clients, see below |
code_challenge_method | Required 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 the member_id + organization_id, session_jwt, or session_id. Prior to executing the authorization call, 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 flask import Flask, request, make_response
from stytch import B2BClient
app = Flask(__name__)
client = B2BClient(
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"
b2bapi "github.com/stytchauth/stytch-go/v16/stytch/b2b/b2bstytchapi"
"github.com/stytchauth/stytch-go/v16/stytch/b2b/idp/oauth"
)
func performAuthorize(ctx context.Context, client *b2bapi.Client, params *oauth.AuthorizeParams) (*oauth.AuthorizeResponse, error) {
return client.IDP.OAuth.Authorize(ctx, params)
}
func main() {
client, err := b2bapi.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 = StytchB2B::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.B2BClient({
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'));
Next, 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/b2b/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');
}
});
2Configure Stytch Project
We will configure your Stytch app in the Connected Apps section of the Stytch Dashboard. Enter the URL where authorization occurs into the Authorization URL field.

3Configure 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:

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:

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.

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.

At this point, off-the-shelf software should be prepared to authorize as a Connected App with Stytch.
4Implementing the server side of the Authorization Code Flow
Generally off-the-shelf software will only need to be configured with the location of the Authorization URL, Client ID, and Client Secret and it should be prepared to authorize with Stytch and this section can be skipped. If, however, you are implementing your own Connected App, or just curious, it's worth understanding the second part of the Authorization Code Flow—what happens upon the Connected App receiving the redirect.
The server-side flow is very similar to Stytch's M2M Authentication in that we will exchange the Connected App's own credentials for an access token. The difference in this case is that we have the code returned from the above steps. This token represents the ability of the client app to act on behalf of the user—in other words, conceptually the code can be thought of as representing permission from the user to enable the app—which must also authorize itself in the request—to see their data and act on their behalf.
To perform the token exchange, just as in M2M Authentication, we will utilize an API call to Stytch.
The request should contain these parameters:
Name | Meaning |
---|---|
grant_type | The type of auth grant we're seeking. For Authorization Code Flow this is always authorization_code |
code | The code received in a parameter of the request sent to the Redirect URL |
client_id | The Stytch client app id, which you will receive when configuring a Client App (below) |
client_secret | The Stytch Connected App's Client Secret - required for confidential clients |
redirect_uri | While there is no redirect issued from this request redirect_uri must be present and identical to the value used in the first part of the flow |
code_verifier | Required for public clients, see below |
For example, here is what such a request might look like using some of our SDKs:
from stytch import B2BClient
client = B2BClient(
project_id=f"{os.getenv('STYTCH_PROJECT_ID')}",
secret=f"{os.getenv('STYTCH_SECRET')}",
)
resp = client.m2m.token(
client_id=f"{os.getenv('STYTCH_CLIENT_ID')}",
client_secret=f"{os.getenv('STYTCH_CLIENT_SECRET')}",
scopes=["read:users", "write:users"]
)
package main
import (
"context"
"log"
"github.com/stytchauth/stytch-go/v16/stytch/b2b/b2bstytchapi"
"github.com/stytchauth/stytch-go/v16/stytch/b2b/m2m"
)
func main() {
client, err := b2bstytchapi.NewClient(
"PROJECT_ID",
"SECRET",
)
if err != nil {
log.Fatalf("error instantiating client: %v", err)
}
resp, err := client.M2M.Token(
context.Background(),
&m2m.TokenParams{
ClientID: "${clientId}",
ClientSecret: "${clientSecret}",
Scopes: []string{"read:users", "write:users"},
},
)
if err != nil {
log.Println(err)
}
log.Println(resp)
}
require 'stytch'
client = StytchB2B::Client.new(
project_id: "PROJECT_ID",
secret: "SECRET"
)
resp = client.m2m.token(
client_id: "${clientId}",
client_secret: "${clientSecret}",
scopes: ["read:users", "write:users"]
)
const stytch = require('stytch');
const client = new stytch.B2BClient({
project_id: "PROJECT_ID",
secret: "SECRET",
});
const params = {
client_id: "${clientId}",
client_secret: "${clientSecret}",
scopes: ['read:users', 'write:users'],
};
client.m2m
.token(params)
.then((resp) => {
console.log(resp);
})
.catch((err) => {
console.log(err);
});
The response from this request will be a JSON payload with the access token for authorizing requests from the Client App.
Public apps: Authorization Code Flow with PKCE
A potential vulnerability exists in the OIDC flow due to the nature of it requiring two asynchronous requests to complete. A malicious party could intercept the code returned in the first step of authorization and attempt to use it to authorize their own app before the legitimate app exchanges it for an access_token. In the case of the Authorization Code Flow this is prevented by the fact that the pre-shared Client Secret is not known to the malicious actor so they don't have enough information to do the complete code exchange.
Due to the nature of, for instance, in-browser single-page apps with no backend, or mobile apps where secrets can be extracted from the app if distributed within the app, there is sometimes no location to safely store the client_secret. We call these "Public" apps. For these apps, we implement a variant of the Authorization Code Flow using PKCE (Proof Key for Code Exchange). This flow uses an extension to the Authorization Code Flow to ensure both requests used during the Authorization Code Flow come from the same Connected App.
When beginning the authorization request, the Connected App will construct a code_verifier (for example, generating a cryptographically random value) and use it to generate a code_challenge. The code_challenge is the SHA-256 hash of the code_verifier. On the initial request to Stytch the additional URL parameters of code_challenge and code_challenge_method should be sent:
Name | Meaning |
---|---|
code_challenge | The SHA-256 hash of the value of the code_verifier |
code_challenge_method | The method of hashing the code_verifier. For Stytch this should always be S256 |
Upon receipt of this request, Stytch will save the value of code_challenge for later. After the user has granted permission for access, Stytch will return a code from this step just as in the regular Authorization Code Flow.
During the code exchange part of the process, the Public app will exchange the code for an access_token as the server would do in the Authorization Code Flow. However, the Public app has no access to a client_secret so cannot send that parameter. Instead, the Public app also sends the original code_verifier with this request:
Name | Meaning |
---|---|
code_verifier | The original value used to generate the code_challenge |
Stytch will verify that its saved code_challenge was generated from this code_verifier before generating and returning credentials for the app. In this way it can be sure that the app that requested the code in the first request is the same app that is requesting the access_token in the second request.
OIDC Compliance and the issuer field
To ensure your Stytch-powered application is fully OpenID Connect (OIDC) compliant, the issuer field in your JWTs must be an https URL that matches the domain of your identity provider.
How Stytch determines the issuer:
- If you use a custom domain (CNAME):
- When your users authenticate via a custom domain (e.g., https://login.yourcompany.com), Stytch will set the issuer in your JWTs to your custom domain.
- This is fully OIDC compliant and required by many OIDC clients and libraries.
- If you do not use a custom domain:
- The issuer will default to stytch.com/$project_id.
- These formats are not fully OIDC compliant and may cause compatibility issues with OIDC clients that strictly enforce the spec.
To get a fully OIDC-compliant issuer:
- Set up a custom domain (CNAME) in your DNS that points to Stytch and configure Stytch to recognize this domain.
- Configure your application to make all API calls to Stytch over this domain.
- Use this domain for all authentication flows.
Why does this matter?
- Many OIDC clients and libraries will reject tokens if the issuer does not match the expected https URL.
- Using a custom domain ensures maximum compatibility and security for your integrations.
Note: If you are using Stytch's default domain and encounter issues with OIDC integrations, setting up a custom domain is the recommended solution.
What's next
Read more about our other offerings: