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
/apipath. - 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
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
| Feature | Free Plan | Standard Plan |
|---|---|---|
| Bandwidth | 100 GB/month | 100 GB/month + Overage charges |
| Storage | 0.5 GB | 2 GB |
| Custom Domains | 2 | 5 |
| Staging Environments | 3 | 10 |
| Azure Functions | Free tier only | Dedicated plan available |
| SLA | None | 99.95% |
| Authenticated Users | Unlimited | Unlimited |
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
- User logs in with an authentication provider.
- Redirects to
/.auth/login/<provider>/callback. - Azure Functions implements
/api/auth/rolesor a custom endpoint. - Returns custom roles based on user information.
- 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/meendpoint. - 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
-
Check User Info
// Get current user info on frontendfetch('/.auth/me').then(res => res.json()).then(data => console.log('User info:', data)); -
Check Role Info
// Log user info within Azure Functionconst userInfo = req.headers['x-ms-client-principal'];const decoded = Buffer.from(userInfo, 'base64').toString('utf-8');context.log('User details:', JSON.parse(decoded)); -
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.