---
title: "Azure AD + YAML Allowlist — AuthN/AuthZ Split Pattern"
aliases: [azure-ad-yaml-allowlist, msal-yaml-authz, shared-app-reg-authz]
tags: [azure-ad, msal, fastapi, auth, yaml, allowlist, spa, docker, python]
sources:
  - "daily/2026-04-28.md"
created: 2026-04-28
updated: 2026-04-28
---

# Azure AD + YAML Allowlist — AuthN/AuthZ Split Pattern

A pragmatic authorization pattern: use Azure AD (MSAL) solely for authentication (who the user is), and a backend YAML file for authorization (what the user can do). This avoids touching Azure AD security groups when doing so would risk breaking other apps sharing the same app registration.

## Key Points

- **AuthN via Azure AD, AuthZ via local YAML:** Azure AD proves identity; a `config/allowed_users.yaml` maps `email → role` and gates access
- **When to use:** Shared app registration (touching Azure AD groups risks breaking other apps); small team (~10–30 people) where git PR + redeploy is an acceptable audit trail instead of AD group management
- **SPA redirect URI type is required:** Azure AD redirect URI must be registered as type **SPA** (not Web) for MSAL PKCE flow — "Web" type does not return the token directly to the browser
- **Docker volume for config dir:** `./config:/app/config` mount needed in `docker-compose.yml` so the backend reads the YAML at runtime without rebuilding the image
- **Custom 403 code:** Use `code: not_allowlisted` in the 403 response so the frontend interceptor can distinguish "no access" from generic auth errors

## Details

### When This Pattern Applies

The typical scenario: an app is given access to an existing Azure AD app registration that is also used by another production app (e.g., a V1 SSO). Creating or modifying Azure AD security groups on that registration carries risk — a misconfiguration could break SSO for the other app.

For small internal teams (10–30 people), a git-based YAML file provides a sufficient audit trail:
- Access changes go through a PR → review → merge → redeploy cycle
- No Azure portal access required for routine user management
- The YAML is version-controlled and changes are traceable

This pattern is less appropriate for large teams, compliance-heavy environments, or when role changes need to take effect instantly (YAML requires a redeploy or hot-reload).

### Implementation

**`config/allowed_users.yaml`:**
```yaml
allowed_users:
  user@example.com: admin
  another@example.com: viewer
  contractor@agency.com: editor
```

**FastAPI dependency:**
```python
import yaml
from fastapi import Depends, HTTPException
from app.auth import get_current_user  # Azure AD token validation

def load_allowlist() -> dict:
    with open("config/allowed_users.yaml") as f:
        data = yaml.safe_load(f)
    return data.get("allowed_users", {})

async def require_allowlisted(user=Depends(get_current_user)):
    allowlist = load_allowlist()
    email = user.get("preferred_username") or user.get("email")
    if email not in allowlist:
        raise HTTPException(
            status_code=403,
            detail={"code": "not_allowlisted", "message": "Access not granted"}
        )
    return {"user": user, "role": allowlist[email]}
```

**`requirements.txt` addition:**
```
PyYAML>=6.0
```
PyYAML is not included in FastAPI's default dependencies — it must be added explicitly.

### Azure AD SPA Redirect URI Type

When registering the redirect URI in Azure portal (App Registrations → Authentication → Add a platform):

- **SPA (Single-page application):** Token returned directly to the browser via PKCE. Required for MSAL.js PKCE flow.
- **Web:** Token returned via server-side redirect. If the redirect URI is registered as "Web" instead of "SPA", MSAL's PKCE flow receives an authorization code but the token exchange is rejected — the browser gets no token.

Common symptom: MSAL login completes (redirect happens), but `acquireTokenSilent` or `handleRedirectPromise` returns `null` or throws an error about invalid grant.

### Docker Volume for Config

```yaml
# docker-compose.yml
services:
  backend:
    build: ./backend
    volumes:
      - ./config:/app/config   # ← exposes allowed_users.yaml to container
    environment:
      - ...
```

Without this volume, the YAML file is not present inside the container at runtime. The backend will raise `FileNotFoundError` on first access to a protected route. The volume mount means user management changes can be applied without rebuilding the Docker image — just edit the YAML and restart the container (or implement hot-reload).

## Related Concepts

- [[wiki/concepts/msal-vanilla-js-pkce]] — MSAL.js v5 PKCE flow details and Azure portal platform type requirement
- [[wiki/tech-patterns/_index]] — FastAPI + Azure AD tech pattern used across Oliver Agency projects

## Sources

- [[daily/2026-04-28.md]] — oliver-sales-ops-platform SSO: Azure AD authn + YAML allowlist authz implemented to avoid touching shared app registration; SPA redirect URI type discovered; Docker volume for config dir; custom 403 `not_allowlisted` code; PyYAML requirement
