Skip to main content

Azure Static Web Apps

Azure Static Web Apps is a fully managed service that automatically builds and deploys code from GitHub or Azure DevOps to Azure. It delivers static content globally and seamlessly integrates serverless APIs using Azure Functions.

Overview

What is Azure Static Web Apps

Azure Static Web Apps is a hosting service for modern web application development with the following features:

  • Automated Build & Deploy: Integrates with GitHub or Azure DevOps to automatically build and deploy on push.
  • Global CDN: Delivers static content from edge locations worldwide.
  • Integrated API: Automatic integration of serverless APIs using Azure Functions.
  • Built-in Authentication: Use multiple authentication providers without configuration.
  • Custom Domains & SSL: Support for free SSL certificates and custom domains.
  • Preview Environments: Automatically creates staging environments for each pull request.

Architecture

┌─────────────────────────────────────────────────────┐
│ GitHub / Azure DevOps │
│ (Source Code) │
└──────────────────┬──────────────────────────────────┘
│ Push/PR

┌─────────────────────────────────────────────────────┐
│ GitHub Actions / Azure Pipelines │
│ (Automated Build & Deploy) │
└──────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│ Azure Static Web Apps │
│ ┌──────────────────┐ ┌───────────────────┐ │
│ │ Static Content │ │ Azure Functions │ │
│ │ (HTML/JS/CSS) │ │ (API) │ │
│ └──────────────────┘ └───────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Authentication & Authorization │ │
│ │ (GitHub, Azure AD, Twitter, Google, etc.) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘


Users (Delivered from global edge)

Key Features

1. Static Content Hosting

  • Hosts static files such as HTML, CSS, JavaScript, and images.
  • Supports SPAs (Single Page Applications) like React, Angular, Vue.js, Blazor WebAssembly.
  • Supports static site generators like Next.js, Gatsby, Hugo.

2. Serverless API Integration

  • Automatically integrates Azure Functions at the /api path.
  • Supports Node.js, Python, .NET, Java, etc.
  • Served on the same domain as static content, eliminating CORS issues.

3. Built-in Authentication

The following authentication providers are available by default:

  • Azure Active Directory (AAD)
  • GitHub
  • Twitter
  • Google
  • Facebook

Authentication Endpoints:

  • /.auth/login/<provider>: Login
  • /.auth/logout: Logout
  • /.auth/me: Get user information

4. Custom Domains and SSL

  • Automatically issues free SSL certificates (Let's Encrypt).
  • Easy custom domain configuration.
  • Supports both Apex domains and subdomains.

Pricing Plans

FeatureFree PlanStandard Plan
Bandwidth100 GB/month100 GB/month + Overage charges
Storage0.5 GB2 GB
Custom Domains25
Staging Environments310
Azure FunctionsFree tier onlyDedicated plan available
SLANone99.95%
Authenticated UsersUnlimitedUnlimited

Implementing Custom Roles with Azure Functions

In Azure Static Web Apps, you can implement custom authentication logic and role assignment using Azure Functions.

Custom Authentication Mechanism

  1. User logs in with an authentication provider.
  2. Redirects to /.auth/login/<provider>/callback.
  3. Azure Functions implements /api/auth/roles or a custom endpoint.
  4. Returns custom roles based on user information.
  5. Role information is available in subsequent requests.

Example Implementation of Custom Role API (Node.js)

Create an api folder in your project directory and implement an Azure Function like the following.

api/GetUserRoles/index.js

module.exports = async function (context, req) {
// Get authenticated user info
const userInfo = req.headers['x-ms-client-principal'];

if (!userInfo) {
context.res = {
status: 401,
body: { error: "Authentication required" }
};
return;
}

// Base64 decode
const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');
const user = JSON.parse(decoded);

// Determine roles based on user info
const roles = determineUserRoles(user);

context.res = {
status: 200,
body: {
userId: user.userId,
userDetails: user.userDetails,
roles: roles
}
};
};

function determineUserRoles(user) {
const roles = ['authenticated'];

// Assign role based on email address
if (user.userDetails && user.userDetails.endsWith('@company.com')) {
roles.push('employee');
}

// Grant admin role to specific user IDs
const adminIds = [
'github|123456',
'aad|abcd-1234-efgh-5678'
];

if (adminIds.includes(user.userId)) {
roles.push('admin');
}

// Example of fetching role info from database
// const dbRoles = await fetchRolesFromDatabase(user.userId);
// roles.push(...dbRoles);

return roles;
}

api/GetUserRoles/function.json

{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["get", "post"],
"route": "auth/roles"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}

Role Management Integrated with Database

In real applications, it is common to manage role information in a database.

const { CosmosClient } = require('@azure/cosmos');

// Initialize Cosmos DB client
const client = new CosmosClient(process.env.COSMOS_CONNECTION_STRING);
const database = client.database('UsersDB');
const container = database.container('UserRoles');

async function getUserRolesFromDatabase(userId) {
try {
const { resource } = await container
.item(userId, userId)
.read();

return resource ? resource.roles : ['authenticated'];
} catch (error) {
console.error('Error fetching roles:', error);
return ['authenticated']; // Default role
}
}

module.exports = async function (context, req) {
const userInfo = req.headers['x-ms-client-principal'];

if (!userInfo) {
context.res = { status: 401 };
return;
}

const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');
const user = JSON.parse(decoded);

// Get roles from database
const roles = await getUserRolesFromDatabase(user.userId);

context.res = {
status: 200,
body: {
userId: user.userId,
roles: roles
}
};
};

Authentication Settings with staticwebapp.config.json

By placing a staticwebapp.config.json file in the root of your project, you can configure routing, authentication, and authorization in detail.

Basic Configuration File

{
"routes": [
{
"route": "/admin/*",
"allowedRoles": ["admin"]
},
{
"route": "/api/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/public/*",
"allowedRoles": ["anonymous"]
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["/images/*", "/css/*", "/api/*"]
},
"responseOverrides": {
"401": {
"rewrite": "/login.html",
"statusCode": 302
},
"403": {
"rewrite": "/forbidden.html"
},
"404": {
"rewrite": "/404.html"
}
}
}

Key Configuration Items

1. Route and Role-Based Access Control

{
"routes": [
{
"route": "/admin/*",
"allowedRoles": ["admin"],
"statusCode": 403
},
{
"route": "/profile",
"allowedRoles": ["authenticated"]
},
{
"route": "/login",
"allowedRoles": ["anonymous"]
}
]
}
  • route: Path to apply access control (wildcard * available).
  • allowedRoles: Roles allowed access (array).
    • "anonymous": All users including unauthenticated ones.
    • "authenticated": All authenticated users.
    • Custom roles: "admin", "editor", "employee", etc.
  • statusCode: HTTP status code when access is denied (optional).

2. Customizing Authentication Providers

{
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/<tenant-id>/v2.0",
"clientIdSettingName": "AAD_CLIENT_ID",
"clientSecretSettingName": "AAD_CLIENT_SECRET"
},
"login": {
"loginParameters": ["scope=openid profile email"]
}
},
"customOpenIdConnectProviders": {
"myCustomProvider": {
"registration": {
"clientIdSettingName": "CUSTOM_CLIENT_ID",
"clientCredential": {
"clientSecretSettingName": "CUSTOM_CLIENT_SECRET"
},
"openIdConnectConfiguration": {
"wellKnownOpenIdConfiguration": "https://provider.com/.well-known/openid-configuration"
}
},
"login": {
"nameClaimType": "name",
"scopes": ["openid", "profile", "email"]
}
}
}
}
}
}

3. Redirect Rules

{
"routes": [
{
"route": "/old-page",
"redirect": "/new-page",
"statusCode": 301
},
{
"route": "/external",
"redirect": "https://example.com",
"statusCode": 302
}
]
}

4. Customizing Headers

{
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'self'"
},
"routes": [
{
"route": "/api/*",
"headers": {
"Cache-Control": "no-cache, no-store, must-revalidate",
"X-Custom-Header": "Custom-Value"
}
}
]
}

5. MIME Type Settings

{
"mimeTypes": {
".json": "application/json",
".wasm": "application/wasm",
".csv": "text/csv"
}
}

Practical Configuration Examples

Multi-Tenant Application

{
"routes": [
{
"route": "/admin/*",
"allowedRoles": ["admin", "superadmin"]
},
{
"route": "/tenant/*/dashboard",
"allowedRoles": ["authenticated"]
},
{
"route": "/api/tenant/*",
"allowedRoles": ["tenant-user", "tenant-admin"]
}
],
"navigationFallback": {
"rewrite": "/index.html"
},
"responseOverrides": {
"401": {
"redirect": "/.auth/login/aad",
"statusCode": 302
},
"403": {
"rewrite": "/access-denied.html"
}
},
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "SAMEORIGIN",
"Strict-Transport-Security": "max-age=31536000"
}
}

Configuration for SPA (Single Page Application)

{
"routes": [
{
"route": "/api/*",
"allowedRoles": ["authenticated"]
},
{
"route": "/admin",
"allowedRoles": ["admin"],
"rewrite": "/index.html"
}
],
"navigationFallback": {
"rewrite": "/index.html",
"exclude": ["/assets/*", "/images/*", "/*.{css,js,json,ico,png,jpg,svg}"]
},
"responseOverrides": {
"401": {
"redirect": "/.auth/login/github",
"statusCode": 302
}
}
}

Best Practices

1. Security

  • Principle of Least Privilege: Grant only the minimum necessary privileges to roles.
  • Protect Sensitive Information: Use environment variables for API keys and connection strings.
  • Enforce HTTPS: Use HTTPS for all communications (enabled by default).
  • Security Headers: Properly configure CSP, X-Frame-Options, etc.
{
"globalHeaders": {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
}
}

2. Performance

  • Cache Strategy: Set appropriate cache headers for static assets.
  • File Size Optimization: Compress and optimize images and JavaScript.
  • Leverage CDN: Azure Static Web Apps automatically uses a global CDN.
  • API Efficiency: Avoid unnecessary data transfer and minimize responses.
{
"routes": [
{
"route": "/assets/*",
"headers": {
"Cache-Control": "public, max-age=31536000, immutable"
}
},
{
"route": "/api/*",
"headers": {
"Cache-Control": "no-cache, no-store, must-revalidate"
}
}
]
}

3. Deployment and CI/CD

  • Environment Variable Management: Manage settings that differ by environment in the Azure Portal.
  • Leverage Staging Environments: Test in preview environments automatically created for each PR.
  • Phased Rollout: Apply changes to the production environment in phases.
  • Rollback Plan: Prepare quick rollback procedures in case of issues.

4. Monitoring and Logging

  • Application Insights: Monitor application performance and errors.
  • Logging: Record appropriate logs within Azure Functions.
  • Alert Settings: Detect abnormal traffic or rising error rates.
// Example of logging within Azure Functions
const appInsights = require('applicationinsights');
appInsights.setup(process.env.APPINSIGHTS_INSTRUMENTATIONKEY);
const client = appInsights.defaultClient;

module.exports = async function (context, req) {
try {
// Processing
client.trackEvent({
name: 'UserRoleAssigned',
properties: { userId: user.userId, role: 'admin' }
});
} catch (error) {
client.trackException({ exception: error });
context.log.error('Error occurred:', error);
}
};

Troubleshooting

Common Issues and Solutions

1. Not Redirecting After Authentication

Cause: Improper setting of post_login_redirect_uri.

Solution:

// Specify correct redirect URI in login link
<a href="/.auth/login/github?post_login_redirect_uri=/dashboard">
Login with GitHub
</a>

2. 403 Error Accessing API

Cause: Improper setting of allowedRoles.

Solution: Check staticwebapp.config.json.

{
"routes": [
{
"route": "/api/*",
"allowedRoles": ["authenticated"] // Changed from anonymous
}
]
}

3. Custom Roles Not Reflected

Cause: Role assignment API is not being called correctly.

Solution:

  • Check the Network tab in browser developer tools.
  • Check current role info at /.auth/me endpoint.
  • Check Azure Functions logs for errors.
# Check logs with Azure CLI
az staticwebapp functions logs show --name <app-name> --resource-group <rg-name>

4. Authentication Testing in Local Development

Solution: Use Azure Static Web Apps CLI.

# Install
npm install -g @azure/static-web-apps-cli

# Run locally
swa start ./build --api-location ./api

# Run with authentication emulation
swa start ./build --api-location ./api --app-devserver-url=http://localhost:3000

Debugging Techniques

  1. Check User Info

    // Get current user info on frontend
    fetch('/.auth/me')
    .then(res => res.json())
    .then(data => console.log('User info:', data));
  2. Check Role Info

    // Log user info within Azure Function
    const userInfo = req.headers['x-ms-client-principal'];
    const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');
    context.log('User details:', JSON.parse(decoded));
  3. Validate Configuration File

    • Check "Configuration" of Static Web App in Azure Portal.
    • Check build errors in deployment logs.
    • Check GitHub Actions logs.

Summary

Azure Static Web Apps is the optimal platform for modern web application development:

  • Simple Deployment: Automated deployment integrated with GitHub.
  • Scalable: High-speed delivery via global CDN.
  • Secure: Flexible access control with built-in authentication and custom roles.
  • Cost-Effective: Rich features even in the free plan.
  • Developer Experience: Comprehensive toolchain from local development to preview environments.

By combining staticwebapp.config.json and Azure Functions, you can build enterprise-level authentication and authorization systems.

References