server/routes/newspaperArchive.ts (103 lines of code) (raw):
import { Router } from 'express';
import type { Request, Response } from 'express';
import fetch from 'node-fetch';
import { z } from 'zod';
import { authorizationOrCookieHeader } from '../apiProxy';
import { s3ConfigPromise } from '../awsIntegration';
import { conf } from '../config';
import { log, putMetric } from '../log';
import { withIdentity } from '../middleware/identityMiddleware';
type NewspapersRequestBody = {
expires?: number; // defaults to 24 hours
'query-string'?: string;
};
// { url: "https://<subdomain>.newspapers.com/…?tpa=<token>" }
const NewspapersResponseSchema = z.object({
url: z.string(),
});
const userBenefitsSchema = z.object({
benefits: z.array(z.string()),
});
type NewspaperArchiveConfig = {
authString: string;
};
function base64(input: string) {
return Buffer.from(input).toString('base64');
}
const newspaperArchiveConfigPromise: Promise<
NewspaperArchiveConfig | undefined
> = s3ConfigPromise<NewspaperArchiveConfig>('authString')('newspaper-archive');
const router = Router();
router.use(withIdentity(401, true));
router.get('/auth', async (req: Request, res: Response) => {
try {
const config = await newspaperArchiveConfigPromise;
const authString = config?.authString;
if (authString === undefined) {
log.error(`Missing newspaper archive auth key`);
return res.sendStatus(500);
}
console.log('Checking supporter entitlement');
const hasCorrectEntitlement = await checkSupporterEntitlement(req);
if (!hasCorrectEntitlement) {
// ToDo: show the user an error/info page
console.log('User does not have the newspaper archive entitlement');
return res.redirect('/');
}
console.log('User has the newspaper archive entitlement');
const authHeader = base64(`${authString}`);
const requestBody: NewspapersRequestBody = {};
const response = await fetch(
'https://www.newspapers.com/api/userauth/public/get-tpa-token',
{
headers: {
Authorization: `Basic ${authHeader}`,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(requestBody),
},
);
const responseJson = NewspapersResponseSchema.parse(
await response.json(),
);
const archiveReturnUrlString = req.query['ncom-return-url'];
if (
archiveReturnUrlString &&
typeof archiveReturnUrlString === 'string'
) {
putMetric({
loggingCode: 'REDIRECT_FROM_NEWSPAPERS_COM',
isOK: true,
});
const tpaToken = new URL(responseJson.url).searchParams.get('tpa');
const archiveReturnUrl = new URL(archiveReturnUrlString);
if (archiveReturnUrl.hostname !== 'theguardian.newspapers.com') {
log.error('Invalid ncom return URL hostname');
return res.redirect(responseJson.url);
}
archiveReturnUrl.searchParams.set('tpa', tpaToken ?? '');
return res.redirect(archiveReturnUrl.toString());
}
return res.redirect(responseJson.url);
} catch (e) {
log.error(
`Something went wrong authenticating with newspapers.com. ${e}`,
);
return res.sendStatus(500);
}
});
export { router };
async function checkSupporterEntitlement(req: Request): Promise<boolean> {
const supporterAttributes = await getSupporterStatus(req)
.then((res) => res.json())
.then((json) => userBenefitsSchema.parse(json));
return supporterAttributes.benefits.includes('newspaperArchive');
}
async function getSupporterStatus(req: Request) {
const host = 'user-benefits.' + conf.API_DOMAIN;
return fetch(`https://${host}/benefits/me`, {
method: 'GET',
headers: {
...(await authorizationOrCookieHeader({ req, host })),
},
});
}