Sumor OAuth framework
npm install sumor

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.
- đ 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
``bash`
npm install sumor
`typescript
import express from 'express'
import setupSumor from 'sumor'
const app = express()
// Define permissions for your application
// Format:
// 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'))
`
`typescript
// 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 })
}
})
`
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:
`typescript`
const permissions = {
permissions: ['posts:view', 'posts:edit', 'posts:delete', 'users:view', 'users:create'],
permissionLabels: [
{
module: 'posts',
zh: 'å¸åįŽĄį',
en: 'Posts Management'
}
]
}
await setupSumor(app, permissions)
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:
`typescript`
app.get('/api/protected', (req: any, res) => {
const { userId, roles, permissions } = req.jwtUser
res.json({ userId, roles: roles?.split(','), permissions: permissions?.split(',') })
})
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.
`typescript`
const userInfo = await req.sumor.getUserInfo('user123')
// Returns: { userId, name, email, avatar, ... }
#### getUsersInfo(userIds)
Get information for multiple users.
`typescript`
const users = await req.sumor.getUsersInfo(['user1', 'user2'])
// Returns: [{ userId, name, ... }, ...]
#### searchUsers(searchTerm, limit)
Search for users by name or email.
`typescript`
const results = await req.sumor.searchUsers('john', 20)
// Returns: [{ userId, name, email, ... }, ...]
#### exchangeCode(grantType, code, redirectUri, codeVerifier)
Exchange authorization code for tokens (internal use).
`typescript`
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.
`typescript`
const isBlacklisted = await req.sumor.checkBlacklist(sessionId)
#### revokeSession(sessionId)
Revoke (logout) a session.
`typescript`
await req.sumor.revokeSession(sessionId)
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)endpoint
- Response includes and authorizeUrl for OAuth configuration, and user object with current user infoPOST /api/oauth/logout
- - Logout and revoke session (requires valid token)
The Sumor framework includes a client-side class for browser applications to manage OAuth and user state.
`typescript
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
`
- 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
#### refresh(force = false)
Refresh OAuth configuration and user info from PUT /api/oauth/token using the stored refresh token.
`typescript
// 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)).
`typescript`
await window.sumor.refreshConfig()
#### login()
Redirect to OAuth authorization page.
`typescript`
window.sumor.login()
#### logout()
Logout and clear local user state.
`typescript`
await window.sumor.logout()
// window.sumor.user becomes null
#### hasPermission(module, operation = '\*')
Check if user has a specific permission.
`typescript
// 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.
`typescript`
if (window.sumor.hasRole('admin')) {
// User is admin
}
#### onUserChange(callback)
Subscribe to user state changes (login, logout, token refresh).
`typescript`
window.sumor.onUserChange(user => {
if (user) {
console.log('User logged in:', user.id)
} else {
console.log('User logged out')
}
})
`typescript
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')
}
}
}
`
Sumor uses environment variables for configuration. The key is configuring the OAuth provider endpoint:
`bash`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_ENDPOINT}/api/oauth/...
- OAuth endpoints are automatically derived: OAUTH_CLIENT_KEY
- and OAUTH_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:
`bashLocal 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
đ Usage Examples
$3
`typescript
// 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
`$3
`typescript
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))
})
`$3
`typescript
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 })
}
})
`$3
`typescript
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
$3
- 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
$3
Always validate permissions on sensitive operations:
`typescript
const userPermissions = req.jwtUser.permissions?.split(',') || []if (!userPermissions.includes('users:edit')) {
return res.status(403).json({ error: 'User edit permission required' })
}
`$3
Logout invalidates tokens immediately:
`typescript
// On logout
await req.sumor.revokeSession(req.jwtUser.jti)
// Token is added to blacklist and becomes invalid
`$3
Implement CSRF tokens for state-changing operations:
`typescript
const csrf = require('csurf')
const csrfProtection = csrf({ cookie: false })app.post('/api/posts', csrfProtection, (req: any, res) => {
// Handle POST with CSRF protection
})
`đ Troubleshooting
$3
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$3
Cause: Missing or invalid JWT token in request
Solution: Client must include Authorization header:
`
Authorization: Bearer
`$3
Cause: Session ID (jti) is invalid or already revoked
Solution: Check that
req.jwtUser.jti contains a valid session ID$3
Cause: Permissions string format is incorrect
Solution: Permissions are comma-separated strings. Parse correctly:
`typescript
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`Contributions are welcome! Please feel free to submit issues and pull requests.
MIT License - see LICENSE for details
For issues, questions, or suggestions, please open an issue on GitHub.
---
Made with â¤ī¸ by Lycoo