MSAL Auth Handler
Status
Initial Rust implementation is complete in light-pingora and light-gateway. It includes config loading (msal-auth.yml), standalone Microsoft Entra ID token validation through security-msal.yml, double-submit cookie CSRF handling, gateway auth-principal propagation, and downstream Authorization injection.
Purpose
The msal-auth module is an alternative to msal-exchange for Microsoft Entra ID single-page application (SPA) architectures where the frontend acts as the primary OAuth client.
In this flow:
- The SPA handles Microsoft authentication, token acquisition, and token refresh directly.
- The SPA submits the Entra ID access token to the gateway's
/auth/ms/loginendpoint. - The gateway validates the Entra ID token with
security-msal.ymland sets theaccessTokenandcsrfcookies using the double-submit cookie pattern. - On subsequent API calls, the gateway validates the Microsoft JWT with expiry enforcement, compares the CSRF request value to the CSRF cookie, sets the gateway auth principal for later handlers, and forwards the token in the
Authorization: Bearerheader.
This eliminates the need for an internal light-oauth token exchange, reducing infrastructure dependencies while maintaining backend API security.
Configuration
handler.yml
Register msal-auth in the handler chain before handlers that need ctx.auth or the downstream Authorization header, such as access-control, router, or proxy handling.
handlers:
- cors
- msal-auth
- router
chains:
bff:
- cors
- msal-auth
- router
paths:
- path: /auth/ms/login
method: POST
exec:
- bff
- path: /auth/ms/login
method: OPTIONS
exec:
- bff
- path: /auth/ms/logout
method: POST
exec:
- bff
- path: /auth/ms/logout
method: OPTIONS
exec:
- bff
defaultHandlers:
- cors
- msal-auth
- router
msal-auth.yml
enabled: ${msal-auth.enabled:true}
loginPath: ${msal-auth.loginPath:/auth/ms/login}
logoutPath: ${msal-auth.logoutPath:/auth/ms/logout}
logoutCsrfEnforced: ${msal-auth.logoutCsrfEnforced:false}
cookieDomain: ${msal-auth.cookieDomain:localhost}
cookiePath: ${msal-auth.cookiePath:/}
cookieSecure: ${msal-auth.cookieSecure:false}
sessionTimeout: ${msal-auth.sessionTimeout:3600}
cookieSameSite: ${msal-auth.cookieSameSite:None}
security-msal.yml
msal-auth requires security-msal.yml when the handler is active and msal-auth.enabled is true. The config is loaded independently from the normal security.yml runtime.
enableVerifyJwt: ${security-msal.enableVerifyJwt:true}
ignoreJwtExpiry: ${security-msal.ignoreJwtExpiry:false}
enableRelaxedKeyValidation: ${security-msal.enableRelaxedKeyValidation:false}
issuer: ${security-msal.issuer:}
audience: ${security-msal.audience:}
jwt:
clockSkewInSeconds: ${security-msal.jwt.clockSkewInSeconds:60}
Handlers
- Login (
/auth/ms/login): Expects an Entra ID token in theAuthorization: Bearerheader. Validates it using thesecurity-msalruntime with expiry enforcement. Generates a secure CSRF token and returns bothaccessTokenandcsrfasSet-Cookieheaders. - Logout (
POST /auth/ms/logout): Validates logout CSRF when configured, clears every cookie the runtime sets (accessTokenandcsrf), and returns204 No Contentwithout a response body or content type. - Session Validation (any path with cookies): Reads the
accessTokencookie. Validates the JWT with expiry enforcement. Checks that the CSRF request value matches the CSRF cookie. If valid, it sets the gateway auth principal and forwards theaccessTokendownstream in theAuthorization: Bearerheader.
Frontend Integration
The Single Page Application (SPA) must coordinate with the gateway for session creation and destruction.
Login Request
When the SPA acquires an access token from Microsoft Entra ID (e.g., using
MSAL.js), it must send that token to the gateway's login endpoint to establish
the secure HTTP-only cookies. Both login and logout use POST; neither needs a
request body. A zero-length body is also accepted when a shared client sets
Content-Type: application/json.
For cross-origin deployments, both examples require preflight: login sends the
non-safelisted Authorization header and CSRF-protected logout sends the
non-safelisted X-CSRF-TOKEN header. Keep the explicit OPTIONS routes and
qualify the exact origin, credentials, method, and requested headers.
async function gatewayLogin(entraIdToken) {
const response = await fetch('/auth/ms/login', {
method: 'POST',
credentials: 'include',
headers: {
'Authorization': `Bearer ${entraIdToken}`
}
});
if (!response.ok) {
throw new Error('Failed to create gateway session');
}
console.log('Gateway session established');
}
Logout Request
When the user logs out, the SPA must call the gateway's logout endpoint to
clear the HTTP-only session cookies. Send credentials and the
X-CSRF-TOKEN header read from the readable csrf cookie; no body is needed.
// Helper to read the csrf cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
async function gatewayLogout() {
const csrfToken = getCookie('csrf');
const response = await fetch('/auth/ms/logout', {
method: 'POST',
credentials: 'include',
headers: {
'X-CSRF-TOKEN': csrfToken
}
});
if (response.status !== 204) {
throw new Error(`Unexpected logout status ${response.status}`);
}
console.log('Gateway session cleared');
}
Login and logout are POST-only. A legacy GET or any other unsupported method
returns 405, ERR10008, and Allow: POST before authentication or cookie
side effects. Keep explicit OPTIONS routes permanently with cors before
msal-auth in the selected chain.
Error Handling
| Code | Meaning |
|---|---|
ERR10008 | Method is not allowed; MSAL login/logout responses advertise Allow: POST. |
ERR10036 | Logout CSRF header is missing when enforcement is enabled. |
ERR11649 | Logout CSRF cookie/header validation failed without exposing either value. |
API Request
For standard API calls to backend services, the browser will automatically include the HTTP-only accessToken cookie. However, any request that modifies state or requires CSRF protection must include the CSRF token. The SPA must read the csrf cookie and append it as the X-CSRF-TOKEN header.
async function callBackendApi(endpoint, data) {
const csrfToken = getCookie('csrf');
const response = await fetch(endpoint, {
method: 'POST', // or PUT, DELETE, etc.
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('API call failed');
}
return response.json();
}
WebSocket Connection
The browser's native WebSocket API does not allow setting custom HTTP headers. To pass the CSRF token during the WebSocket handshake upgrade, the SPA must pass it as a subprotocol string prefixed with csrf.. The gateway will extract and validate it.
function connectWebSocket(path) {
const csrfToken = getCookie('csrf');
// Create a subprotocol string that the gateway recognizes
const csrfProtocol = `csrf.${csrfToken}`;
// Note: Depending on your WebSocket server, you may also need to pass
// the actual subprotocol you intend to use (e.g., 'wamp', 'graphql-ws')
// alongside the csrf protocol.
const ws = new WebSocket(`wss://api.example.com${path}`, [csrfProtocol]);
ws.onopen = () => {
console.log('WebSocket connected securely');
};
ws.onerror = (error) => {
console.error('WebSocket connection failed (possible CSRF or Auth issue)', error);
};
return ws;
}
Double Submit Cookie CSRF
Because an Entra ID token cannot be minted with a custom CSRF claim by this gateway, msal-auth enforces CSRF protections using the double-submit cookie pattern. The SPA reads the generated csrf cookie and submits it back.
The CSRF value is accepted from the following sources, in order of precedence:
X-CSRF-TOKENheader.Sec-WebSocket-Protocolvalue starting withcsrf.(when the request hasSec-WebSocket-KeyandSec-WebSocket-Version). This provides specialized CSRF support for Websocket upgrades since browser WebSockets cannot send custom HTTP headers.- Query parameter
csrf.
The gateway compares the value from one of these sources against the csrf cookie. If they match, the session is validated.
Refresh Flow
Unlike msal-exchange or stateless-auth, msal-auth does not issue or manage refresh tokens. The SPA is responsible for using MSAL.js to silently refresh the Entra ID token and calling /auth/ms/login again to update the session cookies before they expire.
Reload Behavior
The gateway reloads msal-auth when handler.yml, msal-auth.yml, or security-msal.yml changes. Reloading security-msal.yml refreshes both msal-auth and msal-exchange because both handlers validate Microsoft tokens with that security runtime.