B2B Saas Authentication

/

Quickstarts

/

Quickstarts

/

Python

Python Quickstart

This quickstart guide outlines the essential steps to build a Discovery sign-up and login flow in your Python app using Stytch's B2B SaaS Authentication product. While this example uses Flask specifically, Stytch's Python SDK is framework agnostic and can be used with the Python framework of your choosing.

Overview

Stytch offers a Python SDK that can be used either stand-alone, for an entirely backend integration with Stytch, or alongside our frontend SDKs. This guide covers the steps for an entirely backend integration with Stytch.

Want to skip straight to the source code? Check out an example app here.

Getting started

1
Install Stytch SDK and configure your API Keys

Create a Stytch B2B Project in your Stytch Dashboard if you haven't already.

Install our Python SDK in your Flask environment:

pip install stytch

Configure your Stytch Project's API keys as environment variables:

STYTCH_PROJECT_ID="YOUR_STYTCH_PROJECT_ID"
STYTCH_SECRET="YOUR_STYTCH_PROJECT_SECRET"
# Use your Project's 'test' or 'live' credentials

2
Set up your app and login route

Set up a basic app using the framework of your choice and initialize the Stytch client with the environment variables you set in the previous step. Create a /login route that takes in the user's email address and initiates the sign-up or login flow by calling Stytch.

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)

3
Add a route to handle redirect callback from Stytch

When a user completes an authentication flow in Stytch, we will call the Redirect URL specified in your Stytch Dashboard with a token used to securely complete the auth flow. By default the redirect URL is set tohttp://localhost:3000/authenticate.

You can read more about redirect URLs and possible token types in this guide.

@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

4
Create new Organization or login to existing Organization

At this point in the flow, the end user has authenticated but has not specified whether they want to create a new Organization or log into another Organization they belong to or can join through their verified email domain and JIT Provisioning.

stytch_client.magic_links.discovery.authenticate() will return an Intermediate Session Token (IST) which allows you to preserve the authentication state while you present the user with options on how they wish to proceed. For the purposes of this quickstart, we will automatically create a new Organization if the end user does not have any Organizations they can log into and otherwise log into their first Organization.

@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"

5
Add session protected route

Add a helper method that returns the session user information, and use it to gate any protected route that should only be accessed by an authenticated user.

@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

6
Test your application

Run your application and send a POST request to the /login endpoint with your email address to initiate the Discovery flow, and then click on the email magic link you receive in your inbox to finish signing up or logging in.

What's Next

Check out the example app here to see how to extend this quickstart to enable JIT Provisioning by email domain, session exchange between orgs and more!

Completed example

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)