Security Best Practices
Comprehensive security guide for implementing and maintaining Signia authentication in your applications.
Overview
Security is a shared responsibility between Signia (the platform) and you (the application developer). This guide covers best practices for secure implementation.
Authentication Security
1. Always Use HTTPS
Why: Prevents man-in-the-middle attacks and token interception
Implementation:
// ✅ Production
redirectUri: 'https://myapp.com/oidc-callback'
// ❌ Never in production
redirectUri: 'http://myapp.com/oidc-callback'
// ✅ Local development exception
redirectUri: 'http://localhost:5173/oidc-callback'
Enforce HTTPS:
// Express middleware
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production' && !req.secure) {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});
2. Protect Client Secrets
Never:
- ❌ Commit secrets to version control
- ❌ Expose secrets in frontend code
- ❌ Log secrets in application logs
- ❌ Share secrets in support tickets
Always:
- ✅ Store in environment variables
- ✅ Use secret management systems (AWS Secrets Manager, HashiCorp Vault)
- ✅ Rotate secrets regularly (every 90 days)
- ✅ Use different secrets per environment
Example:
# .env (not committed to git)
OIDC_CLIENT_SECRET=sk_live_abc123...
# .gitignore
.env
.env.local
.env.production
// Server-side (Node.js/Express) - Load from environment
const oidcClient = new OIDCClient({
clientId: process.env.OIDC_CLIENT_ID!,
clientSecret: process.env.OIDC_CLIENT_SECRET!, // Server-side only
redirectUri: process.env.OIDC_REDIRECT_URI!,
issuer: process.env.OIDC_ISSUER!
});
3. Implement PKCE
What: Proof Key for Code Exchange
Why: Protects against authorization code interception
When: Always use for public clients (SPAs, mobile apps)
const oidcClient = new OIDCClient({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'http://localhost:5173/oidc-callback',
issuer: 'https://tenant.signiaauth.com',
usePKCE: true // ✅ Enable PKCE
});
How it works:
- Client generates random
code_verifier - Creates
code_challenge= SHA256(code_verifier) - Sends
code_challengein authorization request - Server stores
code_challenge - Client sends
code_verifierduring token exchange - Server verifies SHA256(code_verifier) == stored challenge
4. Validate State Parameter
What: Random string to prevent CSRF attacks
Why: Ensures callback came from your authorization request
// Signia SDK handles this automatically
// But if implementing manually:
// Before redirect
const state = crypto.randomBytes(32).toString('hex');
sessionStorage.setItem('oauth_state', state);
// On callback
const returnedState = new URL(window.location.href).searchParams.get('state');
const expectedState = sessionStorage.getItem('oauth_state');
if (returnedState !== expectedState) {
throw new Error('Invalid state parameter - possible CSRF attack');
}
5. Validate Redirect URIs
Backend validation:
const ALLOWED_REDIRECT_URIS = [
'http://localhost:5173/oidc-callback',
'https://myapp.com/oidc-callback',
'https://staging.myapp.com/oidc-callback'
];
function validateRedirectUri(uri: string): boolean {
return ALLOWED_REDIRECT_URIS.some(allowed =>
uri.startsWith(allowed)
);
}
Dashboard configuration: Only add redirect URIs you control. Never add third-party domains.
Token Security
1. Secure Token Storage
Frontend (Browser):
// ✅ Best: HTTP-only cookies (backend-managed)
// Tokens not accessible to JavaScript
// Protected from XSS
// ⚠️ Acceptable: Memory only (loses on refresh)
// No persistence, good for security
// Bad for UX
// ❌ Never: localStorage or sessionStorage
// Vulnerable to XSS attacks
Recommended approach:
// Store tokens in memory only
class TokenManager {
private accessToken: string | null = null;
private refreshToken: string | null = null;
setTokens(access: string, refresh: string) {
this.accessToken = access;
this.refreshToken = refresh;
}
getAccessToken(): string | null {
return this.accessToken;
}
clear() {
this.accessToken = null;
this.refreshToken = null;
}
}
Backend (Node.js):
// ✅ Use secure session stores
import session from 'express-session';
import RedisStore from 'connect-redis';
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible to JavaScript
sameSite: 'lax', // CSRF protection
maxAge: 3600000 // 1 hour
}
}));
2. Token Validation
Always validate tokens:
async function validateAccessToken(token: string): Promise<boolean> {
try {
// Verify signature and claims
const decoded = await oidcClient.verifyAccessToken(token);
// Check expiration
if (decoded.exp < Date.now() / 1000) {
return false;
}
// Check issuer
if (decoded.iss !== 'https://tenant.signiaauth.com') {
return false;
}
// Check audience
if (decoded.aud !== 'YOUR_CLIENT_ID') {
return false;
}
return true;
} catch (error) {
return false;
}
}
3. Token Refresh
Automatic refresh before expiration:
async function refreshTokenIfNeeded(tokens: Tokens): Promise<Tokens> {
const decoded = jwt.decode(tokens.access_token);
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
// Refresh if less than 5 minutes remaining
if (expiresIn < 300) {
const newTokens = await oidcClient.refreshAccessToken(tokens.refresh_token);
return newTokens;
}
return tokens;
}
4. Token Scope Validation
Validate required scopes:
function requireScope(requiredScope: string) {
return (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
const decoded = jwt.decode(token);
if (!decoded.scope.includes(requiredScope)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.get('/api/admin', requireScope('admin'), (req, res) => {
// Admin-only endpoint
});
Session Security
1. Session Configuration
// Express session configuration
app.use(session({
secret: process.env.SESSION_SECRET!, // Strong random secret
name: '__session', // Don't use default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS in production
httpOnly: true, // Prevent XSS
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: process.env.COOKIE_DOMAIN // Set appropriate domain
},
store: new RedisStore({ // Use persistent store
client: redisClient,
prefix: 'sess:'
})
}));
2. Session Timeout
Implement idle timeout:
function sessionTimeout(timeout: number) {
return (req, res, next) => {
if (req.session.lastActivity) {
const elapsed = Date.now() - req.session.lastActivity;
if (elapsed > timeout) {
req.session.destroy(() => {
return res.redirect('/login?reason=timeout');
});
return;
}
}
req.session.lastActivity = Date.now();
next();
};
}
// 30 minute idle timeout
app.use(sessionTimeout(30 * 60 * 1000));
3. Concurrent Session Management
Limit concurrent sessions:
interface UserSession {
userId: string;
sessionId: string;
createdAt: number;
}
async function checkConcurrentSessions(userId: string, maxSessions: number) {
const sessions = await redis.smembers(`user:${userId}:sessions`);
if (sessions.length >= maxSessions) {
// Remove oldest session
const oldest = sessions[0];
await redis.srem(`user:${userId}:sessions`, oldest);
await redis.del(`session:${oldest}`);
}
}
API Security
1. Rate Limiting
Prevent brute force attacks:
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false
});
app.post('/api/login', loginLimiter, loginHandler);
2. CORS Configuration
Restrict cross-origin requests:
import cors from 'cors';
app.use(cors({
origin: [
'https://myapp.com',
'https://staging.myapp.com',
process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : ''
].filter(Boolean),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
3. Input Validation
Validate all inputs:
import { z } from 'zod';
const loginSchema = z.object({
email: z.string().email(),
redirectUri: z.string().url()
});
app.post('/api/login', async (req, res) => {
try {
const validated = loginSchema.parse(req.body);
// Process login
} catch (error) {
return res.status(400).json({ error: 'Invalid input' });
}
});
4. SQL Injection Prevention
Use parameterized queries:
// ✅ Safe - parameterized
const user = await db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
// ❌ Vulnerable - string concatenation
const user = await db.query(
`SELECT * FROM users WHERE email = '${email}'`
);
Frontend Security
1. XSS Prevention
Sanitize user input:
import DOMPurify from 'dompurify';
function SafeHTML({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Use framework protections:
// ✅ React automatically escapes
<div>{userInput}</div>
// ❌ Dangerous
<div dangerouslySetInnerHTML={{ __html: userInput }} />
2. Content Security Policy
Implement CSP headers:
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' https://signiaauth.com; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://signiaauth.com https://api.myapp.com; " +
"frame-ancestors 'none';"
);
next();
});
3. Secure Dependencies
Regularly audit dependencies:
# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Use dependency scanning (GitHub, Snyk, etc.)
Monitoring & Logging
1. Security Logging
Log security events:
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'security.log' })
]
});
// Log authentication events
logger.info('User login', {
userId: user.id,
ip: req.ip,
userAgent: req.get('user-agent'),
timestamp: new Date()
});
// Log suspicious activity
logger.warn('Failed login attempt', {
email: req.body.email,
ip: req.ip,
reason: 'Invalid credentials'
});
2. Anomaly Detection
Detect suspicious patterns:
async function detectAnomalies(userId: string, ip: string) {
const recentLogins = await getRecentLogins(userId, 24); // Last 24 hours
// Check for multiple IPs
const uniqueIPs = new Set(recentLogins.map(l => l.ip));
if (uniqueIPs.size > 5) {
await alertSecurity('Multiple IP addresses detected', { userId, count: uniqueIPs.size });
}
// Check for unusual location
const location = await getIPLocation(ip);
const userLocations = await getUserLocations(userId);
if (!userLocations.includes(location.country)) {
await alertSecurity('Login from new country', { userId, country: location.country });
}
}
Compliance
1. GDPR Compliance
Data minimization:
// Only request necessary scopes
scopes: ['openid', 'email'] // ✅ Minimal
// Avoid
scopes: ['openid', 'profile', 'email', 'address', 'phone'] // ❌ Excessive
Right to be forgotten:
async function deleteUserData(userId: string) {
// Delete user account
await db.users.delete({ id: userId });
// Delete sessions
await redis.del(`user:${userId}:*`);
// Delete audit logs (anonymize or delete per policy)
await db.auditLogs.update(
{ userId },
{ userId: 'deleted_user' }
);
}
2. PCI DSS (if applicable)
- Never store credit card data
- Use tokenization for payments
- Implement network segmentation
- Regular security audits
Security Checklist
Pre-Launch
- HTTPS enabled in production
- Client secrets in environment variables
- PKCE enabled for public clients
- State parameter validation implemented
- Redirect URI validation configured
- Rate limiting enabled
- CORS properly configured
- Session security configured
- Token storage secure
- Input validation implemented
- Dependencies audited
- CSP headers configured
- Security logging enabled
Post-Launch
- Monitor security logs daily
- Rotate secrets every 90 days
- Update dependencies monthly
- Conduct security audits quarterly
- Review access controls quarterly
- Test incident response plan annually
Incident Response
1. Compromise Response
If client secret compromised:
- Immediately regenerate secret in dashboard
- Update application configuration
- Review audit logs for suspicious activity
- Notify affected users if necessary
If user account compromised:
- Suspend account immediately
- Revoke all sessions
- Reset authentication credentials
- Investigate breach
- Notify user
2. Security Contacts
Report vulnerabilities:
- Email: security@signiaid.com
- Include: Detailed description, steps to reproduce
- Response time: Within 24 hours
Next Steps
- WebAuthn & Passkeys - Passwordless authentication
- Multi-Tenancy - Tenant security
- Dashboard - Admin controls