HTTP Authentication¶
This recipe covers authentication and authorization for Azure Functions HTTP triggers — authorization levels, function keys, Easy Auth for identity provider integration, and manual JWT validation in Python.
Authentication Flow¶
flowchart LR
CLIENT[Client App] --> TOKEN[Identity Provider Token Issuance]
TOKEN --> REQUEST[Bearer Token in HTTP Request]
REQUEST --> EASYAUTH[Easy Auth on Function App]
EASYAUTH -->|Valid| FUNC[Python Function Code]
EASYAUTH -->|Invalid or Missing| DENY[401 or Redirect] Authorization Levels¶
Every HTTP-triggered function has an authorization level that controls access. The v2 programming model supports three levels:
| Level | Constant | Key Required | Typical Use |
|---|---|---|---|
| Anonymous | func.AuthLevel.ANONYMOUS | None | Public APIs, health checks |
| Function | func.AuthLevel.FUNCTION | Function key or host key | Internal APIs, service-to-service |
| Admin | func.AuthLevel.ADMIN | Master host key only | Admin operations |
Setting Auth Level Globally¶
Set the default authorization level on the FunctionApp constructor. All registered HTTP functions inherit this level unless overridden:
import azure.functions as func
from blueprints.health import bp as health_bp
from blueprints.api import bp as api_bp
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
app.register_functions(health_bp)
app.register_functions(api_bp)
With this configuration, every HTTP endpoint requires a function key by default.
Setting Auth Level Per Route¶
Override the global level on individual routes using the auth_level parameter:
import azure.functions as func
import json
bp = func.Blueprint()
@bp.route(route="health", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def health(req: func.HttpRequest) -> func.HttpResponse:
"""Public health check — no key required."""
return func.HttpResponse(
json.dumps({"status": "healthy"}),
mimetype="application/json",
status_code=200
)
@bp.route(route="admin/config", methods=["POST"], auth_level=func.AuthLevel.ADMIN)
def admin_config(req: func.HttpRequest) -> func.HttpResponse:
"""Admin-only endpoint — requires master key."""
body = req.get_json()
return func.HttpResponse(
json.dumps({"updated": True}),
mimetype="application/json",
status_code=200
)
Function Keys¶
When a function uses FUNCTION or ADMIN auth level, callers must include a key. There are two ways to pass the key:
Query Parameter¶
Request Header¶
Key Types¶
| Key Type | Scope | How to Retrieve |
|---|---|---|
| Function key | Single function | az functionapp function keys list --name your-func --resource-group your-rg --function-name my_func |
| Host key | All functions in the app | az functionapp keys list --name your-func --resource-group your-rg |
Master key (_master) | All functions + admin API | az functionapp keys list --name your-func --resource-group your-rg |
Security: Function keys provide basic access control but are shared secrets. For production workloads with user authentication, use Easy Auth or JWT validation instead.
Rotating Keys¶
Regenerate a function key:
az functionapp function keys set \
--name your-func \
--resource-group your-rg \
--function-name my_func \
--key-name default \
--key-value "new-key-value-here"
Easy Auth (Built-In Authentication)¶
Easy Auth adds authentication at the platform level — before your function code executes. It supports multiple identity providers without any changes to your Python code.
Supported Providers¶
| Provider | Typical Scenario |
|---|---|
| Microsoft Entra ID (Azure AD) | Enterprise / internal apps |
| GitHub | Developer tools |
| Consumer apps | |
| Consumer apps | |
| Any OpenID Connect | Custom identity providers |
Enable Easy Auth for Azure AD¶
az webapp auth update \
--resource-group <resource-group> \
--name <function-app-name> \
--enabled true \
--action LoginWithAzureActiveDirectory \
--aad-client-id <client-id> \
--aad-token-issuer-url "https://login.microsoftonline.com/<tenant-id>/v2.0"
Note: Azure Functions uses the same App Service Authentication (Easy Auth) as Web Apps. The
az webapp authcommands apply to function apps as well.
Accessing User Identity in Code¶
When Easy Auth is enabled, the platform injects identity information into request headers:
import json
import azure.functions as func
bp = func.Blueprint()
@bp.route(route="profile", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def profile(req: func.HttpRequest) -> func.HttpResponse:
"""Returns the authenticated user's profile.
Auth is handled by Easy Auth at the platform level."""
user_id = req.headers.get("X-MS-CLIENT-PRINCIPAL-ID", "")
user_name = req.headers.get("X-MS-CLIENT-PRINCIPAL-NAME", "")
identity_provider = req.headers.get("X-MS-CLIENT-PRINCIPAL-IDP", "")
if not user_id:
return func.HttpResponse(
json.dumps({"error": "Not authenticated"}),
mimetype="application/json",
status_code=401
)
return func.HttpResponse(
json.dumps({
"user_id": user_id,
"user_name": user_name,
"identity_provider": identity_provider
}),
mimetype="application/json",
status_code=200
)
Note: With Easy Auth set to
RedirectToLoginPageorReturn401, unauthenticated requests never reach your function code. The auth level can beANONYMOUSbecause Easy Auth handles authentication at the platform layer.
Manual JWT Validation¶
For scenarios where Easy Auth is not suitable (e.g., custom token issuers, API-to-API auth with bearer tokens), validate JWTs manually using the PyJWT library.
Install Dependencies¶
Add to requirements.txt:
JWT Validation Example¶
import azure.functions as func
import json
import jwt
import logging
from functools import wraps
bp = func.Blueprint()
# Configuration — in production, load from environment variables
TENANT_ID = "your-tenant-id"
CLIENT_ID = "your-client-id"
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0"
JWKS_URL = f"https://login.microsoftonline.com/{TENANT_ID}/discovery/v2.0/keys"
# Cache the JWKS client
_jwks_client = jwt.PyJWKClient(JWKS_URL)
def validate_token(req: func.HttpRequest) -> dict | None:
"""Extract and validate the Bearer token from the Authorization header."""
auth_header = req.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return None
token = auth_header[7:] # Strip "Bearer " prefix
try:
signing_key = _jwks_client.get_signing_key_from_jwt(token)
decoded = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=CLIENT_ID,
issuer=AUTHORITY,
)
return decoded
except jwt.InvalidTokenError as e:
logging.warning(f"Token validation failed: {e}")
return None
@bp.route(route="protected/data", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def protected_data(req: func.HttpRequest) -> func.HttpResponse:
"""Protected endpoint that requires a valid JWT."""
claims = validate_token(req)
if not claims:
return func.HttpResponse(
json.dumps({"error": "Unauthorized", "message": "Valid Bearer token required"}),
mimetype="application/json",
status_code=401
)
return func.HttpResponse(
json.dumps({
"message": "Access granted",
"user": claims.get("preferred_username", "unknown"),
"roles": claims.get("roles", [])
}),
mimetype="application/json",
status_code=200
)
Testing with a Token¶
# Obtain a token (example using az cli)
TOKEN=$(az account get-access-token --resource "api://your-client-id" --query accessToken --output tsv)
# Call the protected endpoint
curl --header "Authorization: Bearer $TOKEN" \
https://your-func.azurewebsites.net/api/protected/data
Choosing an Auth Strategy¶
| Scenario | Recommended Approach |
|---|---|
| Public health check | AuthLevel.ANONYMOUS |
| Service-to-service (simple) | AuthLevel.FUNCTION with function keys |
| User-facing web app | Easy Auth with Azure AD |
| API-to-API with Azure AD | Manual JWT validation or Easy Auth |
| Third-party identity provider | Easy Auth (OpenID Connect) or manual JWT |
See Also¶
- HTTP API Patterns
- Platform Security Design — authentication architecture, Easy Auth, key management design
- Security Operations — key rotation, RBAC audit, CORS, TLS enforcement