Node Quickstart
This quickstart guide outlines the essential steps to build a Discovery sign-up and login flow in your Node app using Stytch's B2B SaaS Authentication product. While this example uses Express, Stytch's Node SDK is framework agnostic.
Overview
Stytch offers a Node 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
1Install Stytch SDK and configure your API Keys
Create a Stytch B2B Project in your Stytch Dashboard if you haven't already.
Install our Node SDK in your Express environment:
npm 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
2Set 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.
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'));
3Add 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.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;
}
});
4Create 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.
stytchClient.magicLinks.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.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;
});
5Add 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.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,
};
}
6Test 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
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'));