Our frontend SDK provide automatic session management, prebuilt login or signup, multi-step flow handling, and more. They can also be used alongside our backend API or SDKs.
Quickstart
This quickstart guides you through building a Discovery login flow. Select a framework:- Node.js
- Go
- Python
- Ruby on Rails
- Java
Example apps
Node.js code examples
Web demo
Hosted Node.js demo
Node.js SDK
Stytch Node.js library
API reference
API & SDK references
Before you start
- Have your
public_tokenandsecret_keyfrom your Stytch project & environment. - While the guide uses Express, the Node.js SDK is framework agnostic.
Install the Node.js SDK and configure API keys
- Install the SDK in your Express environment:
npm
Report incorrect code
Copy
Ask AI
npm install stytch
- Then add your Stytch API keys to your project’s
.envfile:
.env
Report incorrect code
Copy
Ask AI
STYTCH_PROJECT_ID="YOUR_STYTCH_PROJECT_ID"
STYTCH_SECRET="YOUR_STYTCH_PROJECT_SECRET"
# Use your Project's 'live' or any available 'test' credentials
Set up your app and login routes
- Initialize the Stytch client with your environment variables.
- Create a
/loginroute that takes in a user’s email address and initiates the login or signup flow.
POST '/login'
Report incorrect code
Copy
Ask AI
const express = require('express');
const stytch = require('stytch');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
app.use(express.json());
/**
* Express session management
*/
app.use(session({
resave: true,
saveUninitialized: false,
secret: 'session-signing-secret',
cookie: {maxAge: 60000}
}));
const stytchClient = new stytch.B2BClient({
project_id: process.env.STYTCH_PROJECT_ID,
secret: process.env.STYTCH_SECRET,
});
// Key to retrieve the session token from express-session
// Referenced as req.session[StytchSessionToken]
const StytchSessionToken = 'stytch_session_token'
app.post('/login', (req, res) => {
const email = req.body.email;
stytchClient.magicLinks.email.discovery.send({
email_address: email
})
.then(response => {
res.json(response)
})
.catch(err => {
res.status(500).send(err.toString())
});
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
Add a route to handle Stytch's redirect callback
- Stytch calls the authenticate redirect URL with a token to complete a user’s authentication flow.
- Add an
/authenticateroute to handle the redirect callback in your app.
GET '/authenticate'
Report incorrect code
Copy
Ask AI
app.get('/authenticate', async (req, res) => {
const token = req.query.token;
const tokenType = req.query.stytch_token_type;
// Handle Discovery authentication.
if (tokenType !== 'discovery') {
console.error(`Unrecognized token type: '${tokenType}'`);
res.status(400).send();
return;
}
const authResp = await stytchClient.magicLinks.discovery.authenticate({
discovery_magic_links_token: token,
});
if (authResp.status_code !== 200) {
console.error('Authentication error')
res.status(500).send();
return;
}
});
Log in to an organization
- At this point, a user has been authenticated but still needs to specify an Organization to log in to and begin a session.
- Stytch provides an
intermediate_session_tokenthat you can use to preserve the user’s authentication state while you provide them with options to proceed (e.g. selecting from a list of organizations). - For this example, we’ll add logic to automatically sign in to an existing organization or create one if none already exist:
GET '/authenticate'
Report incorrect code
Copy
Ask AI
app.get('/authenticate', async (req, res) => {
const token = req.query.token;
const tokenType = req.query.stytch_token_type;
// Handle Discovery authentication.
if (tokenType !== 'discovery') {
console.error(`Unrecognized token type: '${tokenType}'`);
res.status(400).send();
return;
}
const authResp = await stytchClient.magicLinks.discovery.authenticate({
discovery_magic_links_token: token,
});
if (authResp.status_code !== 200) {
console.error('Authentication error')
res.status(500).send();
return;
}
// Sign into existing Organization if already Member
const ist = authResp.intermediate_session_token
if (authResp.discovered_organizations.length > 0 ) {
const exchangeResp = await stytchClient.discovery.intermediateSessions.exchange({
intermediate_session_token: ist,
organization_id: authResp.discovered_organizations[0].organization.organization_id,
});
if (exchangeResp.status_code !== 200) {
console.error(`Error exchanging IST into Organization: ${JSON.stringify(exchangeResp, null, 2)}`);
res.status(500).send();
return;
}
// Store the returned session and return session member information
// Using express sessions with the const key of StytchSessionToken
req.session[StytchSessionToken] = exchangeResp.session_token;
res.send(`Hello, ${authResp.member.email_address}! You're logged into the ${authResp.organization.organization_name} organization`);
return;
}
// If not eligible to log into an existing org, create new one
const createResp = await stytchClient.discovery.organizations.create({
intermediate_session_token: ist,
});
if (createResp.status_code !== 200) {
console.error(`Error creating Organization: '${JSON.stringify(createResp, null, 2)}'`);
res.status(500).send();
return;
}
// Store the returned session and return session member information
req.session[StytchSessionToken] = createResp.session_token;
res.send(`Hello, ${createResp.member.email_address}! You're logged into the ${createResp.organization.organization_name} organization`);
return;
});
Add a session-protected route
- Create a helper method that returns the user’s Session information.
- Use the method to gate any protected route that should only be accessed by an authenticated user.
GET '/dashboard'
Report incorrect code
Copy
Ask AI
app.get('/dashboard', async (req, res) => {
const sessionInfo = await getAuthenticatedMemberAndOrg(req);
if (sessionInfo && sessionInfo.member && sessionInfo.organization) {
res.send(`Hello, ${sessionInfo.member.email_address}! You're logged into the ${sessionInfo.organization.organization_name} organization`);
return;
}
res.send("Log in to view this page");
});
async function getAuthenticatedMemberAndOrg(req) {
const sessionToken = req.session[StytchSessionToken];
if (!sessionToken) {
return null;
}
const resp = await stytchClient.sessions.authenticate({session_token: sessionToken});
if (resp.status_code !== 200) {
console.error('Invalid session found');
req.session[StytchSessionToken] = undefined;
return null;
}
req.session[StytchSessionToken] = resp.session_token;
return {
member: resp.member,
organization: resp.organization,
};
}
Full completed example:
Report incorrect code
Copy
Ask AI
const express = require('express');
const stytch = require('stytch');
const session = require('express-session');
const bodyParser = require('body-parser');
const app = express();
app.use(express.json());
/**
* Express session management
*/
app.use(session({
resave: true,
saveUninitialized: false,
secret: 'session-signing-secret',
cookie: {maxAge: 60000}
}));
const stytchClient = new stytch.B2BClient({
project_id: process.env.STYTCH_PROJECT_ID,
secret: process.env.STYTCH_SECRET,
});
// Key to retrieve the session token from express-session
// Referenced as req.session[StytchSessionToken]
const StytchSessionToken = 'stytch_session_token'
/**
* Retrieves the authenticated member and organization from the session, if present.
* @param req Express request.
* @returns {Promise<{organization: Organization, member: Member}|null>}
*/
async function getAuthenticatedMemberAndOrg(req) {
const sessionToken = req.session[StytchSessionToken];
if (!sessionToken) {
return null;
}
const resp = await stytchClient.sessions.authenticate({session_token: sessionToken});
if (resp.status_code !== 200) {
console.error('Invalid session found');
req.session[StytchSessionToken] = undefined;
return null;
}
req.session[StytchSessionToken] = resp.session_token;
return {
member: resp.member,
organization: resp.organization,
};
}
/**
* Routes
*/
app.post('/login', (req, res) => {
const email = req.body.email;
stytchClient.magicLinks.email.discovery.send({
email_address: email
})
.then(response => {
res.json(response)
})
.catch(err => {
res.status(500).send(err.toString())
});
});
app.get('/authenticate', async (req, res) => {
const token = req.query.token;
const tokenType = req.query.stytch_token_type;
// Handle Discovery authentication.
if (tokenType !== 'discovery') {
console.error(`Unrecognized token type: '${tokenType}'`);
res.status(400).send();
return;
}
const authResp = await stytchClient.magicLinks.discovery.authenticate({
discovery_magic_links_token: token,
});
if (authResp.status_code !== 200) {
console.error('Authentication error')
res.status(500).send();
return;
}
// Sign into existing Organization if already Member
const ist = authResp.intermediate_session_token
if (authResp.discovered_organizations.length > 0 ) {
const exchangeResp = await stytchClient.discovery.intermediateSessions.exchange({
intermediate_session_token: ist,
organization_id: authResp.discovered_organizations[0].organization.organization_id,
});
if (exchangeResp.status_code !== 200) {
console.error(`Error exchanging IST into Organization: ${JSON.stringify(exchangeResp, null, 2)}`);
res.status(500).send();
return;
}
// Store the returned session and return session member information
req.session[StytchSessionToken] = exchangeResp.session_token;
res.redirect('/dashboard');
return;
}
// If not eligible to log into an existing org, create new one
const createResp = await stytchClient.discovery.organizations.create({
intermediate_session_token: ist,
});
if (createResp.status_code !== 200) {
console.error(`Error creating Organization: '${JSON.stringify(createResp, null, 2)}'`);
res.status(500).send();
return;
}
// Store the returned session and return session member information
req.session[StytchSessionToken] = createResp.session_token;
res.redirect('/dashboard');
return;
});
app.get('/logout', (req, res) => {
req.session[StytchSessionToken] = undefined;
res.redirect('/dashboard');
});
app.get('/dashboard', async (req, res) => {
const sessionInfo = await getAuthenticatedMemberAndOrg(req);
if (sessionInfo && sessionInfo.member && sessionInfo.organization) {
res.send(`Hello, ${sessionInfo.member.email_address}! You're logged into the ${sessionInfo.organization.organization_name} organization`);
return;
}
res.send("Log in to view this page");
});
app.listen(3000, () => console.log('Server running on http://localhost:3000'));
Install the Go SDK and configure API keys
- Install the SDK:
go
Report incorrect code
Copy
Ask AI
go get github.com/stytchauth/stytch-go/v16
- Then add your Stytch API keys to your project’s
.envfile:
.env
Report incorrect code
Copy
Ask AI
STYTCH_PROJECT_ID="YOUR_STYTCH_PROJECT_ID"
STYTCH_SECRET="YOUR_STYTCH_PROJECT_SECRET"
# Use your Project's 'live' or any available 'test' credentials
Set up your app and login routes
- Initialize the Stytch client with your environment variables.
- Register an HTTP handler for a
/loginroute that takes in a user’s email address and initiates the login or signup flow:
Report incorrect code
Copy
Ask AI
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
gorillaSessions "github.com/gorilla/sessions"
"github.com/joho/godotenv"
"github.com/stytchauth/stytch-go/v16/stytch/b2b/b2bstytchapi"
discoveryOrgs "github.com/stytchauth/stytch-go/v16/stytch/b2b/discovery/organizations"
"github.com/stytchauth/stytch-go/v16/stytch/b2b/magiclinks/discovery"
emlDiscovery "github.com/stytchauth/stytch-go/v16/stytch/b2b/magiclinks/email/discovery"
)
var ctx = context.Background()
const sessionKey = "stytch_session_token"
func main() {
// Load variables from .env file into the environment.
if err := godotenv.Load(".env.local"); err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
// Instantiate a new API service.
service := NewAuthService(
os.Getenv("STYTCH_PROJECT_ID"),
os.Getenv("STYTCH_SECRET"),
)
// Register HTTP handlers.
mux := http.NewServeMux()
mux.HandleFunc("/login", service.sendMagicLinkHandler)
// Start server.
server := http.Server{
Addr: ":3000",
Handler: mux,
}
log.Println("WARNING: For testing purposes only. Not intended for production use...")
log.Println("Starting server on http://localhost:3000")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
type AuthService struct {
client *b2bstytchapi.API
store *gorillaSessions.CookieStore
}
func NewAuthService(projectId, secret string) *AuthService {
client, err := b2bstytchapi.NewClient(projectId, secret)
if err != nil {
log.Fatalf("Error creating client: %v", err)
}
return &AuthService{
client: client,
store: gorillaSessions.NewCookieStore([]byte("your-secret-key")),
}
}
func (s *AuthService) sendMagicLinkHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
log.Printf("Error parsing form: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
email := r.Form.Get("email")
if email == "" {
http.Error(w, "Email is required", http.StatusBadRequest)
return
}
_, err := s.client.MagicLinks.Email.Discovery.Send(
ctx,
&emlDiscovery.SendParams{
EmailAddress: email,
})
if err != nil {
log.Printf("Error sending email: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Successfully sent magic link email!")
}
Add a route to handle Stytch's redirect callback
- Stytch calls the authenticate redirect URL with a token to complete a user’s authentication flow.
- Add a route to handle the
/authenticateredirect callback in your app.
Report incorrect code
Copy
Ask AI
import (
// Add new imports
"github.com/stytchauth/stytch-go/v16/stytch/b2b/magiclinks/discovery"
)
func main() {
// Add new HTTP handler
mux.HandleFunc("/authenticate", service.authenticateHandler)
}
func (s *AuthService) authenticateHandler(w http.ResponseWriter, r *http.Request) {
tokenType := r.URL.Query().Get("stytch_token_type")
token := r.URL.Query().Get("token")
if tokenType != "discovery" {
log.Printf("Error: unrecognized token type %s\n", tokenType)
http.Error(
w,
fmt.Sprintf("Unrecognized token type %s", tokenType),
http.StatusBadRequest,
)
return
}
resp, err := s.client.MagicLinks.Discovery.Authenticate(
ctx,
&discovery.AuthenticateParams{
DiscoveryMagicLinksToken: token,
})
if err != nil {
log.Printf("Error authenticating: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
Log in to an organization
- At this point, a user has been authenticated but still needs to specify an Organization to log in to and begin a session.
s.client.MagicLinks.Discovery.Authenticate()returns anintermediate_session_tokenthat you can use to preserve the user’s authentication state while you provide them with options to proceed (e.g. selecting from a list of organizations).- For this example, we’ll add logic to automatically sign in to an existing organization or create one if none already exist:
Report incorrect code
Copy
Ask AI
import (
// Add new imports
"github.com/stytchauth/stytch-go/v15/stytch/b2b/discovery/intermediatesessions"
discoveryOrgs "github.com/stytchauth/stytch-go/v15/stytch/b2b/discovery/organizations"
)
func (s *AuthService) authenticateHandler(w http.ResponseWriter, r *http.Request) {
tokenType := r.URL.Query().Get("stytch_token_type")
token := r.URL.Query().Get("token")
if tokenType != "discovery" {
log.Printf("Error: unrecognized token type %s\n", tokenType)
http.Error(
w,
fmt.Sprintf("Unrecognized token type %s", tokenType),
http.StatusBadRequest
)
return
}
resp, err := s.client.MagicLinks.Discovery.Authenticate(
ctx,
&discovery.AuthenticateParams{
DiscoveryMagicLinksToken: token,
})
if err != nil {
log.Printf("Error authenticating: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ist := resp.IntermediateSessionToken
var sessionToken string
if len(resp.DiscoveredOrganizations) > 0 {
organizationId := resp.DiscoveredOrganizations[0].Organization.OrganizationID
resp, err := s.client.Discovery.IntermediateSessions.Exchange(
ctx,
&intermediatesessions.ExchangeParams{
IntermediateSessionToken: ist,
OrganizationID: organizationId,
})
if err != nil {
log.Printf("Error exchanging organization: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sessionToken = resp.SessionToken
} else {
resp, err := s.client.Discovery.Organizations.Create(
ctx,
&discoveryOrgs.CreateParams{
IntermediateSessionToken: ist,
})
if err != nil {
log.Printf("Error creating organization: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sessionToken = resp.SessionToken
}
// Store the session token
session, _ := s.store.Get(r, sessionKey)
session.Values["token"] = sessionToken
_ = session.Save(r, w)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(
w,
"Welcome %s! You're logged into the %s organization",
resp.Member.EmailAddress,
resp.Organization.OrganizationName
)
}
Add a session-protected route
- Create a helper method that returns the user’s Session information.
- Use the method to gate any protected route that should only be accessed by an authenticated user.
Report incorrect code
Copy
Ask AI
import (
// Add new import
"github.com/stytchauth/stytch-go/v15/stytch/b2b/sessions"
"github.com/stytchauth/stytch-go/v15/stytch/b2b/organizations"
)
func main() {
// Add new HTTP handler
mux.HandleFunc("/", service.indexHandler)
}
func (s *AuthService) indexHandler(w http.ResponseWriter, r *http.Request) {
member, organization, ok := s.authenticatedMemberAndOrg(w, r)
if !ok {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Please log in to see this page")
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(
w,
"Welcome %s! You're logged into the %s organization",
member.EmailAddress,
organization.OrganizationName,
)
}
func (s *AuthService) authenticatedMemberAndOrg(
w http.ResponseWriter,
r *http.Request,
) (*organizations.Member, *organizations.Organization, bool) {
sessionToken, exists := s.getSession(w, r, sessionKey)
if (!exists) {
return nil, nil, false
}
resp, err := s.client.Sessions.Authenticate(
ctx,
&sessions.AuthenticateParams{
SessionToken: sessionToken,
})
if err != nil {
log.Printf("Error authenticating session: %v\n", err)
s.clearSession(w, r, sessionKey)
return nil, nil, false
}
return &resp.Member, &resp.Organization, true
}
func (s *AuthService) clearSession(w http.ResponseWriter, r *http.Request, key string) {
session, _ := s.store.Get(r, key)
delete(session.Values, "token")
_ = s.store.Save(r, w, session)
}
func (s *AuthService) getSession(w http.ResponseWriter, r *http.Request, key string) (string, bool) {
session, err := s.store.Get(r, key)
if session == nil || err != nil {
return "", false
}
token, ok := session.Values["token"].(string)
return token, token != "" && ok
}
Full completed example
Report incorrect code
Copy
Ask AI
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
gorillaSessions "github.com/gorilla/sessions"
"github.com/joho/godotenv"
"github.com/stytchauth/stytch-go/v15/stytch/b2b/b2bstytchapi"
"github.com/stytchauth/stytch-go/v15/stytch/b2b/discovery/intermediatesessions"
discoveryOrgs "github.com/stytchauth/stytch-go/v15/stytch/b2b/discovery/organizations"
"github.com/stytchauth/stytch-go/v15/stytch/b2b/magiclinks/discovery"
emlDiscovery "github.com/stytchauth/stytch-go/v15/stytch/b2b/magiclinks/email/discovery"
"github.com/stytchauth/stytch-go/v15/stytch/b2b/organizations"
"github.com/stytchauth/stytch-go/v15/stytch/b2b/sessions"
)
var ctx = context.Background()
const sessionKey = "stytch_session_token"
func main() {
// Load variables from .env file into the environment.
if err := godotenv.Load(".env.local"); err != nil {
log.Fatalf("Error loading .env file: %v", err)
}
// Instantiate a new API service.
service := NewAuthService(
os.Getenv("STYTCH_PROJECT_ID"),
os.Getenv("STYTCH_SECRET"),
)
// Register HTTP handlers.
mux := http.NewServeMux()
mux.HandleFunc("/", service.indexHandler)
mux.HandleFunc("/login", service.sendMagicLinkHandler)
mux.HandleFunc("/authenticate", service.authenticateHandler)
mux.HandleFunc("/logout", service.logoutHandler)
// Start server.
server := http.Server{
Addr: ":3000",
Handler: mux,
}
log.Println("WARNING: For testing purposes only. Not intended for production use...")
log.Println("Starting server on http://localhost:3000")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
type AuthService struct {
client *b2bstytchapi.API
store *gorillaSessions.CookieStore
}
func NewAuthService(projectId, secret string) *AuthService {
client, err := b2bstytchapi.NewClient(projectId, secret)
if err != nil {
log.Fatalf("Error creating client: %v", err)
}
return &AuthService{
client: client,
store: gorillaSessions.NewCookieStore([]byte("your-secret-key")),
}
}
func (s *AuthService) sendMagicLinkHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
log.Printf("Error parsing form: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
email := r.Form.Get("email")
if email == "" {
http.Error(w, "Email is required", http.StatusBadRequest)
return
}
_, err := s.client.MagicLinks.Email.Discovery.Send(
ctx,
&emlDiscovery.SendParams{
EmailAddress: email,
})
if err != nil {
log.Printf("Error sending email: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Successfully sent magic link email!")
}
func (s *AuthService) authenticateHandler(w http.ResponseWriter, r *http.Request) {
tokenType := r.URL.Query().Get("stytch_token_type")
token := r.URL.Query().Get("token")
if tokenType != "discovery" {
log.Printf("Error: unrecognized token type %s\n", tokenType)
http.Error(w, fmt.Sprintf("Unrecognized token type %s", tokenType), http.StatusBadRequest)
return
}
resp, err := s.client.MagicLinks.Discovery.Authenticate(
ctx,
&discovery.AuthenticateParams{
DiscoveryMagicLinksToken: token,
})
if err != nil {
log.Printf("Error authenticating: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ist := resp.IntermediateSessionToken
var sessionToken string
if len(resp.DiscoveredOrganizations) > 0 {
organizationId := resp.DiscoveredOrganizations[0].Organization.OrganizationID
resp, err := s.client.Discovery.IntermediateSessions.Exchange(
ctx,
&intermediatesessions.ExchangeParams{
IntermediateSessionToken: ist,
OrganizationID: organizationId,
})
if err != nil {
log.Printf("Error exchanging organization: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sessionToken = resp.SessionToken
} else {
resp, err := s.client.Discovery.Organizations.Create(
ctx,
&discoveryOrgs.CreateParams{
IntermediateSessionToken: ist,
})
if err != nil {
log.Printf("Error creating organization: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
sessionToken = resp.SessionToken
}
// Store the session token
session, _ := s.store.Get(r, sessionKey)
session.Values["token"] = sessionToken
_ = session.Save(r, w)
s.indexHandler(w, r)
}
func (s *AuthService) indexHandler(w http.ResponseWriter, r *http.Request) {
member, organization, ok := s.authenticatedMemberAndOrg(w, r)
if !ok {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Please log in to see this page")
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(
w,
"Welcome %s! You're logged into the %s organization",
member.EmailAddress,
organization.OrganizationName,
)
}
func (s *AuthService) logoutHandler(w http.ResponseWriter, r *http.Request) {
s.clearSession(w, r, sessionKey)
s.indexHandler(w, r)
return
}
func (s *AuthService) authenticatedMemberAndOrg(
w http.ResponseWriter,
r *http.Request,
) (*organizations.Member, *organizations.Organization, bool) {
sessionToken, exists := s.getSession(w, r, sessionKey)
if !exists {
return nil, nil, false
}
resp, err := s.client.Sessions.Authenticate(
ctx,
&sessions.AuthenticateParams{
SessionToken: sessionToken,
})
if err != nil {
log.Printf("Error authenticating session: %v\n", err)
s.clearSession(w, r, sessionKey)
return nil, nil, false
}
return &resp.Member, &resp.Organization, true
}
func (s *AuthService) clearSession(w http.ResponseWriter, r *http.Request, key string) {
session, _ := s.store.Get(r, key)
delete(session.Values, "token")
_ = s.store.Save(r, w, session)
}
func (s *AuthService) getSession(w http.ResponseWriter, r *http.Request, key string) (string, bool) {
session, err := s.store.Get(r, key)
if session == nil || err != nil {
return "", false
}
token, ok := session.Values["token"].(string)
return token, ok && token != ""
}
Before you start
- Have your
public_tokenandsecret_keyfrom your Stytch project & environment. - While the guide uses Flask, the Python SDK is framework agnostic.
Install the Python SDK and configure API keys
- Install the SDK in your Express environment:
pip
Report incorrect code
Copy
Ask AI
pip install stytch
- Then add your Stytch API keys to your project’s
.envfile:
.env
Report incorrect code
Copy
Ask AI
STYTCH_PROJECT_ID="YOUR_STYTCH_PROJECT_ID"
STYTCH_SECRET="YOUR_STYTCH_PROJECT_SECRET"
# Use your Project's 'live' or any available 'test' credentials
Set up your app and login routes
- Initialize the Stytch client with your environment variables.
- Create a
/loginroute that takes in a user’s email address and initiates the login or signup flow.
POST '/login'
Report incorrect code
Copy
Ask AI
import os
import dotenv
from flask import Flask, request, redirect, session, url_for
from stytch import B2BClient
from stytch.core.response_base import StytchError
# load the .env file
dotenv.load_dotenv()
# Load stytch client
stytch_client = B2BClient(
project_id=os.getenv("STYTCH_PROJECT_ID"),
secret=os.getenv("STYTCH_SECRET"),
environment="test"
)
app = Flask(__name__)
app.secret_key = 'some-secret-key'
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get("email", None)
if not email:
return "Email required"
try:
stytch_client.magic_links.email.discovery.send(
email_address=email
)
except StytchError as e:
return e.details.original_json
return "Success! Check your email"
if __name__ == '__main__':
app.run(host='localhost', port=3000, debug=True)
Add a route to handle Stytch's redirect callback
- Stytch calls the authenticate redirect URL with a token to complete a user’s authentication flow.
- Add an route to handle the
/authenticateredirect callback in your app.
GET '/authenticate'
Report incorrect code
Copy
Ask AI
@app.route('/authenticate', methods=['GET'])
def authenticate():
token = request.args.get('token', None)
token_type = request.args.get('stytch_token_type', None)
# Distinct token_type for each auth flow
# so you know which authenticate() method to use
if token_type != 'discovery':
return f"token_type: {token_type} not supported"
try:
resp = stytch_client.magic_links.discovery.authenticate(
discovery_magic_links_token=token,
)
except StytchError as e:
return e.details.original_json
Log in to an organization
- At this point, a user has been authenticated but still needs to specify an Organization to log in to and begin a session.
stytch_client.magic_links.discovery.authenticate()provides anintermediate_session_tokenthat you can use to preserve the user’s authentication state while you provide them with options to proceed (e.g. selecting from a list of organizations).- For this example, we’ll add logic to automatically sign in to an existing organization or create one if none already exist:
GET '/authenticate'
Report incorrect code
Copy
Ask AI
@app.route('/authenticate', methods=['GET'])
def authenticate():
token = request.args.get('token', None)
token_type = request.args.get('stytch_token_type', None)
# Distinct token_type for each auth flow
# so you know which authenticate() method to use
if token_type != 'discovery':
return f"token_type: {token_type} not supported"
try:
resp = stytch_client.magic_links.discovery.authenticate(
discovery_magic_links_token=token,
)
except StytchError as e:
return e.details.original_json
ist = resp.intermediate_session_token
discovered_orgs = resp.discovered_organizations
if len(discovered_orgs):
# email belongs to >= 1 organization, log into the first one
try:
resp = stytch_client.discovery.intermediate_sessions.exchange(
intermediate_session_token=ist,
organization_id=discovered_orgs[0].organization.organization_id,
)
except StytchError as e:
return e.details.original_json
# Store the returned session in cookies and redirect to home
session['stytch_session_token'] = resp.session_token
return redirect(url_for("dashboard"))
# email does not belong to any organizations, create a new one
try:
resp = stytch_client.discovery.organizations.create(
intermediate_session_token=ist
)
except StytchError as e:
return e.details.original_json
session['stytch_session_token'] = resp.session_token
return f"Welcome {resp.member.email_address}! You're logged into the {resp.organization.organization_name} organization"
Add a session-protected route
- Create a helper method that returns the user’s Session information.
- Use the method to gate any protected route that should only be accessed by an authenticated user.
GET '/dashboard'
Report incorrect code
Copy
Ask AI
@app.route('/dashboard', methods=["GET"])
def dashboard():
member, organization = get_authenticated_member_and_organization()
if not member or not organization:
return "Log in to view this page."
return f"Welcome {member.email_address}! You're logged into the {organization.organization_name} org!"
def get_authenticated_member_and_organization():
stytch_session = session.get('stytch_session_token')
if not stytch_session:
return None, None
try:
resp = stytch_client.sessions.authenticate(session_token=stytch_session)
except StytchError as e:
# Session has expired or is invalid, clear it
session.pop('stytch_session_token', None)
return None, None
return resp.member, resp.organization
Full completed example:
Report incorrect code
Copy
Ask AI
import os
import dotenv
from flask import Flask, request, redirect, session, url_for
from stytch import B2BClient
from stytch.core.response_base import StytchError
# load the .env file
dotenv.load_dotenv()
# Load stytch client
stytch_client = B2BClient(
project_id=os.getenv("STYTCH_PROJECT_ID"),
secret=os.getenv("STYTCH_SECRET"),
environment="test"
)
app = Flask(__name__)
app.secret_key = 'some-secret-key'
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
email = data.get("email", None)
if not email:
return "Email required"
try:
stytch_client.magic_links.email.discovery.send(
email_address=email
)
except StytchError as e:
return e.details.original_json
return "Success! Check your email"
@app.route('/authenticate', methods=['GET'])
def authenticate():
token = request.args.get('token', None)
token_type = request.args.get('stytch_token_type', None)
# Distinct token_type for each auth flow
# so you know which authenticate() method to use
if token_type != 'discovery':
return f"token_type: {token_type} not supported"
try:
resp = stytch_client.magic_links.discovery.authenticate(
discovery_magic_links_token=token,
)
except StytchError as e:
return e.details.original_json
ist = resp.intermediate_session_token
discovered_orgs = resp.discovered_organizations
if len(discovered_orgs):
# email belongs to >= 1 organization, log into the first one
try:
resp = stytch_client.discovery.intermediate_sessions.exchange(
intermediate_session_token=ist,
organization_id=discovered_orgs[0].organization.organization_id,
)
except StytchError as e:
return e.details.original_json
# Store the returned session in cookies and redirect to home
session['stytch_session_token'] = resp.session_token
return redirect(url_for("dashboard"))
# email does not belong to any organizations, create a new one
try:
resp = stytch_client.discovery.organizations.create(
intermediate_session_token=ist
)
except StytchError as e:
return e.details.original_json
session['stytch_session_token'] = resp.session_token
return redirect(url_for("dashboard"))
@app.route('/dashboard', methods=["GET"])
def dashboard():
member, organization = get_authenticated_member_and_organization()
if not member or not organization:
return "Log in to view this page."
return f"Welcome {member.email_address}! You're logged into the {organization.organization_name} org!"
@app.route('/logout', methods=["GET"])
def logout():
session.pop('stytch_session_token', None)
return redirect(url_for('dashboard'))
def get_authenticated_member_and_organization():
stytch_session = session.get('stytch_session_token')
if not stytch_session:
return None, None
try:
resp = stytch_client.sessions.authenticate(session_token=stytch_session)
except StytchError as e:
# Session has expired or is invalid, clear it
session.pop('stytch_session_token', None)
return None, None
return resp.member, resp.organization
if __name__ == '__main__':
app.run(host='localhost', port=3000, debug=True)
Install the Ruby SDK and configure API keys
- Install the SDK in your Express environment:
gem
Report incorrect code
Copy
Ask AI
gem 'stytch'
- Then add your Stytch API keys to your project’s
.envfile:
.env
Report incorrect code
Copy
Ask AI
STYTCH_PROJECT_ID="YOUR_STYTCH_PROJECT_ID"
STYTCH_SECRET="YOUR_STYTCH_PROJECT_SECRET"
# Use your Project's 'live' or any available 'test' credentials
Set up your Rails app
- Initialize the Stytch client.
- Define
/loginand/authenticateroutes and controller actions to handle authentication.
Report incorrect code
Copy
Ask AI
Rails.application.routes.draw do
post 'login', to: 'authentication#login'
get 'authenticate', to: 'authentication#authenticate'
end
Set a redirect URL
Specify an authenticate redirect URL in your Dashboard.
Create an API client
Report incorrect code
Copy
Ask AI
import com.stytch.java.b2b.StytchB2BClient
val client = StytchB2BClient(
projectId = "YOUR_PROJECT_ID",
secret = "YOUR_SECRET_KEY"
)
Create an organization
Report incorrect code
Copy
Ask AI
when (val result = client.organizations.create(CreateRequest(
organizationName = "Acme Co",
organizationSlug = "acme-co",
emailAllowedDomains = listOf("acme.co"),
))) {
is StytchResult.Success -> println(result.value)
is StytchResult.Error -> println(result.exception)
}
Log a user into an organization
Report incorrect code
Copy
Ask AI
when (val result = client.magicLinks.email.loginOrSignup(LoginOrSignupRequest(
organizationId = "organization-id-from-create-response-...",
emailAddress = "admin@acme.co",
loginRedirectURL = "https://example.com/authenticate",
signupRedirectURL = "https://example.com/authenticate",
)) {
is StytchResult.Success -> println(result.value)
is StytchResult.Error -> println(result.exception)
}
Next steps
- Handle a session