1 | 'use strict';
|
2 |
|
3 | const s3oMiddlewareUtils = require('@financial-times/s3o-middleware-utils');
|
4 | const got = require('got');
|
5 | const querystring = require('querystring');
|
6 |
|
7 | const {
|
8 | USERNAME: S3O_USERNAME,
|
9 | TOKEN: S3O_TOKEN,
|
10 | DEFAULT_EXPIRY: S3O_DEFAULT_EXPIRY
|
11 | } = s3oMiddlewareUtils.cookies;
|
12 |
|
13 | const publicKeyCache = new Map();
|
14 |
|
15 | const getHost = (event) => {
|
16 | const forwardingHost = event.headers['X-FT-Forwarding-Host'];
|
17 | if (forwardingHost) {
|
18 | return forwardingHost;
|
19 | }
|
20 |
|
21 | const fastlyOriginalHost = event.headers['Fastly-Orig-Host'];
|
22 | if (fastlyOriginalHost) {
|
23 | return fastlyOriginalHost;
|
24 | }
|
25 | return event.headers.Host;
|
26 | }
|
27 |
|
28 | const 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 |
|
39 | const getRedirectUrl = (event, opts) => {
|
40 | const query = Object.assign({}, event.queryStringParameters);
|
41 |
|
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 |
|
52 | const 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 |
|
60 | const getAuthFromCookie = (cookie) => ({
|
61 | username: getFromCookie(cookie, S3O_USERNAME),
|
62 | token: getFromCookie(cookie, S3O_TOKEN),
|
63 | });
|
64 |
|
65 | const authenticate = async(s3oClientName, cookie) => {
|
66 | const publicKey = await s3oMiddlewareUtils.publickey.get({ cache: publicKeyCache });
|
67 | const auth = getAuthFromCookie(cookie);
|
68 | return s3oMiddlewareUtils
|
69 |
|
70 | .authenticate(() => publicKey)
|
71 | .authenticateToken(auth.username, s3oClientName, auth.token);
|
72 | };
|
73 |
|
74 | const requireFunction = (func, message) => {
|
75 | if (typeof func !== 'function') {
|
76 | throw new Error(message);
|
77 | }
|
78 | }
|
79 |
|
80 | const getCookieOptions = (opts) => `; Max-Age=${opts.cookies.maxAge}` +
|
81 | `${opts.cookies.secure ? '; Secure' : ''}` +
|
82 | `${opts.cookies.httpOnly ? '; HttpOnly' : ''}`
|
83 |
|
84 | const badRequest = (statusCode, error, headers = {}) => ({
|
85 | statusCode,
|
86 | body: JSON.stringify({ error }),
|
87 | headers: Object.assign({
|
88 | 'Content-Type': 'application/json',
|
89 | }, headers)
|
90 | })
|
91 |
|
92 | const internalServerError = (error, message) => ({
|
93 | statusCode: 500,
|
94 | body: JSON.stringify({message, error}),
|
95 | headers: {
|
96 | 'Content-Type': 'application/json',
|
97 | }
|
98 | })
|
99 |
|
100 | const 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 |
|
109 | const setCookies2 = (cookieValues) => ({
|
110 | 'Set-Cookie': cookieValues[0],
|
111 | 'set-cookie': cookieValues[1]
|
112 | })
|
113 |
|
114 |
|
115 | const 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 |
|
125 | const s3oAuthenticateRedirect = (opts, queryParameters, headers) =>
|
126 | redirectResponse(`https://s3o.ft.com/v3/authenticate?${queryParameters}`, headers)
|
127 |
|
128 | const getResponse = (cookies, isSignedIn = false) => {
|
129 | const {username} = getAuthFromCookie(cookies)
|
130 | return {
|
131 | username: username === '' ? undefined : username,
|
132 | isSignedIn,
|
133 | };
|
134 | };
|
135 |
|
136 | module.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 |
|
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 | };
|