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 🙏

© 2026 – Pkg Stats / Ryan Hefner

sumor

v3.0.10

Published

Sumor OAuth framework

Readme

Sumor - OAuth Authentication Framework

npm version MIT License

A comprehensive OAuth 2.0 authentication framework for Express.js applications with role-based access control (RBAC). Sumor simplifies OAuth integration, token management, and permission-based route protection in multi-service architectures.

✨ Key Features

  • 🔐 OAuth 2.0 Complete Flow: Full authorization code flow with PKCE support
  • 🔑 Session & Token Management: Secure token exchange, refresh, and blacklisting
  • 🛡️ JWT Verification: Built-in JWT validation using JWKS (JSON Web Key Set)
  • 👥 Role-Based Access Control (RBAC): Permission-based route protection and middleware
  • 📝 TypeScript First: Full TypeScript support with complete type definitions
  • 🚀 Express Integration: Drop-in middleware and route setup
  • 🎯 Permission Sync: Automatic permission synchronization with OAuth provider
  • 💾 Session Revocation: Token blacklist support for logout and session management
  • 🌐 Multi-Domain Support: Built-in domain and origin handling
  • ⚡ Request Context: Access user info and OAuth service in Express request object

📦 Installation

npm install sumor

🚀 Quick Start

Basic Setup

import express from 'express'
import setupSumor from 'sumor'

const app = express()

// Define permissions for your application
// Format: <module>:<operation>
// Operations: view, create, edit, delete
const permissions = {
  permissions: [
    'users:view', // View user information
    'users:edit', // Edit users
    'posts:view', // View posts
    'posts:create', // Create posts
    'posts:edit', // Edit posts
    'posts:delete' // Delete posts
  ],
  permissionLabels: [
    {
      module: 'users',
      zh: '用户管理',
      en: 'User Management'
    },
    {
      module: 'posts',
      zh: '文章管理',
      en: 'Posts Management'
    }
  ]
}

// Initialize Sumor OAuth
await setupSumor(app, permissions)

// Now your app has:
// - OAuth routes: /api/oauth/authorize, /api/oauth/callback, etc.
// - JWT middleware: Automatically validates tokens on protected routes
// - req.sumor: OAuth service available in all request handlers
// - req.jwtUser: User info from JWT token

app.listen(3000, () => console.log('Server ready'))

Accessing User Info in Routes

// User info from JWT token is available in req.jwtUser
app.get('/api/user/profile', (req: any, res) => {
  const user = req.jwtUser

  res.json({
    userId: user.userId,
    roles: user.roles?.split(','),
    permissions: user.permissions?.split(','),
    verified: user.isVerified
  })
})

// Call OAuth service methods via req.sumor
app.get('/api/users/:userId', async (req: any, res) => {
  try {
    // Fetch user info from OAuth provider
    const userInfo = await req.sumor.getUserInfo(req.params.userId)
    res.json(userInfo)
  } catch (error) {
    res.status(400).json({ error: error.message })
  }
})

📚 API Reference

setupSumor(app, permissions)

Initialize Sumor OAuth with your Express application.

Parameters:

  • app (Express.Application) - Your Express app instance
  • permissions (Object) - Permission configuration:
    • permissions (string[]) - List of available permissions
    • permissionLabels (Array) - Human-readable labels for permission modules

Returns: Promise

Example:

const permissions = {
  permissions: ['posts:view', 'posts:edit', 'posts:delete', 'users:view', 'users:create'],
  permissionLabels: [
    {
      module: 'posts',
      zh: '帖子管理',
      en: 'Posts Management'
    }
  ]
}
await setupSumor(app, permissions)

req.jwtUser

Available in all route handlers after middleware initialization. Contains the user info from JWT token.

Properties:

  • userId (string) - Unique user identifier
  • roles (string) - Comma-separated role IDs
  • permissions (string) - Comma-separated user permissions
  • isVerified (number) - Verification status
  • tenantId (string) - Multi-tenant identifier
  • exp (number) - Token expiration timestamp
  • iat (number) - Token issued at timestamp

Example:

app.get('/api/protected', (req: any, res) => {
  const { userId, roles, permissions } = req.jwtUser
  res.json({ userId, roles: roles?.split(','), permissions: permissions?.split(',') })
})

req.sumor (OAuthService)

Available in all route handlers. Provides methods to call the OAuth provider's API.

Methods:

getUserInfo(userId)

Get detailed user information from the OAuth provider.

const userInfo = await req.sumor.getUserInfo('user123')
// Returns: { userId, name, email, avatar, ... }

getUsersInfo(userIds)

Get information for multiple users.

const users = await req.sumor.getUsersInfo(['user1', 'user2'])
// Returns: [{ userId, name, ... }, ...]

searchUsers(searchTerm, limit)

Search for users by name or email.

const results = await req.sumor.searchUsers('john', 20)
// Returns: [{ userId, name, email, ... }, ...]

exchangeCode(grantType, code, redirectUri, codeVerifier)

Exchange authorization code for tokens (internal use).

const tokens = await req.sumor.exchangeCode(
  'authorization_code',
  authCode,
  'http://localhost:3000/callback',
  codeVerifier
)
// Returns: { accessToken, refreshToken, expiresIn, tokenType }

checkBlacklist(sessionId)

Check if a session token is revoked.

const isBlacklisted = await req.sumor.checkBlacklist(sessionId)

revokeSession(sessionId)

Revoke (logout) a session.

await req.sumor.revokeSession(sessionId)

OAuth Routes

Sumor automatically registers these routes:

  • GET /api/oauth/callback - Handle OAuth provider callback with authorization code (no auth required)
  • PUT /api/oauth/token - Refresh access token and get user info + authorization URL (can use refreshToken from body or cookie)
    • Response includes endpoint and authorizeUrl for OAuth configuration, and user object with current user info
  • POST /api/oauth/logout - Logout and revoke session (requires valid token)

🎯 Web Client (Sumor Class)

The Sumor framework includes a client-side class for browser applications to manage OAuth and user state.

Basic Setup

import { setupSumor } from 'sumor'

// Call this on app initialization
await setupSumor()

// Now use window.sumor to access the Sumor client
console.log(window.sumor.user) // Current user info or null

Sumor Client Properties

  • endpoint - OAuth provider endpoint
  • authorizeUrl - Authorization URL for login redirect
  • user - Current user object or null
    • id - User ID
    • isVerified - Verification status
    • roles - Comma-separated role list
    • permissions - Comma-separated permission list

Sumor Client Methods

refresh(force = false)

Refresh OAuth configuration and user info from PUT /api/oauth/token using the stored refresh token.

// Use cache if available
await window.sumor.refresh()

// Force refresh, ignore cache
await window.sumor.refresh(true)

refreshConfig()

Manually refresh configuration (same as refresh(true)).

await window.sumor.refreshConfig()

login()

Redirect to OAuth authorization page.

window.sumor.login()

logout()

Logout and clear local user state.

await window.sumor.logout()
// window.sumor.user becomes null

hasPermission(module, operation = '*')

Check if user has a specific permission.

// Check specific permission
if (window.sumor.hasPermission('posts', 'edit')) {
  // User can edit posts
}

// Check module (any operation)
if (window.sumor.hasPermission('posts')) {
  // User has any posts permission
}

if (window.sumor.hasPermission('posts', '*')) {
  // Same as above
}

hasRole(role)

Check if user has a specific role.

if (window.sumor.hasRole('admin')) {
  // User is admin
}

onUserChange(callback)

Subscribe to user state changes (login, logout, token refresh).

window.sumor.onUserChange(user => {
  if (user) {
    console.log('User logged in:', user.id)
  } else {
    console.log('User logged out')
  }
})

Web Client Example

import { setupSumor } from 'sumor'
import { ref, watch } from 'vue'

export default {
  setup() {
    const userInfo = ref(null)
    const isLoggedIn = ref(false)

    onMounted(async () => {
      await setupSumor()

      // Get initial user state
      userInfo.value = window.sumor.user
      isLoggedIn.value = !!window.sumor.user

      // Subscribe to user changes
      window.sumor.onUserChange(user => {
        userInfo.value = user
        isLoggedIn.value = !!user
      })
    })

    return {
      userInfo,
      isLoggedIn,
      login: () => window.sumor.login(),
      logout: () => window.sumor.logout(),
      canEdit: () => window.sumor.hasPermission('posts', 'edit')
    }
  }
}

📚 API Reference

Sumor uses environment variables for configuration. The key is configuring the OAuth provider endpoint:

# OAuth Provider Configuration
OAUTH_ENDPOINT=https://auth.example.com
OAUTH_CLIENT_KEY=your-app-client-id
OAUTH_CLIENT_SECRET=your-app-client-secret
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback

How it works:

  • OAUTH_ENDPOINT - Base URL of your OAuth provider (e.g., https://auth.example.com)
  • OAuth endpoints are automatically derived: {OAUTH_ENDPOINT}/api/oauth/...
  • OAUTH_CLIENT_KEY and OAUTH_CLIENT_SECRET - OAuth application credentials
  • OAUTH_REDIRECT_URI - Callback URL that matches your OAuth provider configuration
  • JWT tokens are verified using JWKS public keys from the OAuth provider (no local secret needed)

Example configurations:

# Local development
OAUTH_ENDPOINT=http://localhost:3001
OAUTH_CLIENT_KEY=myapp-dev
OAUTH_CLIENT_SECRET=myapp-dev-secret
OAUTH_REDIRECT_URI=http://localhost:3000/api/oauth/callback

# Production
OAUTH_ENDPOINT=https://auth.mycompany.com
OAUTH_CLIENT_KEY=myapp-prod
OAUTH_CLIENT_SECRET=<secure-secret>
OAUTH_REDIRECT_URI=https://app.mycompany.com/api/oauth/callback

📚 Usage Examples

Example 1: Protect Routes with Permission Checks

// Server-side: Express route with permission check
app.post('/api/posts', (req: any, res) => {
  // Check if user has permission
  const permissions = req.jwtUser.permissions?.split(',') || []

  if (!permissions.includes('posts:create')) {
    return res.status(403).json({ error: 'Insufficient permissions' })
  }

  // Create post logic here
  res.json({ postId: 123 })
})

// Client-side: Vue component with permission check
export default {
  setup() {
    return {
      canCreatePost: () => window.sumor.hasPermission('posts', 'create')
    }
  }
}

// Template
<button v-if="canCreatePost()" @click="createPost">Create Post</button>

Example 2: Multi-Tenant Support

app.get('/api/tenant/users', (req: any, res) => {
  const tenantId = req.jwtUser.tenantId

  // Fetch users for the user's tenant
  db.query('SELECT * FROM users WHERE tenant_id = ?', [tenantId]).then(users => res.json(users))
})

Example 3: Fetch Related User Data

app.get('/api/followers', async (req: any, res) => {
  try {
    // Get list of follower IDs from your local database
    const followerIds = await db.query(
      'SELECT follower_id FROM relationships WHERE leader_id = ?',
      [req.jwtUser.userId]
    )

    // Get detailed info for all followers from OAuth provider
    const followerInfo = await req.sumor.getUsersInfo(followerIds.map(r => r.follower_id))

    res.json(followerInfo)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

Example 4: User Search with Pagination

app.get('/api/search/users', async (req: any, res) => {
  const { q, limit = 20 } = req.query

  if (!q) {
    return res.status(400).json({ error: 'Search term required' })
  }

  try {
    const results = await req.sumor.searchUsers(q, limit)
    res.json(results)
  } catch (error) {
    res.status(500).json({ error: error.message })
  }
})

🛡️ Security Considerations

Token Storage

  • Tokens are stored in HTTP-only cookies by default (secure against XSS)
  • Always use HTTPS in production to prevent token interception
  • Refresh tokens should be rotated regularly

Permission Validation

Always validate permissions on sensitive operations:

const userPermissions = req.jwtUser.permissions?.split(',') || []

if (!userPermissions.includes('users:edit')) {
  return res.status(403).json({ error: 'User edit permission required' })
}

Session Revocation

Logout invalidates tokens immediately:

// On logout
await req.sumor.revokeSession(req.jwtUser.jti)
// Token is added to blacklist and becomes invalid

CSRF Protection

Implement CSRF tokens for state-changing operations:

const csrf = require('csurf')
const csrfProtection = csrf({ cookie: false })

app.post('/api/posts', csrfProtection, (req: any, res) => {
  // Handle POST with CSRF protection
})

🐛 Troubleshooting

"Token verification failed"

Cause: JWT signature doesn't match JWKS public key

Solution: Ensure OAUTH_ENDPOINT is correctly configured. Sumor automatically fetches JWKS from {OAUTH_ENDPOINT}/api/oauth/jwks

"Unauthorized" on protected routes

Cause: Missing or invalid JWT token in request

Solution: Client must include Authorization header:

Authorization: Bearer <JWT_TOKEN>

"Session not found" when revoking

Cause: Session ID (jti) is invalid or already revoked

Solution: Check that req.jwtUser.jti contains a valid session ID

Permission check always fails

Cause: Permissions string format is incorrect

Solution: Permissions are comma-separated strings. Parse correctly:

const permissions = req.jwtUser.permissions?.split(',').map(p => p.trim()) || []

🔄 Architecture

┌──────────────────────────────────────┐
│      Browser / Mobile Client         │
└───────────────┬──────────────────────┘
                │ (1) Click Login
                ▼
┌──────────────────────────────────────┐
│    Your Express App (Port 3000)      │
│  ┌────────────────────────────────┐  │
│  │ PUT /api/oauth/token           │  │ (2) Get OAuth URL & User Info
│  │ GET /api/oauth/callback        │  │
│  │ POST /api/oauth/logout         │  │
│  └────────────────────────────────┘  │
└───────────┬──────────────────────────┘
            │ (3) Redirect to OAuth
            ▼
┌──────────────────────────────────────┐
│      OAuth Provider                  │
│   {OAUTH_ENDPOINT}/api/oauth/...     │
│   - Issue JWT tokens                 │
│   - Manage users & permissions       │
│   - Provide JWKS public keys         │
└──────────────────────────────────────┘
            ▲
            │ (4) Verify JWT
            │
        req.sumor

Flow Steps:

  1. User clicks "Login" on your app
  2. App calls PUT /api/oauth/token with refresh token to get OAuth provider URL and user info
  3. User redirected to OAuth provider
  4. OAuth provider authenticates and redirects back to /api/oauth/callback with authorization code
  5. Server exchanges code for JWT token via ITS OAuth API
  6. Server verifies JWT signature using ITS JWKS public keys
  7. Subsequent requests include JWT in Authorization header
  8. Middleware validates JWT and extracts user info (userId, roles, permissions)
  9. Routes access user info via req.jwtUser and call OAuth service via req.sumor

🤝 Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

� License

MIT License - see LICENSE for details

📞 Support

For issues, questions, or suggestions, please open an issue on GitHub.


Made with ❤️ by Lycoo