UNPKG

6.63 kBJavaScriptView Raw
1'use strict';
2
3const s3oMiddlewareUtils = require('@financial-times/s3o-middleware-utils');
4const got = require('got');
5const querystring = require('querystring');
6
7const {
8 USERNAME: S3O_USERNAME,
9 TOKEN: S3O_TOKEN,
10 DEFAULT_EXPIRY: S3O_DEFAULT_EXPIRY
11} = s3oMiddlewareUtils.cookies;
12
13const publicKeyCache = new Map();
14
15const getHost = (event) => {
16 const forwardingHost = event.headers['X-FT-Forwarding-Host'];
17 if (forwardingHost) {
18 return forwardingHost;
19 }
20 // respect the host header Fastly sets if override host is chosen in the UI
21 const fastlyOriginalHost = event.headers['Fastly-Orig-Host'];
22 if (fastlyOriginalHost) {
23 return fastlyOriginalHost;
24 }
25 return event.headers.Host;
26}
27
28const getPath = (event, host) => {
29 const forwardingPath = event.headers['X-FT-Forwarding-Path'];
30 if (forwardingPath) {
31 return forwardingPath;
32 }
33 if (host.includes('amazonaws.com')) {
34 return `/${event.requestContext.stage}${event.path}`;
35 }
36 return event.path;
37}
38
39const getRedirectUrl = (event, opts) => {
40 const query = Object.assign({}, event.queryStringParameters);
41 // remove S3O related parameters
42 delete query.username;
43 delete query.token;
44 const queryString = querystring.stringify(query);
45 const suffix = queryString ? `?${queryString}` : '';
46 const host = opts.getHost(event);
47 const path = opts.getPath(event, host);
48
49 return `${opts.protocol}://${host}${path}${suffix}`;
50};
51
52const getFromCookie = (cookie, key) => {
53 const match = cookie.match(new RegExp(key + '=([^;]*)'));
54 if (Array.isArray(match) && match.length > 0) {
55 return match[1];
56 }
57 return '';
58};
59
60const getAuthFromCookie = (cookie) => ({
61 username: getFromCookie(cookie, S3O_USERNAME),
62 token: getFromCookie(cookie, S3O_TOKEN),
63});
64
65const authenticate = async(s3oClientName, cookie) => {
66 const publicKey = await s3oMiddlewareUtils.publickey.get({ cache: publicKeyCache });
67 const auth = getAuthFromCookie(cookie);
68 return s3oMiddlewareUtils
69 // s3oMiddlewareUtils.authenticate expects a synchronous public key getter
70 .authenticate(() => publicKey)
71 .authenticateToken(auth.username, s3oClientName, auth.token);
72};
73
74const requireFunction = (func, message) => {
75 if (typeof func !== 'function') {
76 throw new Error(message);
77 }
78}
79
80const getCookieOptions = (opts) => `; Max-Age=${opts.cookies.maxAge}` +
81 `${opts.cookies.secure ? '; Secure' : ''}` +
82 `${opts.cookies.httpOnly ? '; HttpOnly' : ''}`
83
84const badRequest = (statusCode, error, headers = {}) => ({
85 statusCode,
86 body: JSON.stringify({ error }),
87 headers: Object.assign({
88 'Content-Type': 'application/json',
89 }, headers)
90})
91
92const internalServerError = (error, message) => ({
93 statusCode: 500,
94 body: JSON.stringify({message, error}),
95 headers: {
96 'Content-Type': 'application/json',
97 }
98})
99
100const redirectResponse = (location, headers = {}) => ({
101 statusCode: 302,
102 headers: Object.assign({
103 location,
104 'Cache-Control': 'no-cache, private, max-age=0',
105 }, headers)
106})
107
108// trick to vary case to include two Set-Cookie headers
109const setCookies2 = (cookieValues) => ({
110 'Set-Cookie': cookieValues[0],
111 'set-cookie': cookieValues[1]
112})
113
114// Response from s3o, redirect the user to the redirect URL, setting s3o cookies
115const s3oResponseRedirect = (opts, redirectUrl, query) => {
116 const cookieOptions = getCookieOptions(opts);
117
118 return redirectResponse(redirectUrl, setCookies2([
119 `${S3O_USERNAME}=${query.username}; Path=/${cookieOptions}`,
120 `${S3O_TOKEN}=${query.token}; Path=/${cookieOptions}`,
121 ]));
122}
123
124// Redirect the user to s3o
125const s3oAuthenticateRedirect = (opts, queryParameters, headers) =>
126 redirectResponse(`https://s3o.ft.com/v3/authenticate?${queryParameters}`, headers)
127
128const getResponse = (cookies, isSignedIn = false) => {
129 const {username} = getAuthFromCookie(cookies)
130 return {
131 username: username === '' ? undefined : username,
132 isSignedIn,
133 };
134};
135
136module.exports = async(event, callback, userOpts = {}) => {
137 const opts = Object.assign({
138 redirect: true,
139 protocol: 'https',
140 }, userOpts);
141 opts.getHost = userOpts.getHost || getHost;
142 opts.getPath = userOpts.getPath || getPath;
143 opts.cookies = Object.assign({
144 httpOnly: true,
145 secure: opts.protocol === 'https',
146 maxAge: S3O_DEFAULT_EXPIRY,
147 }, userOpts.cookies || {})
148
149 let s3oClientName;
150
151 if (typeof opts.getHost === 'string') {
152 const host = opts.getHost;
153 opts.getHost = () => host;
154 }
155 requireFunction(opts.getHost,
156 'The option \'getHost\' must be a function which returns a host string or a constant string. This is used in a redirectURL');
157 if (typeof opts.getPath === 'string') {
158 const path = opts.getPath;
159 opts.getPath = () => path;
160 }
161 requireFunction(opts.getPath,
162 'The option \'getPath\' must be a function which returns a path segment or a constant string, to be used in a redirectURL');
163
164 opts.getS3oClientName = userOpts.getS3oClientName || opts.getHost;
165 if (typeof opts.getS3oClientName === 'string') {
166 s3oClientName = opts.getS3oClientName;
167 opts.getS3oClientName = () => s3oClientName;
168 } else if (typeof opts.getS3oClientName === 'function') {
169 s3oClientName = opts.getS3oClientName(event);
170 }
171 requireFunction(opts.getS3oClientName,
172 'The option \'getS3oClientName\' must be a function which returns an ID or a constant string ID. Only requests with a matching ID are authorised by the returned s3o_token from S3O');
173
174 if (!s3oClientName) {
175 throw new Error(`The option \'getS3oClientName\' returned \'${s3oClientName}\'. This function must return a non-empty value`);
176 }
177
178 const query = event.queryStringParameters || {};
179 const cookie = event.headers && event.headers.Cookie ? event.headers.Cookie : '';
180
181 if (opts.redirect) {
182 if (query.username && query.token) {
183 callback(null, s3oResponseRedirect(opts, getRedirectUrl(event, opts), query))
184
185 return getResponse(cookie, false);
186 }
187 if (!(cookie.includes(S3O_USERNAME) && cookie.includes(S3O_TOKEN))) {
188 const queryString = querystring.stringify({
189 host: s3oClientName,
190 });
191 // redirect needs to be unencoded in the query stringfor s3o to work correctly
192 callback(null, s3oAuthenticateRedirect(opts, `${queryString}&redirect=${getRedirectUrl(event, opts)}`));
193
194 return getResponse(cookie, false);
195 }
196 }
197
198 try {
199 const isSignedIn = await authenticate(s3oClientName, cookie);
200
201 if (!isSignedIn) {
202 callback(null, badRequest(401, 'Not authenticated', setCookies2([
203 `${S3O_USERNAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
204 `${S3O_TOKEN}=; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
205 ])));
206 }
207 return getResponse(cookie, isSignedIn);
208 } catch (error) {
209 callback(null, internalServerError(error, 'Failed to retrieve S3O public key'));
210 return getResponse(cookie, false);
211 }
212};