Implementing OAuth 2.0 with Azure AD (Frontend + Backend) — a detailed step-by-step blog
Tech Enthusiast | 19+ Years in IT | Security, Coding, Trends With over 19 years of experience in the ever-evolving world of Information Technology, I’m passionate about staying ahead of the curve. From mastering secure coding practices to exploring the latest trends in AI, cloud computing, and cybersecurity, my mission is to share valuable insights, practical tips, and the latest industry updates. Whether it's about writing cleaner, more efficient code or enhancing security protocols, I aim to empower developers and IT professionals to excel in their careers while keeping pace with the rapidly changing tech landscape.
Goal: show an end-to-end implementation where a Single Page App (SPA) authenticates a user using Azure AD (OAuth 2.0 Authorization Code + PKCE), receives an access token (JWT), calls a protected backend API, and the backend validates the token before returning data. I’ll also cover machine-to-machine (Client Credentials), how to expose scopes, token validation, and best practices.
register two apps in Azure AD (Frontend SPA + Backend API), expose a scope on the backend, use MSAL.js in the frontend (auth code + PKCE) to get an access token (a JWT), and validate that JWT on the backend using Azure’s JWKS (public keys). Microsoft Learn+2Microsoft Learn+2
Prerequisites
Azure subscription & permissions to create App registrations in Microsoft Entra ID (Azure AD). Microsoft Learn
Local dev: Node 18+, npm/yarn (for SPA + server examples).
Libraries shown:
@azure/msal-browser(frontend),jose(Node backend). (I show code for React/vanilla SPA and Node backend; Python/ .NET examples notes follow.)
Overview (high level)
Register Backend API in Azure AD → Expose an API and add a custom scope (e.g.
api://<api-client-id>/access_as_user). Microsoft LearnRegister Frontend SPA in Azure AD → Redirect URI type SPA (or web) and request the API scope. Use Authorization Code Flow with PKCE (recommended for SPAs). Microsoft Learn
Frontend uses MSAL.js to sign in and acquire an access token (JWT). Microsoft Learn
Frontend sends
Authorization: Bearer <access_token>to backend.Backend validates JWT using Azure AD’s JWKS (from the OpenID Connect discovery endpoint), checks
aud,iss,exp, and requiredscp/roles. Microsoft Learn
Step 1 — Register apps in Azure AD (Portal steps)
A. Create Backend API app
Azure Portal → Microsoft Entra ID → App registrations → New registration. Enter name (e.g.
my-backend-api). Microsoft LearnAfter creation → Expose an API → set the Application ID URI (default looks like
api://<client-id>or a custom URI). Click Add a scope and create a scope likeaccess_as_userwith a display name / admin consent description. (This becomesapi://<client-id>/access_as_user.) Microsoft Learn
B. Create Frontend SPA app
Azure Portal → App registrations → New registration → name
my-spa.Set Redirect URI → Single-page application (SPA) and add URL e.g.
http://localhost:3000. Ensure typespaif available. (This enables the auth code flow for SPAs.) Microsoft Learn+1In the SPA app → API permissions → Add a permission → My APIs → select your Backend API → select the scope
access_as_user. Then Grant admin consent (or user consent depending on your tenant policy).
Note: For machine-to-machine (server → server) create a confidential client app (backend) and use Client Credentials Flow (see Step 7). Microsoft Learn
Step 2 — Understand the endpoints (OIDC discovery & JWKS)
Azure publishes OpenID configuration and JWKS for your tenant:
- Discovery document (v2 example):
https://login.microsoftonline.com/<TENANT_ID>/v2.0/.well-known/openid-configuration
This includesjwks_uriwhich lists public signing keys used to verify JWT signatures. Use this to validate tokens and handle key rollover automatically. Microsoft Learn
Step 3 — Frontend: MSAL.js (Auth code + PKCE) example
Install:
npm install @azure/msal-browser
Minimal auth.js (vanilla or integrate in React):
import { PublicClientApplication } from "@azure/msal-browser";
const msalConfig = {
auth: {
clientId: "<SPA_CLIENT_ID>",
authority: "https://login.microsoftonline.com/<TENANT_ID>",
redirectUri: "http://localhost:3000",
},
cache: {
cacheLocation: "sessionStorage", // or "localStorage" (sessionStorage recommended)
}
};
const msalInstance = new PublicClientApplication(msalConfig);
const loginRequest = {
scopes: ["openid", "profile", "api://<API_CLIENT_ID>/access_as_user"]
};
// Sign-in
async function signIn() {
const loginResp = await msalInstance.loginPopup(loginRequest);
return loginResp;
}
// Acquire token to call API
async function getAccessToken() {
const account = msalInstance.getAllAccounts()[0];
try {
const tokenResp = await msalInstance.acquireTokenSilent({
scopes: ["api://<API_CLIENT_ID>/access_as_user"],
account
});
return tokenResp.accessToken;
} catch (err) {
// fallback to interactive if silent fails
const tokenResp = await msalInstance.acquireTokenPopup({
scopes: ["api://<API_CLIENT_ID>/access_as_user"]
});
return tokenResp.accessToken;
}
}
Then call backend:
const token = await getAccessToken();
fetch('http://localhost:4000/secure-data', {
headers: { Authorization: `Bearer ${token}` }
});
Why this? MSAL implements the Authorization Code + PKCE flow for SPAs (recommended over implicit). PKCE prevents auth code injection and is required for SPA auth code flows. Microsoft Learn
Reference: MSAL.js docs & samples. Microsoft Learn
Step 4 — Backend: validate the incoming JWT (Node.js example using jose)
Install:
npm install express jose
server.js (minimal, production code should add caching, logging, better error messages):
import express from "express";
import { createRemoteJWKSet, jwtVerify } from "jose";
const app = express();
const port = 4000;
const tenantId = "<TENANT_ID>";
const audience = "api://<API_CLIENT_ID>"; // or the client-id GUID
const issuer = `https://login.microsoftonline.com/${tenantId}/v2.0/`;
const jwksUri = `https://login.microsoftonline.com/${tenantId}/discovery/v2.0/keys`;
// create a JWKS remote key store (handles key rollover)
const JWKS = createRemoteJWKSet(new URL(jwksUri));
async function verifyAccessToken(token) {
// will throw on invalid
const { payload } = await jwtVerify(token, JWKS, {
issuer,
audience
});
// payload contains claims like sub, scp, roles, exp
return payload;
}
app.get("/secure-data", async (req, res) => {
try {
const auth = req.headers.authorization || "";
const token = auth.split(" ")[1];
if (!token) return res.status(401).send({ error: "missing token" });
const payload = await verifyAccessToken(token);
// Example: ensure required scope present
const scopes = (payload.scp || "").split(" ");
if (!scopes.includes("access_as_user")) {
return res.status(403).send({ error: "insufficient scopes" });
}
res.send({ message: "Hello from protected API", user: payload.sub });
} catch (err) {
console.error(err);
res.status(401).send({ error: "invalid token" });
}
});
app.listen(port, () => console.log(`API listening ${port}`));
Notes
We use the
jwks_uriso the library fetches the current public keys (handles rollover). Microsoft LearnValidate
aud(audience) andiss(issuer), andexp. Also checkscp(delegated scopes) orroles(app roles) depending on flow. Microsoft Learn
Step 5 — Expose API scopes & grant consent (Azure Portal)
In your Backend app registration → Expose an API → Add scope (e.g.,
access_as_user).In SPA app → API permissions → add the scope from your backend. Grant admin consent if needed. This wires up the permission so your frontend can request the scope and Azure AD will issue an access token targeted to your API. Microsoft Learn
Step 6 — Machine-to-machine: Client Credentials Flow (server → server)
If a service (daemon/app) needs to call the API (no user), use the Client Credentials flow:
Create an app registration for the caller (confidential client) and create a client secret or certificate.
Give that caller Application permission to your API (Expose an API → add application permission OR select
/.defaultscopes).Acquire token (example curl):
curl -X POST https://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token \
-d 'client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&grant_type=client_credentials&scope=api://<API_CLIENT_ID>/.default'
The returned access token is for the app (contains roles claim if app roles granted). Use this token to call the API. Microsoft Learn
Step 7 — Troubleshooting & common pitfalls
Using implicit flow for SPAs: legacy and less secure. Use auth code + PKCE. Microsoft Learn
Wrong audience (
aud): If API validates audience incorrectly (Graph vs your API), token validation fails. Ensureaudmatches your API App ID URI or client-id as configured. Microsoft LearnToken expired: Check
expclaim; refresh tokens are not available to SPAs in some flows; MSAL handles refresh via silent renew. Microsoft LearnKey rollover: Always use
jwks_uri/ automatic key fetching so your server accepts new Microsoft signing keys when rotated. Microsoft Learn
Security best practices
SPAs: use Authorization Code + PKCE (no client secrets in browser). Microsoft Learn
Don't store long-lived secrets in browser. Use sessionStorage or in-memory storage for tokens; MSAL offers caching controls. Microsoft Learn
Validate all tokens on server (signature,
iss,aud,exp, scopes/roles). Treat tokens as bearer credentials. Microsoft LearnUse HTTPS everywhere.
Use short token lifetimes and rotate client secrets / certificates.
For service-to-service, use Client Credentials (confidential client) instead of API keys — prefer OAuth for least privilege and revocation support. Microsoft Learn
Quick reference links (official)
Register an app (quickstart) — Microsoft Entra ID (Azure AD). Microsoft Learn
OAuth 2.0 Auth Code Flow (authorization code with PKCE) — Microsoft identity platform. Microsoft Learn+1
MSAL.js overview & docs. Microsoft Learn
Expose an API & configure scopes. Microsoft Learn
Access token validation guidance. Microsoft Learn
Client Credentials flow (daemon apps). Microsoft Learn




