npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@sesamehr/oauth-client

v1.0.4

Published

OAuth 2.0 + OIDC client library for Sesame SSO authentication

Downloads

427

Readme

Sesame OAuth Client

OAuth 2.0 + OpenID Connect client library for Sesame SSO authentication - Backend only

npm version License: MIT

Features

  • RFC 6749 Compliant - Proper OAuth 2.0 implementation with application/x-www-form-urlencoded and Basic Auth
  • CSRF Protection - Built-in state validation with configurable TTL store
  • Automatic Cleanup - Memory-efficient with periodic cleanup of expired states
  • Pluggable Storage - Default in-memory store, easily swap for Redis or other distributed stores
  • Token Management - Refresh and revoke tokens (RFC 7009)
  • Flexible API - Fetch only what you need (tokens, user info, Sesame credentials)
  • Production Ready - Proper timeout handling, error formatting, and security best practices

Installation

npm install @sesamehr/oauth-client
# or
yarn add @sesamehr/oauth-client

Quick Start

Basic Usage (Express.js)

import express from 'express';
import session from 'express-session';
import { SesameSSO } from '@sesamehr/oauth-client';

const app = express();

// Initialize Sesame SSO client
const sso = new SesameSSO({
  ssoBaseUrl: process.env.SSO_BASE_URL || 'https://sso.sesametime.com',
  clientId: process.env.OAUTH_CLIENT_ID,
  clientSecret: process.env.OAUTH_CLIENT_SECRET,
  redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/callback'
});

// Configure session middleware
app.use(session({
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false
}));

// Login route - redirect to SSO
app.get('/login', (req, res) => {
  const { url, state } = sso.getLoginUrl();
  req.session.oauthState = state; // Store state for CSRF validation
  res.redirect(url);
});

// Callback route - handle OAuth callback
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  try {
    // Exchange code for tokens and fetch user data
    const result = await sso.exchangeCodeForToken(code, state);

    // Store tokens in session
    req.session.tokens = result;

    res.json({
      message: 'Login successful!',
      user: result.userData,
      sesameCredentials: result.sesameCredentials
    });
  } catch (error) {
    res.status(401).send(`Authentication failed: ${error.message}`);
  }
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Response Examples

Complete Authentication Response

When calling exchangeCodeForToken(), you receive a complete object with tokens and user information:

{
  // OAuth 2.0 tokens
  accessToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUz...",
  refreshToken: "def50200a1b2c3d4e5f6...",
  expiresIn: 3600,
  tokenType: "Bearer",
  
  // User information (OpenID Connect claims)
  userData: {
    sub: "019a0aee-a55f-73e4-8e88-38f451f69a08",
    name: "John Doe",
    email: "[email protected]",
    email_verified: false,
    sesame_connected: true,
    sesame_user_id: "8cfec462-505d-48bf-8455-30aedb8a72ff",
    region: "EU1",
    employees: [
      {
        sesame_employee_id: "f5700683-b929-4165-bbd5-ec79cb2e009d",
        company_id: "562e4aae-b234-4467-bc8b-7c0ebca5fb54",
        company_name: "Acme Corp",
        full_name: "John Doe"
      }
    ]
  },
  
  // Sesame API credentials (for direct API access)
  sesameCredentials: {
    sesame_private_token: "7d83d7feff2b9dd8165d...",
    sesame_public_token: "a1b2c3d4e5f6...",
    region: "EU1",
    sesame_user_id: "8cfec462-505d-48bf-8455-30aedb8a72ff",
    employees: [
      {
        sesame_employee_id: "f5700683-b929-4165-bbd5-ec79cb2e009d",
        company_id: "562e4aae-b234-4467-bc8b-7c0ebca5fb54",
        company_name: "Acme Corp",
        full_name: "John Doe"
      }
    ]
  }
}

Multi-Company Support

Users can be associated with multiple companies. The employees array contains all their employee records:

{
  userData: {
    // ... other fields
    employees: [
      {
        sesame_employee_id: "employee-1-uuid",
        company_id: "company-1-uuid",
        company_name: "Company A",
        full_name: "John Doe"
      },
      {
        sesame_employee_id: "employee-2-uuid",
        company_id: "company-2-uuid",
        company_name: "Company B",
        full_name: "John Doe"
      }
    ]
  },
  sesameCredentials: {
    // Same employees array for direct API access
    employees: [/* same structure */]
  }
}

Important: Your application should handle multi-company scenarios by:

  • Letting the user select which company context to use
  • Storing the selected company_id and sesame_employee_id in the session
  • Using the appropriate employee context when making Sesame API calls

Using Sesame API Client (Recommended)

The easiest way to make requests to Sesame API is using the built-in SesameApiClient:

import { SesameApiClient, SesameHelpers } from '@sesamehr/oauth-client';

// After successful authentication
const result = await sso.exchangeCodeForToken(code, state);

// Create API client from credentials
const apiClient = SesameApiClient.create(result.sesameCredentials);

// Make requests to Sesame API
const meData = await apiClient.get('/api/v3/security/me-oauth');
const employees = await apiClient.get('/api/v3/employees');

// Check if user has employees
if (!SesameHelpers.hasEmployees(result.sesameCredentials)) {
  throw new Error('User is not associated with any company');
}

// Handle multiple employees
if (SesameHelpers.hasMultipleEmployees(result.sesameCredentials)) {
  const employees = SesameHelpers.getEmployees(result.sesameCredentials);
  // Show employee selector UI
} else {
  const employee = SesameHelpers.getFirstEmployee(result.sesameCredentials);
  // Use single employee
}

Manual API Calls (Alternative)

If you prefer to make manual calls, use SesameHelpers to generate URLs:

import axios from 'axios';
import { SesameHelpers } from '@sesamehr/oauth-client';

const { sesame_private_token, region } = result.sesameCredentials;

// Get the API URL for the region
const sesameApiUrl = SesameHelpers.getApiUrl(region);
// Returns: 'https://back-eu.sesametime.com' (for EU region)

// Make authenticated requests
const response = await axios.get(`${sesameApiUrl}/api/v3/security/me-oauth`, {
  headers: {
    'Authorization': `Bearer ${sesame_private_token}`,
    'Accept': 'application/json'
  }
});

API Reference

Constructor

const sso = new SesameSSO(config, options);

Config (required)

| Parameter | Type | Description | |-----------|------|-------------| | ssoBaseUrl | string | Base URL of the SSO server | | clientId | string | OAuth client ID | | clientSecret | string | OAuth client secret | | redirectUri | string | OAuth redirect URI |

Options (optional)

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | defaultScope | string | '' | Default OAuth scopes | | timeout | number | 10000 | HTTP timeout in milliseconds | | stateStore | object | InMemoryTTLStore | Custom state store implementation |

Methods

getLoginUrl(params)

Generate OAuth authorization URL with CSRF protection.

const { url, state } = sso.getLoginUrl({
  scope: 'openid profile email',  // Optional
  extraParams: {                   // Optional
    prompt: 'login',
    login_hint: '[email protected]'
  }
});

Returns: { url: string, state: string }


exchangeCodeForToken(code, state, options)

Exchange authorization code for tokens and optionally fetch user data.

// Fetch everything (default)
const result = await sso.exchangeCodeForToken(code, state);

// Only fetch tokens
const result = await sso.exchangeCodeForToken(code, state, {
  includeUserInfo: false,
  includeSesameCredentials: false
});

Parameters:

  • code (string) - Authorization code from callback
  • state (string) - State parameter for CSRF validation
  • options (object, optional):
    • includeUserInfo (boolean) - Fetch user info (default: true)
    • includeSesameCredentials (boolean) - Fetch Sesame credentials (default: true)

Returns:

{
  accessToken: string,
  refreshToken: string,
  expiresIn: number,
  tokenType: string,
  userData?: object,          // If includeUserInfo is true
  sesameCredentials?: object  // If includeSesameCredentials is true
}

getUserInfo(accessToken)

Get user information from userinfo endpoint.

const userInfo = await sso.getUserInfo(accessToken);

Returns: User information object


getSesameCredentials(accessToken)

Get Sesame-specific credentials (private_token, public_token, region).

const credentials = await sso.getSesameCredentials(accessToken);
// { sesame_private_token, sesame_public_token, region, ... }

Returns: Sesame credentials object


refreshToken(refreshToken)

Refresh an access token using a refresh token.

const newTokens = await sso.refreshToken(oldRefreshToken);
// { access_token, refresh_token, expires_in, token_type }

Returns: New token data


revokeToken(token, tokenTypeHint)

Revoke a token (RFC 7009).

await sso.revokeToken(accessToken, 'access_token');
await sso.revokeToken(refreshToken, 'refresh_token');

Returns: true if successful


destroy()

Stop the state store cleanup timer (call when shutting down).

sso.destroy();

Advanced Usage

Custom State Store (Redis)

For production environments with multiple instances or serverless deployments, use a distributed store like Redis:

import Redis from 'ioredis';

class RedisStateStore {
  constructor(redis, ttlSeconds = 600) {
    this.redis = redis;
    this.ttl = ttlSeconds;
  }

  async set(key, value, expiresAt) {
    const ttl = Math.floor((expiresAt - Date.now()) / 1000);
    await this.redis.setex(key, ttl, JSON.stringify(value));
  }

  async get(key) {
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : undefined;
  }

  async has(key) {
    return (await this.redis.exists(key)) === 1;
  }

  async delete(key) {
    await this.redis.del(key);
  }
}

const redis = new Redis();
const sso = new SesameSSO(config, {
  stateStore: new RedisStateStore(redis)
});

Token Refresh Flow

app.use(async (req, res, next) => {
  if (!req.session.tokens) {
    return res.redirect('/login');
  }

  // Check if token is expired
  const expiresAt = req.session.tokens.expiresAt;
  if (Date.now() > expiresAt - 60000) { // Refresh 1 minute before expiry
    try {
      const newTokens = await sso.refreshToken(req.session.tokens.refreshToken);
      req.session.tokens = {
        ...newTokens,
        expiresAt: Date.now() + (newTokens.expires_in * 1000)
      };
    } catch (error) {
      return res.redirect('/login');
    }
  }

  next();
});

Logout with Token Revocation

app.get('/logout', async (req, res) => {
  if (req.session.tokens?.accessToken) {
    try {
      await sso.revokeToken(req.session.tokens.accessToken);
    } catch (error) {
      console.error('Token revocation failed:', error.message);
    }
  }

  req.session.destroy(() => {
    res.redirect('/');
  });
});

Environment Variables

# SSO Server Configuration
SSO_BASE_URL=http://localhost:8000

# OAuth Client Configuration
OAUTH_CLIENT_ID=your-client-id
OAUTH_CLIENT_SECRET=your-client-secret
REDIRECT_URI=http://localhost:3000/callback

# Application Configuration
APP_PORT=3000
SESSION_SECRET=your-session-secret

Security Best Practices

⚠️ Backend Only

This library is designed for backend use only. Never expose clientSecret in frontend code.

CSRF Protection

Always validate the state parameter:

app.get('/login', (req, res) => {
  const { url, state } = sso.getLoginUrl();
  req.session.oauthState = state; // Store in session
  res.redirect(url);
});

app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // State is automatically validated in exchangeCodeForToken
  const result = await sso.exchangeCodeForToken(code, state);
});

Secure Session Configuration

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production', // HTTPS only in production
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

Token Storage

  • Store tokens in server-side sessions (never in localStorage/cookies on client)
  • Use httpOnly cookies for session IDs
  • Implement token refresh before expiry
  • Revoke tokens on logout

Error Handling

All methods throw descriptive errors with OAuth error details:

try {
  const result = await sso.exchangeCodeForToken(code, state);
} catch (error) {
  // Error format: "OAuth token exchange failed [400] (invalid_grant): The authorization code is invalid"
  console.error(error.message);

  if (error.message.includes('CSRF attack')) {
    // Handle CSRF error
  } else if (error.message.includes('invalid_grant')) {
    // Handle invalid/expired code
  }
}

TypeScript Support

Type definitions are coming soon! For now, use JSDoc:

/**
 * @typedef {Object} SesameTokens
 * @property {string} accessToken
 * @property {string} refreshToken
 * @property {number} expiresIn
 * @property {string} tokenType
 * @property {Object} [userData]
 * @property {Object} [sesameCredentials]
 */

/** @type {SesameTokens} */
const result = await sso.exchangeCodeForToken(code, state);

Testing

Example test with Jest:

import { SesameSSO, InMemoryTTLStore } from '@sesamehr/oauth-client';

describe('SesameSSO', () => {
  let sso;

  beforeEach(() => {
    sso = new SesameSSO({
      ssoBaseUrl: 'http://localhost:8000',
      clientId: 'test-client',
      clientSecret: 'test-secret',
      redirectUri: 'http://localhost:3000/callback'
    });
  });

  afterEach(() => {
    sso.destroy(); // Cleanup timers
  });

  test('generates login URL with state', () => {
    const { url, state } = sso.getLoginUrl();

    expect(url).toContain('http://localhost:8000/oauth/authorize');
    expect(url).toContain(`client_id=test-client`);
    expect(url).toContain(`state=${state}`);
    expect(state).toHaveLength(64); // 32 bytes hex
  });
});

License

MIT License - see LICENSE file for details

Support

For support and questions, please contact the Sesame team at [email protected]


Made with ❤️ by the Sesame team