DEV Community

Cover image for Secure Serverless Authentication: 5 Production-Ready Techniques for Developers
Aarav Joshi
Aarav Joshi

Posted on

Secure Serverless Authentication: 5 Production-Ready Techniques for Developers

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Authentication is one of the critical components of any modern application. With the growing popularity of serverless architectures, implementing authentication systems requires specialized approaches that work within the constraints of stateless, ephemeral computing environments. I've spent years building these systems and want to share the most effective techniques I've discovered.

Token-Based Authentication in Serverless Environments

Serverless functions are stateless by nature, making token-based authentication an ideal fit. JSON Web Tokens (JWT) have become the standard for this purpose due to their self-contained nature and cryptographic security.

When implementing JWT authentication in a serverless context, I focus on creating a system that validates user credentials and issues a signed token containing the user's identity and permissions.

const jwt = require('jsonwebtoken'); const bcrypt = require('bcryptjs'); exports.login = async (event) => { const { username, password } = JSON.parse(event.body); // Fetch user from database (implementation depends on your database) const user = await getUserFromDB(username); if (!user || !bcrypt.compareSync(password, user.passwordHash)) { return { statusCode: 401, body: JSON.stringify({ message: 'Invalid credentials' }) }; } // Create token with appropriate claims and expiration const token = jwt.sign( { sub: user.id, username: user.username, roles: user.roles }, process.env.JWT_SECRET, { expiresIn: '1h' } ); // Set token as HttpOnly cookie for security return { statusCode: 200, headers: { 'Set-Cookie': `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600; Path=/`, }, body: JSON.stringify({ message: 'Login successful' }) }; }; 
Enter fullscreen mode Exit fullscreen mode

For token validation, I create a dedicated middleware function that can be reused across multiple serverless functions:

const jwt = require('jsonwebtoken'); const verifyToken = async (event) => { try { // Extract token from cookies or Authorization header const token = extractTokenFromRequest(event); if (!token) { throw new Error('No token provided'); } // Verify token and return user information const decoded = jwt.verify(token, process.env.JWT_SECRET); return { isAuthorized: true, user: decoded }; } catch (error) { return { isAuthorized: false, error: error.message }; } }; // Helper function to extract token from different sources function extractTokenFromRequest(event) { // Check Authorization header const authHeader = event.headers?.Authorization; if (authHeader?.startsWith('Bearer ')) { return authHeader.substring(7); } // Check cookies if (event.cookies) { const cookies = event.cookies; const tokenCookie = cookies.find(c => c.startsWith('token=')); if (tokenCookie) { return tokenCookie.split('=')[1]; } } return null; } 
Enter fullscreen mode Exit fullscreen mode

I've found that using short-lived tokens (1 hour or less) with refresh token rotation provides the best security. This approach minimizes the risk of token theft while maintaining a smooth user experience.

OAuth Integration for Serverless Functions

OAuth 2.0 enables third-party authentication without exposing user credentials. Implementing OAuth in a serverless environment requires careful handling of the authorization flow.

First, I create a function that initiates the OAuth flow:

const crypto = require('crypto'); exports.initiateOAuth = async (event) => { // Generate state parameter to prevent CSRF const state = crypto.randomBytes(16).toString('hex'); // Store state in a database or cache with short TTL await storeStateInDatabase(state, 10 * 60); // 10 minutes // Construct the authorization URL const authUrl = `https://oauth-provider.com/authorize?` + `client_id=${process.env.CLIENT_ID}&` + `redirect_uri=${encodeURIComponent(process.env.REDIRECT_URI)}&` + `state=${state}&` + `response_type=code&` + `scope=profile email`; return { statusCode: 302, headers: { Location: authUrl } }; }; 
Enter fullscreen mode Exit fullscreen mode

Then, I implement a callback handler to process the authorization code:

const axios = require('axios'); exports.oauthCallback = async (event) => { const { code, state } = event.queryStringParameters; // Verify state parameter to prevent CSRF const storedState = await getStateFromDatabase(state); if (!storedState) { return { statusCode: 400, body: JSON.stringify({ message: 'Invalid state parameter' }) }; } // Exchange code for tokens try { const tokenResponse = await axios.post('https://oauth-provider.com/token', { grant_type: 'authorization_code', code, redirect_uri: process.env.REDIRECT_URI, client_id: process.env.CLIENT_ID, client_secret: process.env.CLIENT_SECRET }); const { access_token, refresh_token } = tokenResponse.data; // Get user profile using access token const profileResponse = await axios.get('https://oauth-provider.com/userinfo', { headers: { Authorization: `Bearer ${access_token}` } }); // Create or update user in your database const user = await createOrUpdateUser(profileResponse.data); // Create session token for the user const sessionToken = createSessionToken(user); return { statusCode: 302, headers: { 'Set-Cookie': `token=${sessionToken}; HttpOnly; Secure; Path=/`, Location: '/dashboard' } }; } catch (error) { console.error('OAuth token exchange failed:', error); return { statusCode: 500, body: JSON.stringify({ message: 'Authentication failed' }) }; } }; 
Enter fullscreen mode Exit fullscreen mode

A key consideration with serverless OAuth is cold start times. I prepackage dependencies and use connection pooling where possible to minimize latency during the authentication flow.

Multi-Factor Authentication for Added Security

Implementing MFA in serverless systems often means managing time-based one-time passwords (TOTP). The most secure approach stores only the secret key and validates codes within the serverless function.

Here's how I typically implement TOTP-based MFA:

const speakeasy = require('speakeasy'); const QRCode = require('qrcode'); // Function to set up MFA for a user exports.setupMFA = async (event) => { const userId = getUserIdFromEvent(event); // Generate a new secret const secret = speakeasy.generateSecret({ name: `MyApp:${userId}`, length: 20 }); // Store the secret in your database await storeUserSecret(userId, secret.base32); // Generate QR code for easy setup const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url); return { statusCode: 200, body: JSON.stringify({ secret: secret.base32, qrCode: qrCodeUrl }) }; }; // Function to verify TOTP code during login exports.verifyMFA = async (event) => { const { userId, code } = JSON.parse(event.body); // Retrieve the user's secret from the database const userSecret = await getUserSecret(userId); // Verify the provided code const verified = speakeasy.totp.verify({ secret: userSecret, encoding: 'base32', token: code, window: 1 // Allow 1 period before and after for clock skew }); if (!verified) { return { statusCode: 401, body: JSON.stringify({ message: 'Invalid MFA code' }) }; } // Code is valid, proceed with authentication // Generate session token or complete the login process const token = generateSessionToken(userId); return { statusCode: 200, body: JSON.stringify({ token }) }; }; 
Enter fullscreen mode Exit fullscreen mode

I've found that storing MFA secrets in a separate, highly secure database collection with additional encryption provides the best protection. Remember that MFA secrets are effectively password equivalents and should be treated with the same level of security.

Passwordless Authentication Systems

Passwordless authentication is gaining popularity for its security and usability benefits. In serverless environments, I implement this using short-lived tokens sent via email or SMS.

const crypto = require('crypto'); const AWS = require('aws-sdk'); const ses = new AWS.SES(); // Function to initiate passwordless login exports.initiatePasswordlessLogin = async (event) => { const { email } = JSON.parse(event.body); // Generate a secure random token const token = crypto.randomBytes(32).toString('hex'); // Set expiration time (15 minutes from now) const expiresAt = new Date(); expiresAt.setMinutes(expiresAt.getMinutes() + 15); // Store token in database with expiration await storeLoginToken(email, token, expiresAt); // Create magic link const magicLink = `https://myapp.com/verify-login?token=${token}&email=${encodeURIComponent(email)}`; // Send email with magic link await ses.sendEmail({ Source: 'noreply@myapp.com', Destination: { ToAddresses: [email] }, Message: { Subject: { Data: 'Your login link' }, Body: { Text: { Data: `Click this link to log in: ${magicLink}` }, Html: { Data: `<p>Click <a href="${magicLink}">here</a> to log in</p>` } } } }).promise(); return { statusCode: 200, body: JSON.stringify({ message: 'Login link sent' }) }; }; // Function to verify magic link and complete login exports.verifyLoginToken = async (event) => { const { token, email } = event.queryStringParameters; // Retrieve token from database const storedToken = await getLoginToken(email, token); // Check if token exists and is not expired if (!storedToken || new Date() > new Date(storedToken.expiresAt)) { return { statusCode: 401, body: JSON.stringify({ message: 'Invalid or expired token' }) }; } // Delete the used token await deleteLoginToken(email, token); // Create a session for the user const user = await getUserByEmail(email); const sessionToken = createSessionToken(user.id); return { statusCode: 302, headers: { 'Set-Cookie': `token=${sessionToken}; HttpOnly; Secure; Path=/`, Location: '/dashboard' } }; }; 
Enter fullscreen mode Exit fullscreen mode

The key security considerations I focus on with passwordless systems are token entropy (using cryptographically secure random values), short expiration times, and single-use tokens. These measures prevent token reuse or brute force attacks.

Rate Limiting to Prevent Abuse

Authentication endpoints are prime targets for brute force attacks. Implementing rate limiting is essential but presents challenges in serverless environments due to their stateless nature.

I use distributed rate limiting with DynamoDB for state management:

const AWS = require('aws-sdk'); const dynamoDB = new AWS.DynamoDB.DocumentClient(); // Middleware for rate limiting exports.rateLimitMiddleware = async (event) => { const ip = event.requestContext.identity.sourceIp; const endpoint = event.path; const key = `${ip}:${endpoint}`; const now = Date.now(); const windowSize = 60 * 1000; // 1 minute window const maxAttempts = 5; // Maximum attempts allowed in the window // Get current rate limiting status const params = { TableName: 'RateLimits', Key: { id: key } }; try { const result = await dynamoDB.get(params).promise(); const record = result.Item || { id: key, count: 0, firstRequest: now }; // Check if we need to reset the window if (now - record.firstRequest > windowSize) { // Window expired, reset counter record.count = 1; record.firstRequest = now; } else { // Increment counter record.count += 1; } // Save updated record await dynamoDB.put({ TableName: 'RateLimits', Item: record }).promise(); // Check if rate limit exceeded if (record.count > maxAttempts) { return { statusCode: 429, body: JSON.stringify({ message: 'Too many requests, please try again later' }) }; } // Rate limit not exceeded, proceed with request return null; } catch (error) { console.error('Rate limiting error:', error); // On error, allow the request to proceed to avoid blocking legitimate users return null; } }; // Example usage in an authentication function exports.login = async (event) => { // Apply rate limiting const rateLimitResult = await exports.rateLimitMiddleware(event); if (rateLimitResult) { return rateLimitResult; } // Proceed with normal login logic // ... }; 
Enter fullscreen mode Exit fullscreen mode

For high-volume applications, I've found Redis-based rate limiting to be more performant, though it requires maintaining a Redis instance outside the serverless environment.

Session Management for Serverless Applications

Session management in serverless applications requires careful consideration of state persistence and security. I typically use a combined approach of JWT for session identification and a database for session control.

const jwt = require('jsonwebtoken'); const AWS = require('aws-sdk'); const dynamoDB = new AWS.DynamoDB.DocumentClient(); // Create a new session exports.createSession = async (userId) => { const sessionId = generateUniqueId(); const now = Date.now(); // Store session in database with metadata await dynamoDB.put({ TableName: 'Sessions', Item: { id: sessionId, userId, createdAt: now, lastActivity: now, expiresAt: now + (7 * 24 * 60 * 60 * 1000), // 7 days userAgent: event.headers['User-Agent'] } }).promise(); // Create session token with minimal claims const token = jwt.sign( { sid: sessionId }, process.env.JWT_SECRET, { expiresIn: '7d' } ); return token; }; // Verify and refresh session exports.verifySession = async (event) => { try { // Extract and verify token const token = extractTokenFromRequest(event); const decoded = jwt.verify(token, process.env.JWT_SECRET); // Get session from database const result = await dynamoDB.get({ TableName: 'Sessions', Key: { id: decoded.sid } }).promise(); const session = result.Item; // Check if session exists and is not expired if (!session || Date.now() > session.expiresAt) { throw new Error('Session expired'); } // Update last activity time await dynamoDB.update({ TableName: 'Sessions', Key: { id: decoded.sid }, UpdateExpression: 'set lastActivity = :now', ExpressionAttributeValues: { ':now': Date.now() } }).promise(); // Return user information return { isAuthorized: true, userId: session.userId }; } catch (error) { return { isAuthorized: false, error: error.message }; } }; // End session (logout) exports.endSession = async (event) => { try { const token = extractTokenFromRequest(event); const decoded = jwt.verify(token, process.env.JWT_SECRET); // Delete session from database await dynamoDB.delete({ TableName: 'Sessions', Key: { id: decoded.sid } }).promise(); return { statusCode: 200, headers: { 'Set-Cookie': 'token=; HttpOnly; Secure; SameSite=Strict; Max-Age=0; Path=/' }, body: JSON.stringify({ message: 'Logged out successfully' }) }; } catch (error) { return { statusCode: 400, body: JSON.stringify({ message: 'Invalid session' }) }; } }; 
Enter fullscreen mode Exit fullscreen mode

This hybrid approach gives me the best of both worlds: the performance benefits of JWTs for session identification and the security benefits of server-side session validation and revocation.

I also implement a background function that runs periodically to clean up expired sessions:

exports.cleanupExpiredSessions = async (event) => { const now = Date.now(); // Scan for expired sessions const scanResults = await dynamoDB.scan({ TableName: 'Sessions', FilterExpression: 'expiresAt < :now', ExpressionAttributeValues: { ':now': now } }).promise(); // Delete expired sessions const deletePromises = scanResults.Items.map(session => { return dynamoDB.delete({ TableName: 'Sessions', Key: { id: session.id } }).promise(); }); await Promise.all(deletePromises); return { statusCode: 200, body: JSON.stringify({ message: `Cleaned up ${deletePromises.length} expired sessions` }) }; }; 
Enter fullscreen mode Exit fullscreen mode

Through years of implementing these authentication techniques in serverless environments, I've learned that security and performance can coexist. The key is to understand the unique constraints of serverless computing and design authentication systems that work with these constraints rather than against them.

By implementing token-based authentication with proper security measures, integrating OAuth for third-party authentication, adding multi-factor authentication for sensitive operations, offering passwordless options for better user experience, protecting against abuse with rate limiting, and managing sessions securely, I've built authentication systems that provide both security and scalability in serverless architectures.

The serverless paradigm continues to evolve, and so do authentication techniques. I'm constantly refining these approaches based on new security research and platform capabilities. The techniques shared here represent current best practices that have proven effective in production environments across various applications.


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (1)

Collapse
 
wpqwpq profile image
WS

Excellent article.

I would add passkeys as another authentication mechanism which is gaining traction to complete the article.