B2B Saas Authentication

/

Guides

/

Authentication

/

Magic Links

/

Integration Guides

/

Backend integration

Backend Integration of Email Magic Links

In Stytch’s B2B product there are two different versions of the EML authentication flow:

  1. Discovery Authentication: used for self-serve Organization creation or login prior to knowing the Organization context
  2. Organization-specific Authentication: used when you already know the Organization that the end user is trying to log into

The guides below cover how to offer Email Magic Links for both scenarios, using a backend integration approach.

Discovery Sign-Up or Login

The discovery flow is designed for situations where your end users are signing up or logging in from a central landing page, and have not specified which organization they are trying to access or are attempting to create a new Organization.

The sequence for how this flow works when using a backend integration approach is as follows:

Backend integration of discovery Magic Links

1
Complete config steps

If you haven't done so already complete the steps in the EML Integration Guide Start Here

2
Create login page with email input

You'll need some way for the user to input their email in order to trigger the magic link flow. Create a route that checks to will surface the discovery login or sign-up view to your end user.

You can create a super simple template for taking in the user input:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Dashboard</title>
    <link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
    <div class="card">
    <p>Sign-up or Login with Email Magic Links</p>
    <div class="divider">
        <hr class="line" />
    </div>
        <form action="/send-discovery-eml" method="post">
            <label for="email">Email Address:</label>
            <input type="email" id="email" name="email" required>
            <button type="submit">Submit</button>
        </form>
    </div>
</body>
</html>

which would be served by a basic route like the following:

@app.route("/", methods=["GET"])
def index(slug: str):

    # Next up: check for session, and if present redirect to logged in view

    return render_template("login.html")

3
Handle submission of email

After the user submits the form with their email, you'll need a route for triggering the outbound call to Stytch to initiate the magic link discovery flow.

@app.route("/send-discovery-eml", methods=["POST"])
def send_discovery_eml() -> str:
    email = request.form.get("email", None)
    if email is None:
        return "Email is required", 400

    resp = stytch_client.magic_links.email.discovery.send(email_address=email)
    if resp.status_code != 200:
        return "Error sending EML", 500

    return "Success"

4
Configure callback and surface UI for selecting an organization

Stytch will make a callback to the Discovery RedirectURL that you specified in the Stytch dashboard. Your application should handle checking the stytch_token_type for the callback, and call the appropriate authentication method to finish the login process.

If your RedirectURL was http://localhost:3000/discovery you would add the following route to your application:

@app.route("/discovery", methods=["GET"])
def discovery() -> str:
    token_type = request.args["stytch_token_type"]
    token = request.args["token"]
    if token_type != "discovery":
        # add handling for other discovery token types like discovery_oauth in the future
        return "Unsupported auth method", 400

    resp = stytch_client.magic_links.discovery.authenticate(discovery_magic_links_token=token)
    if resp.status_code != 200:
        return "Authentication error", 500

    # store IST as cookie or other mechanism for use in subsequent request to exchange
    session['ist'] = resp.intermediate_session_token
    orgs = []
    for discovered in resp.discovered_organizations:
        org = {
            "organization_id": discovered.organization.organization_id,
            "organization_name": discovered.organization.organization_name,
        }
        orgs.append(org)

    return render_template(
        'discoveredOrgs.html',
        discovered_organizations=orgs,
        email_address=resp.email_address
    )

Create a template that surfaces the available organizations to the end user as well as the option to create a new Organization.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Dashboard</title>
    <link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
    <div class="card">
    <div class="card-content">
        <h1>
            Discovered Organizations for {{ email_address }}
        </h1>
    </div>
    <p>Login to existing Organization or create a new one!</p>
    <div id="button-containers"></div>
    <div class="divider">
        <hr class="line" />
    </div>
        <button class="button" onclick="createOrg()"> Create New Organization </button>
    </div>
    <script>
        function selectOrg(organization_id) {
            window.location.href = `/login/${organization_id}`;
        }
        function createOrg() {
            window.location.href = `/create_org`;
        }
        const unparsedOrgs = "{{ discovered_organizations }}"
        const orgs = JSON.parse(unparsedOrgs.replaceAll("&#39;", "\""))
        function iterateOverOrgs() {
            document.getElementById('button-containers').innerHTML = orgs.map(org => (
            `<button class="button" onclick="selectOrg('${org.organization_id}')">
            ${org.organization_name}
            </button>`
            )).join('\n\n');
        }
        iterateOverOrgs();
    </script>
</body>
</html>

5
Create routes for handling user selection

Create two routes to handle the options presented to the end user: logging into an existing Organization or creating a new Organization.

@app.route("/login/<string:organization_id>", methods=["GET"])
def login_to_org(organization_id):
    ist = session.get('ist', None)
    if ist is None:
        return "No IST found"

    resp = stytch_client.discovery.intermediate_sessions.exchange(
        intermediate_session_token=ist,
        organization_id=organization_id
    )
    if resp.status_code != 200:
        return "Error logging into org", 500

    # Clear IST and set stytch session
    session.pop('ist', None)
    session['stytch_session'] = resp.session_token
    return member.json()

@app.route("/create_org", methods=["GET"])
def create_org() -> str:
    ist = session.get('ist', None)
    if ist is None:
        return "No IST found"

    # Created org name and slug will be based on user's email
    # Can also prompt end user to provide these 
    resp = stytch_client.discovery.organizations.create(
        intermediate_session_token=ist,
        organization_slug='',
        organization_name=''
    )
    if resp.status_code != 200:
        return "Error creating org", 500

    # Clear IST and set stytch session
    session.pop('ist', None)
    session['stytch_session'] = resp.session_token
    return member.json()

6
Test it out

Run your application, enter your email and test out the discovery flow!

Organization Login

If end users of your application login via a page that indicates which Organization they are trying to log into (e.g. <org-slug>.your-app.com or your-app.com/team/<org-slug>) you can offer organization login on that page.

The high level flow using a backend integration with Stytch is as follows:

Backend integration of organization-specific Magic Links

1
Complete config steps

If you haven't done so already complete the steps in the EML Integration Guide Start Here, including creating an Organization.

2
Create organization login page with email input

You’ll need a UI that will allow the end user to identify the Organization they wish to log into and initiate logging in by providing their email address.

For example, if users would log into their organization by going to <your-app>.com/org/<org-slug> you would add a route like:

@app.route("/org/<string:slug>", methods=["GET"])
def org_index(slug: str):

  # Next up: Check for active member session for org, if present show logged in view
  # Otherwise show login screen

  resp = stytch_client.organizations.search(query=SearchQuery(operator="AND", operands=[{
    "filter_name": "organization_slugs",
    "filter_value": [slug]}
    ]))
  if resp.status_code != 200 or len(resp.organizations) == 0:
    return "Error fetching org", 500

  organization = resp.organizations[0]

  return render_template(
    "organizationLogin.html",
    org_name=organization.organization_name,
    org_id=organization.organization_id
  )

That renders a template like:

<head>
  <title>Login</title>
  <link rel="stylesheet" href="{{ url_for('static', filename= 'css/styles.css') }}">
</head>
<body>
  <div class="card">
  <div class="card-content">
    <h1>
      Sign into {{ org_name }} Organization
    </h1>
  </div>
         <form action="/send-eml" method="post">
              <label for="email">Email Address:</label>
              <input type="email" id="email" name="email" required>
              <input type="hidden" id="organization_id" name="organization_id" value="{{ org_id }}">
              <button type="submit">Submit</button>
          </form>
    </div>
</body>

3
Handle submission of email

After the user submits the form with their email, you'll need a route for triggering the outbound call to Stytch to initiate the magic link login flow.

@app.route("/send-eml", methods=["POST"])
def send_eml() -> str:
    email = request.form.get('email', None)
    organization_id = request.form.get('organization_id', None)
    if email is None or organization_id is None:
        return 'Email and OrgID are required', 400

    resp = stytch_client.magic_links.email.login_or_signup(email_address=email, organization_id=organization_id)
    if resp.status_code != 200:
        return "Error sending EML", 500

    return "Success"

4
Configure callback handler

Stytch will make a callback to the Login or Signup RedirectURL that you specified in the Stytch dashboard. Your application should handle checking the stytch_token_type for the callback, and call the appropriate authentication method to finish the login process.

If your RedirectURL was http://localhost:3000/authenticate you would add the following route to your application:

def authenticate() -> str:
    token_type = request.args["stytch_token_type"]
    if token_type != "multi_tenant_magic_links":
        return "unsupported authentication method"
    resp = stytch_client.magic_links.authenticate(magic_links_token=request.arg["token"])
    if resp.status_code != 200:
      return "something went wrong authenticating token", 500

  # member is successfully logged in
  member = resp.member
  session["stytch_session"] = resp.session_jwt
  return member.json()

5
Test it out

Run your application, enter your email and test out the organization login flow!