sumor
v3.0.10
Published
Sumor OAuth framework
Readme
Sumor - OAuth Authentication Framework
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 instancepermissions(Object) - Permission configuration:permissions(string[]) - List of available permissionspermissionLabels(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 identifierroles(string) - Comma-separated role IDspermissions(string) - Comma-separated user permissionsisVerified(number) - Verification statustenantId(string) - Multi-tenant identifierexp(number) - Token expiration timestampiat(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
endpointandauthorizeUrlfor OAuth configuration, anduserobject with current user info
- Response includes
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 nullSumor Client Properties
endpoint- OAuth provider endpointauthorizeUrl- Authorization URL for login redirectuser- Current user object or nullid- User IDisVerified- Verification statusroles- Comma-separated role listpermissions- 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 nullhasPermission(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/callbackHow 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_KEYandOAUTH_CLIENT_SECRET- OAuth application credentialsOAUTH_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 invalidCSRF 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.sumorFlow Steps:
- User clicks "Login" on your app
- App calls
PUT /api/oauth/tokenwith refresh token to get OAuth provider URL and user info - User redirected to OAuth provider
- OAuth provider authenticates and redirects back to
/api/oauth/callbackwith authorization code - Server exchanges code for JWT token via ITS OAuth API
- Server verifies JWT signature using ITS JWKS public keys
- Subsequent requests include JWT in Authorization header
- Middleware validates JWT and extracts user info (userId, roles, permissions)
- Routes access user info via
req.jwtUserand call OAuth service viareq.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
