Authentication Flow - srynyvas/Documentation GitHub Wiki

Azure Entra ID Authentication Guide

Securing a VS Code Extension → FastAPI API with Scopes & Roles


📑 Table of Contents

  1. Architecture Overview

  2. Prerequisites

  3. 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

  4. Security & Production Check‑list

  5. Troubleshooting


Architecture Overview

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 &lt;token&gt; --> 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.


Prerequisites

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.

Document version 1.0 • Generated 2025‑05‑26 • Author: Platform Operations

# Azure Entra ID Authentication Guide

Securing a VS Code Extension → FastAPI API with Scopes & Roles


📑 Table of Contents

  1. [Architecture Overview](#architecture-overview)
  2. [Prerequisites](#prerequisites)
  3. [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)
  4. [Security & Production Check‑list](#security--production-check‑list)
  5. [Troubleshooting](#troubleshooting)

Architecture Overview

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 &lt;token&gt; --> C[FastAPI<br/>Backend]
    C -- 4. Fetch JWKS --> D[Azure Entra JWKS]
    C -- 5. JSON Response --> A
Loading

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.


Prerequisites

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.

Step‑by‑Step Implementation

3.1 Create App Registrations

  1. Backend API Azure Portal → Azure AD → App registrations → New registration.

    • Type: Web/API
    • Name: My‑API‑App
    • Application ID URI: api://<API‑APP‑ID>
  2. VS Code Extension

    • Type: Public client / native
    • Name: My‑Extension‑App
    • Redirect URI: http://localhost or vscode://<publisher>.<extension‑id>
    • Enable Allow public client flows.

3.2 Expose API & Custom Scopes

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.

3.3 Define App Roles

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.

3.4 Grant Permissions & Admin Consent

VSIX App → API permissions → + Add → My‑API‑App → tick read_dataAdd permissionGrant admin consent.

3.5 VS Code Extension – Acquire Tokens

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.

3.6 FastAPI – Token Validation

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")

3.7 Enforce Scopes & Roles

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"}

3.8 Test the End‑to‑End Flow

🔍 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”.

Security & Production Check‑list

  • 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.

Troubleshooting

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

⚠️ **GitHub.com Fallback** ⚠️