-
Notifications
You must be signed in to change notification settings - Fork 10
/
index.js
154 lines (142 loc) · 4.96 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Note: no need to bundle <aws-sdk>, it's provided by Lambda
const AWS = require('aws-sdk')
const async = require('async')
// const contentType = require('content-type')
const qs = require('querystringparser')
const htpasswd = require('htpasswd-auth')
const cloudfront = require('aws-cloudfront-sign')
// --------------
// Lambda function parameters, as environment variables
// --------------
const CONFIG_KEYS = {
websiteDomain: 'WEBSITE_DOMAIN',
sessionDuration: 'SESSION_DURATION',
redirectOnSuccess: 'REDIRECT_ON_SUCCESS',
cloudFrontKeypairId: 'CLOUDFRONT_KEYPAIR_ID',
cloudFrontPrivateKey: 'ENCRYPTED_CLOUDFRONT_PRIVATE_KEY',
htpasswd: 'ENCRYPTED_HTPASSWD'
}
// --------------
// Main function exported to Lambda
// Checks username/password against the <htaccess> entries
// --------------
exports.handler = (event, context, callback) => {
// try to parse the request payload based on Content-Type
const requestHeaders = normaliseHeaders(event.headers)
const body = parsePayload(event.body, requestHeaders)
if (!body || !body.username || !body.password) {
return callback(null, {
statusCode: 400,
body: 'Bad request'
})
}
// get and decrypt config values
async.mapValues(CONFIG_KEYS, getConfigValue, function (err, config) {
if (err) {
callback(null, {
statusCode: 500,
body: 'Server error'
})
} else {
// validate username and password
htpasswd.authenticate(body.username, body.password, config.htpasswd).then((authenticated) => {
if (authenticated) {
console.log('Successful login for: ' + body.username)
var responseHeaders = cookiesHeaders(config)
var statusCode = 200
if (config.redirectOnSuccess === 'true') {
statusCode = 302
responseHeaders['Location'] = requestHeaders['referer'] || '/'
}
callback(null, {
statusCode: statusCode,
body: 'Authentication successful',
headers: responseHeaders
})
} else {
console.log('Invalid login for: ' + body.username)
callback(null, {
statusCode: 403,
body: 'Authentication failed',
headers: {
// clear any existing cookies
'Set-Cookie': 'CloudFront-Policy=',
'SEt-Cookie': 'CloudFront-Signature=',
'SET-Cookie': 'CloudFront-Key-Pair-Id='
}
})
}
})
}
})
}
// --------------
// Parse the body, either from JSON or Form data
// --------------
function parsePayload (body, headers) {
const type = headers['content-type']
// const parsedType = contentType.parse(rawType)
if (type === 'application/json') {
try {
return JSON.parse(body)
} catch (e) {
console.log('Failed to parse JSON payload')
return null
}
} else if (type === 'application/x-www-form-urlencoded') {
return qs.parse(body)
} else {
return null
}
}
// --------------
// Returns the corresponding config value
// After decrypting it with KMS if required
// --------------
function getConfigValue (configName, target, done) {
if (/^ENCRYPTED/.test(configName)) {
const kms = new AWS.KMS()
const encrypted = process.env[configName]
kms.decrypt({ CiphertextBlob: new Buffer(encrypted, 'base64') }, (err, data) => {
if (err) done(err)
else done(null, data.Plaintext.toString('ascii'))
})
} else {
done(null, process.env[configName])
}
}
// --------------
// Returns an object with all HTTP headers in lowercase
// Because browsers will send inconsistent keys like 'Content-Type' or 'content-type'
// --------------
function normaliseHeaders (headers) {
return Object.keys(headers).reduce((acc, key) => {
acc[key.toLowerCase()] = headers[key]
return acc
}, {})
}
// --------------
// Creates 3 CloudFront signed cookies
// They're effectively an IAM policy, and a private signature to prove it's valid
// --------------
function cookiesHeaders (config) {
const sessionDuration = parseInt(config.sessionDuration, 10)
// create signed cookies
const signedCookies = cloudfront.getSignedCookies('https://' + config.websiteDomain + '/*', {
expireTime: new Date().getTime() + (sessionDuration * 1000),
keypairId: config.cloudFrontKeypairId,
privateKeyString: config.cloudFrontPrivateKey
})
// extra options for all cookies we write
// var date = new Date()
// date.setTime(date + (config.cookieExpiryInSeconds * 1000))
const options = '; Domain=' + config.websiteDomain + '; Path=/; Secure; HttpOnly'
// we use a combination of lower/upper case
// because we need to send multiple cookies
// but the AWS API requires all headers in a single object!
return {
'Set-Cookie': 'CloudFront-Policy=' + signedCookies['CloudFront-Policy'] + options,
'SEt-Cookie': 'CloudFront-Signature=' + signedCookies['CloudFront-Signature'] + options,
'SET-Cookie': 'CloudFront-Key-Pair-Id=' + signedCookies['CloudFront-Key-Pair-Id'] + options
}
}