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
- JWT token validation with JWKS endpoint integration
- API key authentication for simpler scenarios
- Role-based access control (RBAC) for authorization
- Custom authorization policies
- Integration with Azure AD/Microsoft Entra ID
When to Use Each Pattern
| Pattern | Use Case | Security Level |
|---|---|---|
| API Key | Internal services, simple clients | Basic |
| JWT Token | External APIs, mobile apps, SPAs | High |
| Azure AD | Enterprise apps, Microsoft ecosystem | Highest |
| Custom | Special requirements, legacy systems | Variable |
Problem
Your HTTP functions need authentication:
- Validate JWT tokens on incoming requests
- Check API keys
- Implement role-based access control
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
- Always use HTTPS - Never send tokens over plain HTTP
- Validate audience and issuer - Don't just check signature
- Implement token expiration - Short-lived tokens are safer
- Log authentication failures - Monitor for attacks
- Use Azure AD for enterprise - Managed identity eliminates secrets
- Implement rate limiting - Prevent brute-force attacks
Summary
- Implement JWT validation middleware for HTTP triggers
- Use API key validation for simpler scenarios
- Check roles for authorization
- Return appropriate 401/403 responses