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​