@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
196 lines (171 loc) • 6.85 kB
JavaScript
/**
* @fileoverview This module provides middleware functions for authentication and authorization in an Express application.
* It includes functions to check if a user is logged in, handle login and logout routes, and enforce natural person policy.
*
* @module cyLoginAuth
*/
import { getLoginUrl, handleCallback, getLogoutUrl } from '../auth/cyLoginAuth.mjs';
import { logger } from "../utils/govcyLogger.mjs";
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
import { errorResponse } from "../utils/govcyApiResponse.mjs";
import { isApiRequest } from '../utils/govcyApiDetection.mjs';
/**
* Middleware to check if the user is authenticated. If not, redirect to the login page.
*
* @param {object} req The request object
* @param {object} res The response object
* @param {object} next The next middleware function
*/
export function requireAuth(req, res, next) {
if (!req.session.user) {
if (isApiRequest(req)) {
const err = new Error("Unauthorized: user not authenticated");
err.status = 401;
return next(err);
}
// Store the original URL before redirecting to login
req.session.redirectAfterLogin = req.originalUrl;
return res.redirect('/login');
}
next();
}
/* c8 ignore start */
/**
* Middleware to handle the login route. Redirects the user to the login URL.
*
* @param {object} req The request object
* @param {object} res The response object
* @param {object} next The next middleware function
*/
export function handleLoginRoute() {
return async (req, res, next) => {
try {
let loginUrl = await getLoginUrl(req);
res.redirect(loginUrl);
} catch (error) {
next(error); // Pass any errors to Express error handler
}
};
}
/**
* Middleware to handle the sign-in callback from the authentication provider.
*
* @param {object} req The request object
* @param {object} res The response object
* @param {object} next The next middleware function
*/
export function handleSigninOidc() {
return async (req, res, next) => {
try {
const { tokens, claims, userInfo } = await handleCallback(req);
// Store user information in session
req.session.user = {
...userInfo,
id_token: tokens.id_token,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token || null,
};
// Redirect to the stored URL after login or fallback to '/'
const redirectUrl = req.session.redirectAfterLogin || '/';
// Clean up session for redirect after login
delete req.session.redirectAfterLogin;
// Redirect to the stored URL
res.redirect(redirectUrl);
} catch (error) {
logger.debug('Token exchange failed:', error, req);
res.status(500).send('Authentication failed');
}
}
}
/**
* Middleware to handle the logout route. Destroys the session and redirects the user to the logout URL.
*
* @param {object} req The request object
* @param {object} res The response object
* @param {object} next The next middleware function
*/
export function handleLogout() {
return (req, res, next) => {
if (!req.session.user) {
return res.redirect('/'); // Redirect if not logged in
}
const id_token_hint = req.session.user.id_token; // Retrieve ID token
req.session.destroy(() => {
const logoutUrl = getLogoutUrl(id_token_hint);
res.redirect(logoutUrl);
});
};
}
/* c8 ignore end */
/************************************************************************/
/**
* Middleware to enforce natural person policy. If the user is not a verified natural person, return a false.
*
* @param {object} req The request object
*/
export function naturalPersonPolicy(req) {
// https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/42/For-Cyprus-Natural-or-Legal-person
const { profile_type, unique_identifier } = req.session.user || {};
// Allow only natural persons with approved profiles
if (profile_type === 'Individual' && unique_identifier) {
// Validate Cypriot Citizen (starts with "00" and is 10 characters long)
if (unique_identifier.startsWith('00') && unique_identifier.length === 10) {
return true;
}
// Validate Foreigner with ARN (starts with "05" and is 10 characters long)
if (unique_identifier.startsWith('05') && unique_identifier.length === 10) {
return true;
}
}
// Deny access if validation fails
return false;
}
/** * Middleware to enforce legal person policy. If the user is not a verified legal person, return false.
*
* @param {object} req The request object
*/
export function legalPersonPolicy(req) {
// https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/42/For-Cyprus-Natural-or-Legal-person
const { profile_type, legal_unique_identifier } = req.session.user || {};
// Allow only legal persons with approved profiles
if (profile_type === 'Organisation' && legal_unique_identifier) {
return true;
}
// Deny access if validation fails
return false;
}
const policyRegistry = {
naturalPerson: naturalPersonPolicy,
legalPerson: legalPersonPolicy,
};
export function cyLoginPolicy(req, res, next) {
// Check what is allowed in the service configuration
const allowed = req?.serviceData?.site?.cyLoginPolicies || ["naturalPerson"];
// Check each policy in the allowed list
for (const name of allowed) {
const policy = policyRegistry[name];
// Skip if the policy is not registered
if (!policy) {
console.warn(`🚨 Unknown policy: ${name}`);
continue
};
// 🚨 Strict mode: let errors throw naturally if data is malformed
const passed = policy(req);
if (passed) return next();
}
return handleMiddlewareError(
"🚨 Access Denied: none of the allowed CY Login policies matched.",
403,
next
);
}
/************************************************************************/
export function getUniqueIdentifier(req) {
// https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/82/Available-Scopes-and-Claims
const user = req?.session?.user || {};
const id =
user.unique_identifier ??
user.legal_unique_identifier ??
"";
return String(id);
}