@verisure-italy/express-authentication-middleware
v1.7.2
Published
Express middleware for Verisure authentication
Downloads
782
Readme
Express Authentication Middleware
A TypeScript Express middleware for authentication using DynamoDB with support for Bearer token validation and user lookup.
Features
- 🔐 Bearer Token Authentication: Extracts and validates Bearer tokens from Authorization headers
- 🗄️ DynamoDB Integration: Uses dynamo-kit for efficient database operations
- 👤 User Lookup: Automatic user retrieval after token validation
- ⏰ Token Expiration: Built-in token expiration validation
- 🎯 Type Safety: Full TypeScript support with AAA types
- 🔄 Backward Compatibility: Supports both DynamoDB and HTTP-based authentication
Installation
npm install @verisure-italy/express-authentication-middleware
# or
pnpm add @verisure-italy/express-authentication-middlewareQuick Start
import express from 'express'
import { dynamoAuthMiddleware } from '@verisure-italy/express-authentication-middleware'
const app = express()
// Configure the middleware
app.use(dynamoAuthMiddleware({
tokenTableName: 'access_token',
userTableName: 'user',
dynamoConfig: {
region: 'eu-west-1'
}
}))
// Protected route
app.get('/protected', (req, res) => {
if (req._auth) {
res.json({ message: `Hello ${req._auth.user.username}`, user: req._auth.user })
} else {
res.status(401).json({ error: 'Authentication required' })
}
})Configuration
DynamoDB Authentication
import { dynamoAuthMiddleware } from '@verisure-italy/express-authentication-middleware'
app.use(dynamoAuthMiddleware({
// Required: Table names
tokenTableName: 'access_token', // Table containing access tokens
userTableName: 'user', // Table containing user data
// Optional: DynamoDB configuration
dynamoConfig: {
region: 'eu-west-1',
endpoint: 'http://localhost:8000', // For local development
tablePrefix: 'dev' // Table prefix
}
}))Environment Variables
The middleware automatically reads these environment variables:
AWS_REGION: Default AWS regionDYNAMO_ENDPOINT: Custom DynamoDB endpointDYNAMO_TABLE_PREFIX: Table name prefixIS_OFFLINE/AWS_SAM_LOCAL: Auto-detects local development
Database Schema
Access Token Table
Uses the AccessToken type from @verisure-italy/aaa-types:
Required fields:
id(String): Token record IDtoken(String): The access token valueuser(String): Username associated with the tokenexpires(Number): Token expiration timestamp in secondsscope(String, optional): Token scopeclient(Object): Client information withidfield
Required indexes:
token-index: GSI withtokenas hash key
Example record:
{
"id": "token-123",
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": "john.doe",
"expires": 1697234567,
"scope": "read write",
"client": { "id": "client-app" },
"createdAt": 1697148167,
"updatedAt": 1697148167
}User Table
Uses the UserDetails type from @verisure-italy/aaa-types:
Required fields:
id(String): User IDusername(String): Usernameroles(Array): User rolescreatedAt(Number): Creation timestampupdatedAt(Number): Last update timestamp
Required indexes:
username-index: GSI withusernameas hash key
Example record:
{
"id": "user-123",
"username": "john.doe",
"roles": ["ROLE_USER", "ROLE_ADMIN"],
"createdAt": 1697148167,
"updatedAt": 1697148167
}Authentication Flow
- Extract Token: Reads
Authorization: Bearer <token>header - Token Lookup: Queries
access_tokentable usingtoken-index - Expiration Check: Validates token hasn't expired
- User Lookup: Queries
usertable usingusername-index - Request Augmentation: Adds authentication info to
req._auth
Request Augmentation
After successful authentication, the middleware adds:
interface Request {
_auth?: {
user: UserDetails
token: {
accessToken: string
accessTokenExpiresAt: string
scope?: string
}
}
}Note: Only _auth is added to the request. This follows the pattern of prefixing middleware properties with _ to avoid conflicts.
Usage Examples
Basic Protected Route
app.get('/profile', (req, res) => {
if (!req._auth) {
return res.status(401).json({ error: 'Authentication required' })
}
res.json({
id: req._auth.user.id,
username: req._auth.user.username,
roles: req._auth.user.roles
})
})Role-Based Authorization
const requireRole = (role: string) => (req: Request, res: Response, next: NextFunction) => {
if (!req._auth) {
return res.status(401).json({ error: 'Authentication required' })
}
if (!req._auth.user.roles.includes(role)) {
return res.status(403).json({ error: 'Insufficient permissions' })
}
next()
}
app.get('/admin', requireRole('ROLE_ADMIN'), (req, res) => {
res.json({ message: 'Admin access granted', user: req._auth!.user })
})Access Token Information
app.get('/token-info', (req, res) => {
if (!req._auth) {
return res.status(401).json({ error: 'Authentication required' })
}
res.json({
token: {
expiresAt: req._auth.token.accessTokenExpiresAt,
scope: req._auth.token.scope
},
user: req._auth.user
})
})Local Development
// Automatic local detection
app.use(dynamoAuthMiddleware({
tokenTableName: 'access_token',
userTableName: 'user'
// Will automatically use localhost:8000 if IS_OFFLINE=true
}))
// Manual local configuration
app.use(dynamoAuthMiddleware({
tokenTableName: 'access_token',
userTableName: 'user',
dynamoConfig: {
endpoint: 'http://localhost:8000',
region: 'local'
}
}))Multi-Environment Setup
const getAuthMiddleware = () => {
const environment = process.env.NODE_ENV || 'development'
const config = {
tokenTableName: 'access_token',
userTableName: 'user',
dynamoConfig: {
region: 'eu-west-1',
tablePrefix: environment
}
}
if (environment === 'development') {
config.dynamoConfig.endpoint = 'http://localhost:8000'
}
return dynamoAuthMiddleware(config)
}
app.use(getAuthMiddleware())Error Handling
The middleware handles various error scenarios:
Authentication Errors (401)
Invalid token: Token not found in databaseToken expired: Token expiration time has passedUser not found: User no longer exists in database
Error Handling Example
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
if (err.statusCode === 401) {
return res.status(401).json({
error: 'Authentication failed',
message: err.message
})
}
// Handle other errors
next(err)
})Backward Compatibility
The middleware maintains backward compatibility with HTTP-based authentication:
import { middleware } from '@verisure-italy/express-authentication-middleware'
// HTTP-based authentication (legacy)
app.use(middleware({
tokenUrl: 'https://auth.example.com/token/info'
}))
// DynamoDB authentication (new)
app.use(middleware({
tokenTableName: 'access_token',
userTableName: 'user'
}))TypeScript Support
Full TypeScript support with proper type definitions:
import { Request, Response } from 'express'
import { UserDetails } from '@verisure-italy/aaa-types'
app.get('/profile', (req: Request, res: Response) => {
// req._auth is properly typed
if (req._auth) {
// TypeScript knows user properties are available
console.log(req._auth.user.username, req._auth.user.roles)
console.log(req._auth.token.accessTokenExpiresAt)
}
})Performance Considerations
- Client Caching: DynamoDB client is cached for optimal performance
- Index Usage: Efficient GSI queries for token and user lookup
- Minimal Data Transfer: Fetches only required fields
- Connection Reuse: Single client instance per configuration
AAA Types Integration
The middleware uses official AAA types:
AccessToken: From@verisure-italy/aaa-typesfor token recordsUserDetails: From@verisure-italy/aaa-typesfor user information
This ensures consistency across the authentication system and automatic compatibility with AAA service schemas.
Security Notes
- Tokens are validated against database records
- Automatic expiration checking prevents stale token usage
- User existence is verified on each request
- No sensitive data is logged or exposed
- Uses timestamp-based expiration (
expiresfield)
Testing
import request from 'supertest'
import express from 'express'
const app = express()
app.use(dynamoAuthMiddleware({ /* config */ }))
describe('Authentication', () => {
it('should require valid token', async () => {
const response = await request(app)
.get('/protected')
.expect(401)
})
it('should accept valid token', async () => {
const response = await request(app)
.get('/protected')
.set('Authorization', 'Bearer valid-token')
.expect(200)
expect(response.body._auth).toBeDefined()
})
})