Authentication Flow - srynyvas/Documentation GitHub Wiki
-
Step‑by‑Step Implementation
3.1 Create App Registrations
3.2 Expose API & Custom Scopes
3.3 Define App Roles
3.4 Grant Permissions & Consent
3.5 VS Code Extension – Acquire Tokens
3.6 FastAPI – Token Validation
3.7 Enforce Scopes & Roles
3.8 Test the End‑to‑End Flow
flowchart TD
A[VS Code<br/>Extension] -- 1. Login (PKCE) --> B[Azure Entra ID]
B -- 2. JWT Access Token --> A
A -- 3. API Call<br/>Bearer <token> --> C[FastAPI<br/>Backend]
C -- 4. Fetch JWKS --> D[Azure Entra JWKS]
C -- 5. JSON Response --> A
Key Points
• Public client (extension) never holds a secret.
• Backend validatesaud
,iss
, signature, scopes, and roles.
• JWKS is cached to minimise calls to Entra ID.
Requirement | Details |
---|---|
Azure tenant | Global admin rights for one‑time consent grants. |
Azure CLI or Azure Portal access | For app registration & role assignment. |
Node ≥ 18 | VSIX build environment, MSAL Node. |
Python ≥ 3.11 | FastAPI backend. |
Packages | @azure/msal-node, fastapi, python‑jose[cryptography], uvicorn, requests. |
# Azure Entra ID Authentication GuideDocument version 1.0 • Generated 2025‑05‑26 • Author: Platform Operations
- [Architecture Overview](#architecture-overview)
- [Prerequisites](#prerequisites)
- [Step‑by‑Step Implementation](#step-by-step-implementation) 3.1 [Create App Registrations](#step‑31-create-app-registrations) 3.2 [Expose API & Custom Scopes](#step‑32-expose-api--custom-scopes) 3.3 [Define App Roles](#step‑33-define-app-roles) 3.4 [Grant Permissions & Consent](#step‑34-grant-permissions--admin-consent) 3.5 [VS Code Extension – Acquire Tokens](#step‑35-vs-code-extension--acquire-tokens) 3.6 [FastAPI – Token Validation](#step‑36-fastapi--token-validation) 3.7 [Enforce Scopes & Roles](#step‑37-enforce-scopes--roles) 3.8 [Test the End‑to‑End Flow](#step‑38-test-the-endtoend-flow)
- [Security & Production Check‑list](#security--production-check‑list)
- [Troubleshooting](#troubleshooting)
flowchart TD
A[VS Code<br/>Extension] -- 1. Login (PKCE) --> B[Azure Entra ID]
B -- 2. JWT Access Token --> A
A -- 3. API Call<br/>Bearer <token> --> C[FastAPI<br/>Backend]
C -- 4. Fetch JWKS --> D[Azure Entra JWKS]
C -- 5. JSON Response --> A
Key Points • Public client (extension) never holds a secret. • Backend validates
aud
,iss
, signature, scopes, and roles. • JWKS is cached to minimise calls to Entra ID.
Requirement | Details |
---|---|
Azure tenant | Global admin rights for one‑time consent grants. |
Azure CLI or Azure Portal access | For app registration & role assignment. |
Node ≥ 18 | VSIX build environment, MSAL Node. |
Python ≥ 3.11 | FastAPI backend. |
Packages |
@azure/msal-node , fastapi , python‑jose[cryptography] , uvicorn , requests . |
-
Backend API Azure Portal → Azure AD → App registrations → New registration.
- Type: Web/API
- Name: My‑API‑App
- Application ID URI:
api://<API‑APP‑ID>
-
VS Code Extension
- Type: Public client / native
- Name: My‑Extension‑App
- Redirect URI:
http://localhost
orvscode://<publisher>.<extension‑id>
- Enable Allow public client flows.
Backend App → Expose an API → + Add Scope
Field | Example |
---|---|
Scope name | read_data |
Display name | Read project data |
Who can consent | Admins + Users |
Admin consent description | Allow the extension to read data |
You will obtain the scope string:
api://<API‑APP‑ID>/read_data
.
Backend App → App roles → + Create app role
Property | Value |
---|---|
Display name | Admin |
Allowed member types | Users/Groups |
Value (claim) | Admin |
Description | Full administrative access |
Assign users/groups: Enterprise applications → My‑API‑App → Users & groups → Add assignment.
VSIX App → API permissions → + Add → My‑API‑App → tick read_data → Add permission → Grant admin consent.
import { PublicClientApplication } from "@azure/msal-node";
const pca = new PublicClientApplication({
auth: {
clientId: "<EXTENSION-APP-ID>",
authority: "https://login.microsoftonline.com/<TENANT-ID>"
}
});
export async function callApi(url: string) {
const { accessToken } = await pca.acquireTokenInteractive({
scopes: ["api://<API‑APP‑ID>/read_data"]
});
return fetch(url, {
headers: { Authorization: `Bearer ${accessToken}` }
});
}
Token caching: MSAL‑Node persists tokens in an in‑memory cache by default; use CachePlugin
for secure storage.
from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import HTTPBearer
from jose import jwt
import requests
TENANT = "<TENANT-ID>"
AUDIENCE = "<API-APP-ID>"
ISSUER = f"https://login.microsoftonline.com/{TENANT}/v2.0"
JWKS_URL = f"{ISSUER}/discovery/v2.0/keys"
JWKS = {k["kid"]: k for k in requests.get(JWKS_URL, timeout=5).json()["keys"]}
auth_scheme = HTTPBearer()
def verify_token(token: str):
header = jwt.get_unverified_header(token)
key = JWKS.get(header["kid"])
if not key:
raise HTTPException(401, "Invalid signing key")
return jwt.decode(
token,
jwt.algorithms.RSAAlgorithm.from_jwk(key),
audience=AUDIENCE,
issuer=ISSUER,
)
async def current_user(credentials=Depends(auth_scheme)):
try:
return verify_token(credentials.credentials)
except Exception:
raise HTTPException(401, "Token validation failed")
REQUIRED_SCOPE = "read_data"
ALLOWED_ROLES = {"Admin", "DevOps"}
def authorize(payload: dict):
# Scope
scopes = payload.get("scp", "").split()
if REQUIRED_SCOPE not in scopes:
raise HTTPException(403, "Missing scope read_data")
# Role
roles = set(payload.get("roles", []))
if roles.isdisjoint(ALLOWED_ROLES):
raise HTTPException(403, "Missing role")
return payload
@app.get("/secure")
async def secure_endpoint(user=Depends(current_user)):
authorize(user)
return {"user": user["preferred_username"], "status": "ok"}
🔍 Test | Expected result |
---|---|
Extension first run | Browser pops up Entra sign‑in page (PKCE). |
Access /secure with valid user, right role |
HTTP 200 JSON payload. |
Access /secure with wrong role |
HTTP 403 “Missing role”. |
Tamper with token “aud” | HTTP 401 “Token validation failed”. |
-
HTTPS everywhere (including local dev via
https://localhost
). -
Cache JWKS for 24 h; refresh on
kid
miss. - Use Azure Managed Identities for server‑to‑server calls (Client Credentials flow).
- Rotate app secrets / certificates regularly (although public client has none).
- Monitor sign‑ins & audit logs in Entra ID.
- Add WAF & rate‑limiting in front of FastAPI when Internet exposed.
- Run security linter (bandit, semgrep) on extension & API code.
Symptom | Fix |
---|---|
AADSTS65001: The user or administrator has not consented. |
Grant (or re‑grant) admin consent for the scope. |
401 Invalid signing key |
Your JWKS cache is stale; clear & refetch. |
403 Missing scope |
Extension didn’t request the scope → check scopes array in MSAL call. |
Token lacks roles claim |
User/group not assigned to the app role → verify assignment. |
Document version 1.0 • Generated 2025‑05‑26 • Author: Platform Operations