CORS-safe authentication flow utilities for popup-based login with postMessage communication, ensuring secure cross-origin authentication flows.
Middleware for ensuring requests include the X-Requested-With header for CORS safety.
/**
* Middleware that ensures X-Requested-With header is present for CORS safety
* @param req - Express request object
* @param res - Express response object
* @param next - Express next function
*/
function ensuresXRequestedWith(
req: express.Request,
res: express.Response,
next: express.NextFunction
): void;Usage Example:
import { ensuresXRequestedWith } from "@backstage/plugin-auth-backend";
import express from "express";
const app = express();
// Apply CORS safety middleware
app.use("/auth", ensuresXRequestedWith);
// Protected authentication endpoints
app.post("/auth/github/handler/frame", (req, res) => {
// This endpoint is now protected against CSRF attacks
});Utility for sending authentication responses to parent windows via postMessage API.
/**
* Posts authentication response to parent window using postMessage API
* @param res - Express response object
* @param appOrigin - Origin URL of the parent application
* @param response - Authentication response data
*/
function postMessageResponse(
res: express.Response,
appOrigin: string,
response: WebMessageResponse
): void;Usage Example:
import { postMessageResponse } from "@backstage/plugin-auth-backend";
// In OAuth callback handler
app.post("/auth/github/handler/frame", async (req, res) => {
try {
// Process OAuth authentication
const { profile, providerInfo } = await processOAuthCallback(req);
// Send success response to parent window
postMessageResponse(res, "https://backstage.example.com", {
type: "authorization_response",
response: {
profile,
providerInfo,
backstageId: profile.email,
},
});
} catch (error) {
// Send error response to parent window
postMessageResponse(res, "https://backstage.example.com", {
type: "authorization_response",
error: {
name: "AuthenticationError",
message: error.message,
},
});
}
});The typical authentication flow uses these helpers in combination:
import {
ensuresXRequestedWith,
postMessageResponse,
OAuthAdapter
} from "@backstage/plugin-auth-backend";
// Authentication flow implementation
const authProvider = {
async start(req: express.Request, res: express.Response) {
// Redirect to OAuth provider
const authUrl = buildAuthorizationUrl(req);
res.redirect(authUrl);
},
async frameHandler(req: express.Request, res: express.Response) {
try {
// Process OAuth callback
const result = await handleOAuthCallback(req);
// Send success response to parent window
postMessageResponse(res, getAppOrigin(req), {
type: "authorization_response",
response: result,
});
} catch (error) {
// Send error response to parent window
postMessageResponse(res, getAppOrigin(req), {
type: "authorization_response",
error: {
name: error.name,
message: error.message,
},
});
}
},
};
// Apply middleware and create routes
const router = express.Router();
router.use(ensuresXRequestedWith);
router.get("/start", authProvider.start);
router.post("/handler/frame", authProvider.frameHandler);/**
* Response type for post-message authentication responses
*/
interface WebMessageResponse {
/** Message type identifier */
type: string;
/** Success response data (optional) */
response?: {
/** User profile information */
profile: ProfileInfo;
/** Provider-specific information */
providerInfo?: any;
/** Backstage identity ID */
backstageId?: string;
/** Identity token */
idToken?: string;
};
/** Error information (optional) */
error?: {
/** Error name */
name: string;
/** Error message */
message: string;
/** Error stack trace (optional) */
stack?: string;
};
/** Additional message data */
[key: string]: any;
}/**
* CORS-safe request validation options
*/
interface CorsValidationOptions {
/** Required X-Requested-With header value */
requiredHeader?: string;
/** Allowed origins for CORS requests */
allowedOrigins?: string[];
/** Custom validation function */
validator?: (req: express.Request) => boolean;
}The ensuresXRequestedWith middleware provides CSRF protection by requiring the X-Requested-With header:
// Frontend authentication request
fetch("/auth/github/handler/frame", {
method: "POST",
headers: {
"X-Requested-With": "XMLHttpRequest", // Required for CORS safety
"Content-Type": "application/json",
},
body: JSON.stringify(authData),
});Always validate the origin when using postMessageResponse:
function getAppOrigin(req: express.Request): string {
const referer = req.get("Referer");
if (!referer) {
throw new Error("Missing referer header");
}
const url = new URL(referer);
const allowedOrigins = ["https://backstage.example.com"];
if (!allowedOrigins.includes(url.origin)) {
throw new Error(`Invalid origin: ${url.origin}`);
}
return url.origin;
}The authentication flow typically uses popup windows for OAuth flows:
// Frontend popup authentication
function authenticateWithPopup(providerId: string): Promise<AuthResult> {
return new Promise((resolve, reject) => {
const popup = window.open(
`/auth/${providerId}/start`,
"auth",
"width=500,height=600,scrollbars=yes"
);
const messageListener = (event: MessageEvent) => {
if (event.origin !== window.location.origin) {
return; // Ignore messages from other origins
}
if (event.data.type === "authorization_response") {
window.removeEventListener("message", messageListener);
popup?.close();
if (event.data.error) {
reject(new Error(event.data.error.message));
} else {
resolve(event.data.response);
}
}
};
window.addEventListener("message", messageListener);
// Handle popup blocked or closed
const checkClosed = setInterval(() => {
if (popup?.closed) {
clearInterval(checkClosed);
window.removeEventListener("message", messageListener);
reject(new Error("Authentication cancelled"));
}
}, 1000);
});
}