Authentication & Profile
Sesamy JS provides comprehensive authentication and profile management with support for multiple login flows and user data management.
Authentication
The SDK provides comprehensive authentication methods with support for both redirect and popup-based flows.
login()
Smart login that automatically chooses the best authentication method based on browser context.
Parameters:
options(optional):appState: Any JSON-serializable data to preserve during authentication (e.g.,{ source: 'registration-wall', articleId: '123' })authorizationParams:audience: API audiencescope: OAuth scopeslogin_hint: Pre-fill emailorganization: Auth0 organizationredirect_uri: Post-login redirect URL
Returns: Promise<void>
Example:
await window.sesamy.auth.login({
authorizationParams: {
login_hint: 'user@example.com',
},
});Example with metadata tracking:
// Track login source for analytics
await window.sesamy.auth.login({
appState: {
source: 'registration-wall',
articleId: 'article-123',
campaign: 'spring-2025',
},
});loginWithRedirect()
Redirects user to Auth0 hosted login page. Best for desktop browsers.
Parameters:
- Same as
login()
Returns: Promise<void>
Example:
await window.sesamy.auth.loginWithRedirect({
authorizationParams: {
redirect_uri: window.location.origin + '/callback',
},
});loginWithPopup()
Opens login in a popup window. Better for maintaining page state and works better in incognito mode.
Note: To prevent automatic page refresh after authentication, listen for sesamyJsAuthenticated event and call preventDefault().
Parameters:
- Same as
login()
Returns: Promise<void>
Example:
// Prevent auto-refresh after login
window.addEventListener('sesamyJsAuthenticated', (event) => {
event.preventDefault();
console.log('User authenticated!');
});
await window.sesamy.auth.loginWithPopup({
authorizationParams: {
login_hint: 'user@example.com',
},
});logout()
Logs out the user and optionally redirects.
Parameters:
options(optional):returnTo: URL to redirect after logout
Returns: Promise<void>
Example:
await window.sesamy.auth.logout({
returnTo: 'https://yoursite.com',
});isAuthenticated()
Checks if user is currently authenticated.
Returns: Promise<boolean>
Example:
const isAuth = await window.sesamy.auth.isAuthenticated();
if (isAuth) {
console.log('User is logged in');
}getTokenSilently()
Retrieves access token without user interaction.
Parameters:
throwOnUnauthorized(boolean, default: true): Throw error if not authenticatedforceRefresh(boolean, default: false): Force token refresh
Returns: Promise<string | null>
Example:
const token = await window.sesamy.auth.getTokenSilently();
// Use token for API callssetToken()
Manually set an access token.
Parameters:
accessToken(string): The access tokenexpiresIn(number, optional): Token expiration in seconds
Returns: Promise<void>
Example:
await window.sesamy.auth.setToken('your.jwt.token', 3600);Profile Management
profile.get()
Fetches the user's profile information.
Returns: Promise<Profile | null>
Profile Type:
type Profile = {
userId: string;
firstName?: string;
lastName?: string;
emailVerified: boolean;
email: string;
name?: string;
locale?: string;
picture?: string;
createdAt: string;
updatedAt: string;
mobilePhone?: string;
tags: string[];
user_metadata?: Record<string, string | number>;
billingAddress?: Address;
deliveryAddress?: Address;
};Example:
const profile = await window.sesamy.profile.get();
if (profile) {
console.log('User email:', profile.email);
console.log('Name:', profile.firstName, profile.lastName);
console.log('Email verified:', profile.emailVerified);
}profile.update()
Updates the user's profile information.
Parameters:
profile(Partial<Profile>): Fields to update
Returns: Promise<boolean>
Example:
const success = await window.sesamy.profile.update({
firstName: 'John',
lastName: 'Doe',
mobilePhone: '+1234567890',
billingAddress: {
street: '123 Main St',
city: 'New York',
state: 'NY',
postalCode: '10001',
country: 'US',
},
});
if (success) {
console.log('Profile updated successfully');
}profile.openHostedAccountPage()
Opens the Sesamy hosted account management page where users can manage their subscriptions, payment methods, and personal information.
Example:
// Add to a "My Account" button
document.querySelector('#account-btn').addEventListener('click', () => {
window.sesamy.profile.openHostedAccountPage();
});profile.isSpotifyLinked()
Checks if the user has linked their Spotify account.
Returns: Promise<boolean>
Example:
const isLinked = await window.sesamy.profile.isSpotifyLinked();
if (isLinked) {
console.log('Spotify account is linked');
}profile.unlinkSpotify()
Unlinks the user's Spotify account.
Returns: Promise<void>
Example:
await window.sesamy.profile.unlinkSpotify();
console.log('Spotify account unlinked');Authentication Modes
Sesamy JS supports two authentication backends. Choose the one that fits your infrastructure.
BFF (Cookie-based) Auth — recommended for new integrations
The BFF (Backend for Frontend) pattern stores tokens in HttpOnly cookies managed by the Sesamy API proxy. JavaScript never has access to raw access tokens, providing stronger XSS protection and eliminating token storage in localStorage.
For the end-to-end login flow (sequence diagram) and the canonical list of every cookie the proxy sets —
at,__Host-rt,sesamy_is_authenticated,sesamy_cid,__Host-auth-state— see Cookie-Based (BFF) Authentication. This page covers the sesamy-js client configuration.
auth.useHttpCookies: true is the single switch for BFF mode. It turns on:
- Auth flows via cookies —
login/logout/ session check go to/auth/*on your BFF, which sets the encryptedat(access) and__Host-rt(refresh) cookies plus the JS-readablesesamy_is_authenticatedhint. - API transport via cookies — every Sesamy API call (
/api/entitlements,/api/profile, etc.) is sent withcredentials: 'include'and noAuthorizationheader. The BFF reads the cookies, attaches a Bearer token to the upstream request, and forwards it to Sesamy.
You do not need a separate api.useHttpCookies flag — the API transport follows the auth mode automatically.
Requirements:
- The
/auth/*routes (and the API endpoint) must be served from the same origin as your page (so the browser sends cookies automatically). You can achieve this either with a relative proxy path or a custom domain — see the options below. - The Token Handler capability enabled for your vendor (contact Sesamy to set this up)
Option A — Relative path (proxy on same origin)
The simplest setup: your backend (Next.js, Vite dev server, nginx, etc.) proxies /auth/* and API calls to the Sesamy API proxy. No custom domain required.
{
"clientId": "your-vendor-id",
"api": {
"endpoint": "/api"
},
"auth": {
"useHttpCookies": true
}
}All requests (/api/entitlements, /auth/login, etc.) go to the same origin as your page — cookies are first-party automatically.
Option B — Custom API domain
Use a dedicated subdomain (e.g. api.yoursite.com) pointing at the Sesamy API proxy. The page and the API domain must share a top-level domain so the cookie is first-party.
{
"clientId": "your-vendor-id",
"api": {
"endpoint": "https://api.yoursite.com"
},
"auth": {
"useHttpCookies": true
}
}With this setup, all auth flows (login, logout, token refresh, session check) route through /auth/* on your API domain. The session cookie is set as HttpOnly; SameSite=Lax by the server — JS never reads it directly.
Option C — Sub-path namespace
Use when your first-party proxy reserves a sub-path for Sesamy auth (e.g. to avoid colliding with your own /auth/* routes). Set auth.baseUrl to the prefix and the BFF plugin prepends it to every /auth/* path it touches:
{
"clientId": "your-vendor-id",
"auth": {
"useHttpCookies": true,
"baseUrl": "/sesamy-auth"
},
"api": {
"endpoint": "/sesamy-api"
}
}The plugin then calls /sesamy-auth/userinfo, /sesamy-auth/{vendorId}/login, and /sesamy-auth/{vendorId}/logout. auth.baseUrl is independent of api.endpoint — auth and API can live on different sub-paths or hosts. Combine with Proxy Setup for the matching reverse-proxy rules.
BFF endpoints used by sesamy-js:
| Endpoint | Description |
|---|---|
GET /auth/:vendorId/login | Redirects user to the auth provider login page |
GET /auth/:vendorId/callback | Handles the OAuth callback and sets the session cookie |
GET /auth/:vendorId/logout | Clears the session cookie and optionally redirects to the auth provider logout (GET /auth/logout is still accepted but relies on the client hint cookie) |
GET /auth/userinfo | Returns auth status and user info (no token exposure) |
When auth.baseUrl is set (Option C above), the prefix is prepended to every endpoint — e.g. GET /sesamy-auth/userinfo instead of GET /auth/userinfo.
What changes on the wire
With auth.useHttpCookies: true, every API request emitted by sesamy-js looks like this:
GET /api/entitlements HTTP/1.1
Host: yoursite.com
Cookie: at=<HttpOnly>; __Host-rt=<HttpOnly>; sesamy_is_authenticated=true; sesamy_cid=acme
Accept: application/json
# No Authorization header — the BFF attaches Bearer auth upstreamCompared to the token-based mode, the library:
- Omits the
Authorization: Bearer …header (nothing to send — JS can't readat). - Omits the anonymous
Authorization: Basic …fallback. - Sets
credentials: 'include'on everyfetchso the cookies ride along.
The BFF is responsible for everything else — validating the session, refreshing the access token if expired, and forwarding the request upstream with Authorization: Bearer <at>.
Migrating existing readers from Auth0 sessions
Switching a live integration to BFF mode logs out everyone with an existing Auth0 SPA session — those tokens live in localStorage, which the cookie plugin cannot read. Set auth.migrateLegacySession: true to carry those readers over without an interactive prompt:
{
"clientId": "your-vendor-id",
"api": {
"endpoint": "https://api.yoursite.com"
},
"auth": {
"useHttpCookies": true,
"migrateLegacySession": true
}
}On the first page load, if the plugin finds a leftover Auth0 SPA session but no BFF session yet, it makes a single silent (prompt=none) login attempt through /auth/:vendorId/login. When the reader still has an active session at the auth provider, the BFF sets the cookies and they stay logged in with no login screen. When there is no active session, the attempt falls through and the reader stays anonymous — no error page and no redirect loop. The plugin clears the orphaned Auth0 localStorage entries once an attempt is spent, so each browser tries only once.
Same auth domain as before
The silent attempt targets the auth provider the BFF resolves from your auth.domain config — the same domain your Auth0 SPA integration used, so readers with a live session carry over. If you change auth.domain as part of the switch, existing sessions live on the old domain and the attempt returns login_required, leaving those readers anonymous until they log in again.
migrateLegacySession is a transition aid. Leave it on while legacy sessions age out, then remove it.
Troubleshooting
| Symptom | Likely cause |
|---|---|
entitlements.hasAccess(url) returns empty / undefined for a logged-in user | auth.useHttpCookies: true is not set — sesamy-js is sending an anonymous Basic auth header that overrides the BFF's forwarded identity |
| Cookies never arrive at the API | api.endpoint points to a different origin than the page (e.g. api.sesamy.com from publisher.com). Cross-site cookies are blocked — use Option A or Option B so the API is same-origin |
401 on every API call | The BFF is not forwarding the access token upstream, or the session cookie has expired. Check the BFF logs; a fresh login should re-establish the session |
| Only the first request works, subsequent fail | Refresh logic on the BFF — make sure the Token Handler is refreshing at using rt when needed |
| Existing Auth0 readers are logged out after switching to BFF | Set auth.migrateLegacySession: true so the plugin makes a silent prompt=none login on first load. It only carries over readers whose session still exists at the BFF's auth provider |
Auth0 SPA JS Plugin — for existing integrations
For publishers already using the Auth0 redirect/popup flow, the auth0-plugin preserves the existing behaviour. Tokens are managed by @auth0/auth0-spa-js in session/localStorage.
The plugin ships as a separate entry point. Pass it directly to init() via the authPlugin option:
Via npm (recommended):
import { init } from '@sesamy/sesamy-js';
import { createAuth0Plugin } from '@sesamy/sesamy-js/auth0-plugin';
const sesamy = await init(
{
clientId: 'your-client-id',
auth: {
domain: 'login.yoursite.com',
},
},
{ authPlugin: createAuth0Plugin() }
);Via script tags (no build tool):
Load the auth0-plugin before sesamy-js. When the sesamy-js bundle initialises from the <script type="application/json"> element it automatically detects the auth0Plugin global exposed by the IIFE and passes it to init():
<!-- auth0-plugin IIFE must be loaded first -->
<script src="https://cdn.sesamy.com/sesamy-js/latest/auth0-plugin.iife.js"></script>
<script src="https://cdn.sesamy.com/sesamy-js/latest/sesamy-js.iife.js"></script>
<script type="application/json" id="sesamy-js">
{
"clientId": "your-client-id",
"auth": {
"domain": "login.yoursite.com"
}
}
</script>Legacy: registerAuthPlugin()
In older versions of sesamy-js (< 1.95.0) the plugin had to be registered globally before init() ran:
import { registerAuthPlugin, init } from '@sesamy/sesamy-js';
import { createAuth0Plugin } from '@sesamy/sesamy-js/auth0-plugin';
// Must be called before init()
registerAuthPlugin(createAuth0Plugin());
const sesamy = await init({ clientId: 'your-client-id' });This still works but the authPlugin option is preferred — it's explicit and doesn't rely on shared global state.
Scripts Host: automatic mode selection
If you use the Scripts Host service, the auth mode is selected automatically based on your vendor configuration — no code changes required:
| Vendor config | Auth mode |
|---|---|
No apiDomain set | Auth0 plugin is prepended to the bundle. All existing clients continue to work unchanged. |
apiDomain set (e.g. api.yoursite.com) | Cookie (BFF) auth is enabled; the Auth0 plugin is omitted from the bundle. |
Switching modes via Scripts Host is transparent: updating the apiDomain field in your Scripts Host vendor configuration is all that is needed.
Cross-Domain Authentication
For applications spanning multiple domains (e.g., white-label sites, multi-region deployments), Sesamy JS supports automatic domain selection:
{
auth: {
clientId: "your-client-id",
domains: [
"auth.brand1.com",
"auth.brand2.com",
"auth.example.co.uk"
],
domain: "default-auth.example.com" // Fallback domain
}
}The SDK automatically selects the appropriate auth domain based on the current page's top-level domain, ensuring seamless authentication across your domain portfolio.
Common Patterns
Protected Content
window.addEventListener('sesamyJsReady', async () => {
const isAuth = await window.sesamy.auth.isAuthenticated();
if (isAuth) {
const profile = await window.sesamy.profile.get();
document.querySelector('.user-name').textContent = profile.name;
document.querySelector('.premium-content').style.display = 'block';
} else {
document.querySelector('.login-prompt').style.display = 'block';
}
});Login/Logout Button
const authBtn = document.querySelector('#auth-btn');
window.addEventListener('sesamyJsReady', async () => {
const updateAuthButton = async () => {
const isAuth = await window.sesamy.auth.isAuthenticated();
if (isAuth) {
const profile = await window.sesamy.profile.get();
authBtn.textContent = `Logout (${profile.email})`;
authBtn.onclick = () => window.sesamy.auth.logout();
} else {
authBtn.textContent = 'Login';
authBtn.onclick = () => window.sesamy.auth.login();
}
};
await updateAuthButton();
// Update button after authentication changes
window.addEventListener('sesamyJsAuthenticated', updateAuthButton);
window.addEventListener('sesamyJsLoggedOut', updateAuthButton);
});Profile Form
window.addEventListener('sesamyJsReady', async () => {
const profile = await window.sesamy.profile.get();
if (profile) {
// Pre-fill form
document.querySelector('#firstName').value = profile.firstName || '';
document.querySelector('#lastName').value = profile.lastName || '';
document.querySelector('#email').value = profile.email;
}
// Handle form submission
document.querySelector('#profileForm').addEventListener('submit', async (e) => {
e.preventDefault();
const success = await window.sesamy.profile.update({
firstName: document.querySelector('#firstName').value,
lastName: document.querySelector('#lastName').value,
mobilePhone: document.querySelector('#phone').value,
});
if (success) {
alert('Profile updated successfully!');
}
});
});Login Paywall with Tracking
Track where users are logging in from using appState:
// Custom login paywall component
class LoginPaywall extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="paywall">
<h2>Subscribe to continue reading</h2>
<button id="paywall-login">Sign up or Login</button>
</div>
`;
this.querySelector('#paywall-login').addEventListener('click', () => {
window.sesamy.auth.login({
appState: {
source: 'registration-wall',
articleId: this.getAttribute('article-id'),
contentType: this.getAttribute('content-type') || 'article',
},
});
});
}
}
customElements.define('login-paywall', LoginPaywall);
// Listen for authentication with metadata
window.addEventListener('sesamyJsAuthenticated', (event) => {
const { appState, email, sub } = event.detail;
if (appState?.source === 'registration-wall') {
// Track conversion
console.log('User registered from paywall', {
source: appState.source,
articleId: appState.articleId,
contentType: appState.contentType,
userId: sub,
});
// Send to analytics
if (window.gtag) {
window.gtag('event', 'registration_completed', {
source: appState.source,
article_id: appState.articleId,
});
}
}
});Usage in HTML:
<login-paywall article-id="spring-news-2025" content-type="premium-article"></login-paywall>Next Steps
- API Reference - Check entitlements and manage subscriptions
- Content Management - Implement paywalls and access control
- Analytics - Track user interactions