← Back to ArticlesFunctions

Functions — HTTP Trigger Authentication and Authorization Patterns

Implementing JWT validation, API key authentication, role-based authorization, and custom auth for Azure Functions HTTP triggers.

Functions — HTTP Trigger Authentication and Authorization Patterns

Overview

Securing your Azure Functions HTTP endpoints is critical for protecting your APIs and data. This guide covers multiple authentication patterns - from simple API keys to enterprise-grade JWT validation with role-based authorization. Each pattern has specific use cases depending on your security requirements and client types.

What You'll Learn

When to Use Each Pattern

PatternUse CaseSecurity Level
API KeyInternal services, simple clientsBasic
JWT TokenExternal APIs, mobile apps, SPAsHigh
Azure ADEnterprise apps, Microsoft ecosystemHighest
CustomSpecial requirements, legacy systemsVariable

Problem

Your HTTP functions need authentication:


Solution

JWT Validation Middleware

// auth middleware
import { AzureFunction, Context, HttpRequest } from "@azure/functions";

interface AuthConfig {
    issuer: string;
    audience: string;
    jwksUrl: string;
}

const validateJwt = async (req: HttpRequest, config: AuthConfig): Promise<boolean> => {
    const authHeader = req.headers.get("authorization");
    
    if (!authHeader?.startsWith("Bearer ")) {
        return false;
    }
    
    const token = authHeader.substring(7);
    
    // Validate token (use jsonwebtoken library)
    try {
        const payload = await verifyToken(token, config);
        req.user = payload;
        return true;
    } catch {
        return false;
    }
};

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    const isValid = await validateJwt(req, {
        issuer: "https://your-idp.com",
        audience: "your-api",
        jwksUrl: "https://your-idp.com/.well-known/jwks.json"
    });
    
    if (!isValid) {
        context.res = { status: 401, body: { error: "Unauthorized" } };
        return;
    }
    
    // Check roles
    const userRoles = req.user.roles || [];
    if (!userRoles.includes("Admin")) {
        context.res = { status: 403, body: { error: "Forbidden" } };
        return;
    }
    
    // Process request
    context.res = { status: 200, json: { data: "Success" } };
};

API Key Validation

const validateApiKey = (req: HttpRequest): boolean => {
    const apiKey = req.headers.get("x-api-key");
    const validKeys = ["key1", "key2", "key3"];
    
    return validKeys.includes(apiKey);
};

Real-Time Scenarios

Scenario 1: Multi-Tenant SaaS API with JWT

A SaaS application serving multiple customers with different permissions:

interface TenantContext {
    tenantId: string;
    userId: string;
    roles: string[];
    permissions: string[];
}

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    // Extract JWT and validate
    const authHeader = req.headers.get("authorization");
    if (!authHeader?.startsWith("Bearer ")) {
        context.res = { 
            status: 401, 
            body: { error: "Missing or invalid authorization header" } 
        };
        return;
    }
    
    const token = authHeader.substring(7);
    const payload = await validateAndDecodeJwt(token);
    
    // Build tenant context from JWT claims
    const tenantContext: TenantContext = {
        tenantId: payload.tenant_id,
        userId: payload.sub,
        roles: payload.roles || [],
        permissions: payload.permissions || []
    };
    
    // Check if user can access this tenant
    if (!tenantContext.permissions.includes("api:read")) {
        context.res = { 
            status: 403, 
            body: { error: "Insufficient permissions for this operation" } 
        };
        return;
    }
    
    // Get resource ID from URL
    const resourceId = req.params.id;
    
    // Verify tenant has access to this resource
    const hasAccess = await verifyResourceAccess(tenantContext.tenantId, resourceId);
    if (!hasAccess) {
        context.res = { 
            status: 403, 
            body: { error: "Access denied to this resource" } 
        };
        return;
    }
    
    // Process request with tenant context
    const result = await getResource(tenantContext.tenantId, resourceId);
    context.res = { status: 200, json: result };
};

Scenario 2: API Key Management with Key Rotation

Managing API keys with automatic rotation for service-to-service communication:

class ApiKeyManager {
    private validKeys: Map<string, ApiKeyInfo> = new Map();
    private rotationInterval: number = 90 * 24 * 60 * 60 * 1000; // 90 days
    
    async validateApiKey(apiKey: string): Promise<ApiKeyValidationResult> {
        const keyInfo = this.validKeys.get(apiKey);
        
        if (!keyInfo) {
            return { valid: false, reason: "Invalid API key" };
        }
        
        // Check if key is expired
        if (keyInfo.expiresAt && keyInfo.expiresAt < new Date()) {
            return { valid: false, reason: "API key has expired" };
        }
        
        // Check if key is revoked
        if (keyInfo.revoked) {
            return { valid: false, reason: "API key has been revoked" };
        }
        
        // Check rate limit
        const rateLimitKey = `rate:${apiKey}`;
        const requestCount = await this.getRateLimitCount(rateLimitKey);
        if (requestCount > keyInfo.rateLimit) {
            return { valid: false, reason: "Rate limit exceeded", retryAfter: 60 };
        }
        
        // Log access
        await this.logApiKeyAccess(apiKey, keyInfo.serviceId);
        
        return { 
            valid: true, 
            serviceId: keyInfo.serviceId,
            permissions: keyInfo.permissions 
        };
    }
    
    async rotateKey(serviceId: string): Promise<string> {
        // Generate new key
        const newKey = this.generateApiKey();
        
        // Store new key
        this.validKeys.set(newKey, {
            serviceId,
            createdAt: new Date(),
            expiresAt: new Date(Date.now() + this.rotationInterval),
            permissions: await this.getServicePermissions(serviceId),
            rateLimit: 10000
        });
        
        // Revoke old key after grace period
        const oldKey = await this.getCurrentKey(serviceId);
        if (oldKey) {
            await this.scheduleKeyRevocation(oldKey, 24 * 60 * 60 * 1000); // 24 hours
        }
        
        return newKey;
    }
}

const validateApiKey = (req: HttpRequest): boolean => {
    const apiKey = req.headers.get("x-api-key");
    const validKeys = ["key1", "key2", "key3"];
    
    return validKeys.includes(apiKey);
};

Scenario 3: OAuth 2.0 with Azure AD

Integrating with Azure Active Directory (Microsoft Entra ID):

import { DefaultAzureCredential, TokenCredential } from "@azure/identity";

// .NET Example for Azure AD integration
public class AzureAdAuthMiddleware
{
    private readonly TokenCredential _credential;
    private readonly string _validIssuer;
    private readonly string _audience;
    
    public async Task<IActionResult> InvokeAsync(HttpContext context)
    {
        var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
        
        if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
        {
            return Unauthorized(new { error = "Missing bearer token" });
        }
        
        var token = authHeader.Substring(7);
        
        try
        {
            // Validate token against Azure AD
            var claims = await ValidateAzureAdTokenAsync(token);
            
            // Add claims to request context
            context.Items["UserId"] = claims.Subject;
            context.Items["TenantId"] = claims.TenantId;
            context.Items["Roles"] = claims.Roles;
            
            // Check required role
            var requiredRole = context.Request.RouteValues["requiredRole"]?.ToString();
            if (!string.IsNullOrEmpty(requiredRole) && !claims.Roles.Contains(requiredRole))
            {
                return Forbid();
            }
        }
        catch (Exception ex)
        {
            return Unauthorized(new { error = "Invalid token", details = ex.Message });
        }
        
        return null; // Continue to next middleware
    }
}

Scenario 4: Role-Based Access Control (RBAC)

Implementing granular permissions for different user types:

interface Permission {
    resource: string;
    action: "create" | "read" | "update" | "delete";
}

// Define role permissions
const rolePermissions: Record<string, Permission[]> = {
    Admin: [
        { resource: "*", action: "create" },
        { resource: "*", action: "read" },
        { resource: "*", action: "update" },
        { resource: "*", action: "delete" }
    ],
    Manager: [
        { resource: "orders", action: "create" },
        { resource: "orders", action: "read" },
        { resource: "orders", action: "update" },
        { resource: "reports", action: "read" }
    ],
    User: [
        { resource: "orders", action: "create" },
        { resource: "orders", action: "read" }
    ],
    Guest: [
        { resource: "products", action: "read" }
    ]
};

function checkPermission(userRoles: string[], resource: string, action: string): boolean {
    for (const role of userRoles) {
        const permissions = rolePermissions[role] || [];
        
        const hasPermission = permissions.some(p => 
            (p.resource === "*" || p.resource === resource) && 
            p.action === action
        );
        
        if (hasPermission) return true;
    }
    
    return false;
}

const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
    const userRoles = req.user?.roles || [];
    const resource = req.params.resource;
    const action = req.method.toLowerCase() as any;
    
    if (!checkPermission(userRoles, resource, action)) {
        context.res = { 
            status: 403, 
            body: { 
                error: "Insufficient permissions",
                required: { resource, action },
                userRoles 
            } 
        };
        return;
    }
    
    // Process authorized request
    context.res = { status: 200, json: { success: true } };
};

Scenario 5: Custom Claims-Based Authorization

Extending authorization with custom business logic:

interface CustomClaims {
    userId: string;
    tenantId: string;
    department: string;
    clearanceLevel: number;
    managedTeams: string[];
}

async function authorizeWithCustomClaims(
    req: HttpRequest, 
    claims: CustomClaims
): Promise<AuthorizationResult> {
    // Check department access
    const department = req.params.department;
    if (claims.department !== department && claims.clearanceLevel < 5) {
        return { authorized: false, reason: "Department access denied" };
    }
    
    // Check team management permissions
    if (req.url.includes("/team/") && req.method === "DELETE") {
        const teamId = req.params.teamId;
        if (!claims.managedTeams.includes(teamId)) {
            return { authorized: false, reason: "Not authorized to manage this team" };
        }
    }
    
    // Check time-based access (e.g., business hours)
    const now = new Date();
    const hour = now.getUTCHours();
    if (hour < 9 || hour > 17) {
        // Allow if user has after-hours access
        if (!claims.managedTeams.some(t => true)) { // Check custom claim
            return { authorized: false, reason: "Access only available during business hours" };
        }
    }
    
    return { authorized: true };
}

Testing Authentication

# Test with valid JWT
curl -X GET "https://your-function.azurewebsites.net/api/protected" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Test with invalid token
curl -X GET "https://your-function.azurewebsites.net/api/protected" \
  -H "Authorization: Bearer invalid-token"

# Test with API key
curl -X GET "https://your-function.azurewebsites.net/api/protected" \
  -H "X-Api-Key: your-api-key"

# Test missing auth
curl -X GET "https://your-function.azurewebsites.net/api/protected"

Best Practices

  1. Always use HTTPS - Never send tokens over plain HTTP
  2. Validate audience and issuer - Don't just check signature
  3. Implement token expiration - Short-lived tokens are safer
  4. Log authentication failures - Monitor for attacks
  5. Use Azure AD for enterprise - Managed identity eliminates secrets
  6. Implement rate limiting - Prevent brute-force attacks

Summary