Skip to content

Latest commit

 

History

History
216 lines (161 loc) · 8.01 KB

File metadata and controls

216 lines (161 loc) · 8.01 KB

Entra Express Auth Plugin (entra-express-auth)

Overview

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

Flow Overview

  1. Frontend redirects browser to GET /login (no XHR).
  2. Plugin builds MSAL auth URL and redirects to Microsoft login.
  3. User signs in; MSAL calls backend GET /auth/redirect with auth code.
  4. Backend exchanges code for tokens via MSAL.
  5. Plugin merges claims, signs RS256 JWT, stores it with refresh token and expiry.
  6. Backend sets tiny session_id cookie and redirects to frontend.
  7. Frontend calls GET /auth/me to retrieve authenticated context (no JWT in browser).
  8. If JWT expired, backend refreshes via MSAL refresh token and rotates JWT.
  9. Protected routes use verifyJwtToken(publicKey) to validate session + JWT.

Installation

npm install https://github.com/sayanspeaks/entra-express-auth.git

Dependencies and purpose

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 reading session_id cookies)
  • dotenv (the plugin does not call dotenv.config() itself)

Setup

1. Generate JWT Keys

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 .env with JWT_PRIVATE_KEY (escaped newlines)
  • Print the JWT_PUBLIC_KEY PEM to copy into .env

2. Configure Environment Variables

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 \n with real newlines: process.env.JWT_PUBLIC_KEY?.replace(/\\n/g, '\n')

3. Use in Your Express App

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'));

API

  • msalAuthPlugin():

    • GET /login – Initiates Microsoft login
    • GET /auth/redirect – OAuth callback
    • GET /auth/me – Returns current user (refreshes if needed)
    • GET /logout – Clears server-side session and cookie
  • verifyJwtToken(publicKey):

    • Express middleware; validates session by session_id cookie, verifies JWT with RS256 public key, sets req.user.
  • setupJwtKeys(options?):

    • Generates RSA keypair if JWT_PRIVATE_KEY missing in .env.
    • Options: root (project root override).

How It Works

Token Generation (generateCustomToken)

  • Builds JWT payload with standard claims: iss, sub, iat, exp.
  • Signs with RS256 using private key; expiry default 1h.

Session-Based Token Handling

  • JWT stored server-side (Map/Redis), not in cookie.
  • Client receives only session_id cookie (HTTP-only, secure, SameSite=None).
  • /auth/me reads session_id, verifies JWT; refreshes via MSAL if expired.

Token Verification (verifyJwtToken)

  • Reads session_id from req.cookies.
  • Fetches JWT from session store; verifies with RS256 public key.
  • Attaches decoded claims to req.user; continues or signals refresh.

Logout

  • Reads cookie session_id, deletes entry from session store, clears cookie, returns success JSON.

Session Store Details

Local development example:

global.sessionStore = new Map();
global.sessionStore.set(sessionId, {
  token,
  refreshToken,
  expiresAt: Date.now() + 60 * 60 * 1000,
});
  • token: Active short-lived JWT
  • refreshToken: MSAL refresh token
  • expiresAt: JWT expiry timestamp

For production, prefer Redis to persist sessions and support scaling.

Azure Entra ID Setup (Web App)

Use the Confidential Client flow (Web application):

  1. Azure Portal → Entra ID → App registrations → Your App → Authentication.
  2. Add platform → Web; set redirect URI to your backend MSAL_REDIRECT_URI.
  3. Ensure “Allow public client flows” is OFF; remove SPA entries if present.
  4. Certificates & secrets → New client secret → use the secret value in .env.

Precautions & Best Practices

  • Cookie size: keep well under 4096 bytes; store JWT server-side.
  • Security: httpOnly: true, sameSite: "None", secure: true in production.
  • CORS: enable credentials and configure origins appropriately.
  • HTTPS: required for secure cookies.
  • Scopes: include offline_access to receive refresh tokens.
  • Token rotation: update stored refresh token if MSAL provides a new one.

Safety & Security

  • No JWTs in the browser: Only a lightweight session_id cookie 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, and SameSite=None to reduce XSS and CSRF risk (requires HTTPS in production).
  • Config-driven keys: Keys are loaded from environment variables; helper setupJwtKeys can generate keys locally but does not transmit them.
  • Dependency scope: Uses battle-tested libraries (@azure/msal-node, jsonwebtoken, express); plugin avoids auto-calling dotenv.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.

Example Directory Structure

/your-project
├── .env
├── app.js
└── node_modules/entra-express-auth

Next Steps

  • Implement change-user endpoint (if applicable).
  • Switch session store to Redis for production.