The Entra Express Auth Plugin provides a reusable backend authentication layer that integrates Microsoft Authentication Library (MSAL) with Azure Entra ID. It performs server-side authentication using Client ID, Tenant ID, and Client Secret and generates RS256-signed JWTs.
- Backend-centric OAuth flow using
ConfidentialClientApplication - Short-lived JWT stored server-side (session store), cookie holds only
session_id - Automatic refresh via MSAL refresh tokens
- Asymmetric JWT signing (RS256) for secure multi-service verification
- Frontend redirects browser to
GET /login(no XHR). - Plugin builds MSAL auth URL and redirects to Microsoft login.
- User signs in; MSAL calls backend
GET /auth/redirectwith auth code. - Backend exchanges code for tokens via MSAL.
- Plugin merges claims, signs RS256 JWT, stores it with refresh token and expiry.
- Backend sets tiny
session_idcookie and redirects to frontend. - Frontend calls
GET /auth/meto retrieve authenticated context (no JWT in browser). - If JWT expired, backend refreshes via MSAL refresh token and rotates JWT.
- Protected routes use
verifyJwtToken(publicKey)to validate session + JWT.
npm install https://github.com/sayanspeaks/entra-express-auth.git
| Module | Purpose | Required | Notes |
|---|---|---|---|
@azure/msal-node |
Acquire/refresh tokens from Azure Entra ID (Confidential Client flow) | Yes | Core auth client |
express |
Router/middleware host for auth routes | Yes | Attach msalAuthPlugin() to your app |
jsonwebtoken |
Sign and verify RS256 JWTs | Yes | Uses your private/public keys |
uuid |
Generate session_id values stored server-side |
Yes | Avoids large cookies |
cookie-parser |
Parse session_id cookies in the consuming app |
Peer | Install in your app: npm install cookie-parser |
dotenv |
Load environment variables in the consuming app | Peer | The plugin does not call dotenv.config() |
Peer dependencies expected in the consuming app:
cookie-parser(for readingsession_idcookies)dotenv(the plugin does not calldotenv.config()itself)
Use the helper once to generate RSA keypair and write JWT_PRIVATE_KEY into .env:
const { setupJwtKeys } = require('entra-express-auth');
setupJwtKeys();
Or via CLI:
node -e "require('entra-express-auth').setupJwtKeys()"
This will:
- Create/update
.envwithJWT_PRIVATE_KEY(escaped newlines) - Print the
JWT_PUBLIC_KEYPEM to copy into.env
Create a .env in your backend root:
# Azure Entra ID
MSAL_CLIENT_ID=<your-msal-client-id>
MSAL_TENANT_ID=<your-tenant-id>
MSAL_CLIENT_SECRET=<your-msal-client-secret>
MSAL_REDIRECT_URI=http://localhost:3000/auth/redirect
# URLs
FRONTEND_URL=http://localhost:4200
# JWT Key (generated by setupJwtKeys)
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
PORT=3000
Notes:
- RS256 requires both private and public keys.
- If generating manually:
openssl genrsa -out private.pem 2048 && openssl rsa -in private.pem -pubout -out public.pem - When reading from
.env, replace\nwith real newlines:process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, '\n')
require('dotenv').config();
const express = require('express');
const cookieParser = require('cookie-parser');
const { msalAuthPlugin, verifyJwtToken } = require('entra-express-auth');
const app = express();
app.use(cookieParser());
// Configure the plugin
global.MSAL_PLUGIN_CONFIG = {
clientId: process.env.MSAL_CLIENT_ID,
tenantId: process.env.MSAL_TENANT_ID,
clientSecret: process.env.MSAL_CLIENT_SECRET,
redirectUri: process.env.MSAL_REDIRECT_URI,
authServiceUrl: process.env.AUTH_SERVICE_URL,
frontendUrl: process.env.FRONTEND_URL,
jwtPublicKey: process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, '\n'),
};
// Mount auth routes
app.use('/', msalAuthPlugin());
// Example protected route
app.get('/api/protected', verifyJwtToken(process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, '\n')), (req, res) => {
res.json({ user: req.user });
});
app.listen(process.env.PORT || 3000, () => console.log('Server running'));
-
msalAuthPlugin():GET /login– Initiates Microsoft loginGET /auth/redirect– OAuth callbackGET /auth/me– Returns current user (refreshes if needed)GET /logout– Clears server-side session and cookie
-
verifyJwtToken(publicKey):- Express middleware; validates session by
session_idcookie, verifies JWT with RS256 public key, setsreq.user.
- Express middleware; validates session by
-
setupJwtKeys(options?):- Generates RSA keypair if
JWT_PRIVATE_KEYmissing in.env. - Options:
root(project root override).
- Generates RSA keypair if
- Builds JWT payload with standard claims:
iss,sub,iat,exp. - Signs with RS256 using private key; expiry default
1h.
- JWT stored server-side (Map/Redis), not in cookie.
- Client receives only
session_idcookie (HTTP-only, secure, SameSite=None). /auth/mereadssession_id, verifies JWT; refreshes via MSAL if expired.
- Reads
session_idfromreq.cookies. - Fetches JWT from session store; verifies with RS256 public key.
- Attaches decoded claims to
req.user; continues or signals refresh.
- Reads cookie
session_id, deletes entry from session store, clears cookie, returns success JSON.
Local development example:
global.sessionStore = new Map();
global.sessionStore.set(sessionId, {
token,
refreshToken,
expiresAt: Date.now() + 60 * 60 * 1000,
});
token: Active short-lived JWTrefreshToken: MSAL refresh tokenexpiresAt: JWT expiry timestamp
For production, prefer Redis to persist sessions and support scaling.
Use the Confidential Client flow (Web application):
- Azure Portal → Entra ID → App registrations → Your App → Authentication.
- Add platform → Web; set redirect URI to your backend
MSAL_REDIRECT_URI. - Ensure “Allow public client flows” is OFF; remove SPA entries if present.
- Certificates & secrets → New client secret → use the secret value in
.env.
- Cookie size: keep well under 4096 bytes; store JWT server-side.
- Security:
httpOnly: true,sameSite: "None",secure: truein production. - CORS: enable credentials and configure origins appropriately.
- HTTPS: required for
securecookies. - Scopes: include
offline_accessto receive refresh tokens. - Token rotation: update stored refresh token if MSAL provides a new one.
- No JWTs in the browser: Only a lightweight
session_idcookie is sent; JWTs and refresh tokens stay server-side. - Asymmetric signing (RS256): Private key stays on the auth service; public key can be distributed to other services for verification.
- Refresh tokens never exposed: Stored server-side and used only by the backend to rotate access tokens/JWTs.
- HTTP-only, secure cookies: Designed to be
httpOnly,secure, andSameSite=Noneto reduce XSS and CSRF risk (requires HTTPS in production). - Config-driven keys: Keys are loaded from environment variables; helper
setupJwtKeyscan generate keys locally but does not transmit them. - Dependency scope: Uses battle-tested libraries (
@azure/msal-node,jsonwebtoken,express); plugin avoids auto-callingdotenv.config()so consumers control secrets loading. - Auditable surfaces: All auth logic is centralized in the plugin router and middleware, making it easier to audit changes and enforce org standards.
/your-project
├── .env
├── app.js
└── node_modules/entra-express-auth
- Implement change-user endpoint (if applicable).
- Switch session store to Redis for production.