forked from awslabs/cognito-at-edge
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
202 lines (191 loc) · 7.76 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
const axios = require('axios');
const querystring = require('querystring');
const pino = require('pino');
const awsJwtVerify = require('aws-jwt-verify');
class Authenticator {
constructor(params) {
this._verifyParams(params);
this._region = params.region;
this._userPoolId = params.userPoolId;
this._userPoolAppId = params.userPoolAppId;
this._userPoolAppSecret = params.userPoolAppSecret;
this._userPoolDomain = params.userPoolDomain;
this._cookieExpirationDays = params.cookieExpirationDays || 365;
this._disableCookieDomain = ('disableCookieDomain' in params && params.disableCookieDomain === true) ? true : false;
this._cookieBase = `CognitoIdentityServiceProvider.${params.userPoolAppId}`;
this._logger = pino({
level: params.logLevel || 'silent', // Default to silent
base: null, //Remove pid, hostname and name logging as not usefull for Lambda
});
this._jwtVerifier = awsJwtVerify.CognitoJwtVerifier.create({
userPoolId: params.userPoolId,
clientId: params.userPoolAppId,
tokenUse: 'id',
});
}
/**
* Verify that constructor parameters are corrects.
* @param {object} params constructor params
* @return {void} throw an exception if params are incorects.
*/
_verifyParams(params) {
if (typeof params !== 'object') {
throw new Error('Expected params to be an object');
}
[ 'region', 'userPoolId', 'userPoolAppId', 'userPoolDomain' ].forEach(param => {
if (typeof params[param] !== 'string') {
throw new Error(`Expected params.${param} to be a string`);
}
});
if (params.cookieExpirationDays && typeof params.cookieExpirationDays !== 'number') {
throw new Error('Expected params.cookieExpirationDays to be a number');
}
if ('disableCookieDomain' in params && typeof params.disableCookieDomain !== 'boolean') {
throw new Error('Expected params.disableCookieDomain to be a boolean');
}
}
/**
* Exchange authorization code for tokens.
* @param {String} redirectURI Redirection URI.
* @param {String} code Authorization code.
* @return {Promise} Authenticated user tokens.
*/
_fetchTokensFromCode(redirectURI, code) {
const authorization = this._userPoolAppSecret && Buffer.from(`${this._userPoolAppId}:${this._userPoolAppSecret}`).toString('base64');
const request = {
url: `https://${this._userPoolDomain}/oauth2/token`,
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
...(authorization && {'Authorization': `Basic ${authorization}`}),
},
data: querystring.stringify({
client_id: this._userPoolAppId,
code: code,
grant_type: 'authorization_code',
redirect_uri: redirectURI,
}),
};
this._logger.debug({ msg: 'Fetching tokens from grant code...', request, code });
return axios.request(request)
.then(resp => {
this._logger.debug({ msg: 'Fetched tokens', tokens: resp.data });
return resp.data;
})
.catch(err => {
this._logger.error({ msg: 'Unable to fetch tokens from grant code', request, code });
throw err;
});
}
/**
* Create a Lambda@Edge redirection response to set the tokens on the user's browser cookies.
* @param {Object} tokens Cognito User Pool tokens.
* @param {String} domain Website domain.
* @param {String} location Path to redirection.
* @return {Object} Lambda@Edge response.
*/
async _getRedirectResponse(tokens, domain, location) {
const decoded = await this._jwtVerifier.verify(tokens.id_token);
const username = decoded['cognito:username'];
const usernameBase = `${this._cookieBase}.${username}`;
const directives = (!this._disableCookieDomain) ?
`Domain=${domain}; Expires=${new Date(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure` :
`Expires=${new Date(new Date() * 1 + this._cookieExpirationDays * 864e+5)}; Secure`;
const response = {
status: '302' ,
headers: {
'location': [{ key: 'Location', 'value': location }],
'set-cookie': [
{
key: 'Set-Cookie',
value: `${usernameBase}.accessToken=${tokens.access_token}; ${directives}`,
},
{
key: 'Set-Cookie',
value: `${usernameBase}.idToken=${tokens.id_token}; ${directives}`,
},
{
key: 'Set-Cookie',
value: `${usernameBase}.refreshToken=${tokens.refresh_token}; ${directives}`,
},
{
key: 'Set-Cookie',
value: `${usernameBase}.tokenScopesString=phone email profile openid aws.cognito.signin.user.admin; ${directives}`,
},
{
key: 'Set-Cookie',
value: `${this._cookieBase}.LastAuthUser=${username}; ${directives}`,
},
],
},
};
this._logger.debug({ msg: 'Generated set-cookie response', response });
return response;
}
/**
* Extract value of the authentication token from the request cookies.
* @param {Array} cookies Request cookies.
* @return {String} Extracted access token. Throw if not found.
*/
_getIdTokenFromCookie(cookies) {
this._logger.debug({ msg: 'Extracting authentication token from request cookie', cookies });
// eslint-disable-next-line no-useless-escape
const regex = new RegExp(`${this._userPoolAppId}\..+?\.idToken=(.*?);`);
if (cookies) {
for (let i = 0; i < cookies.length; i++) {
const matches = cookies[i].value.match(regex);
if (matches && matches.length > 1) {
this._logger.debug({ msg: ' Found token in cookie', token: matches[1] });
return matches[1];
}
}
}
this._logger.debug(" idToken wasn't present in request cookies");
throw new Error("Id token isn't present in the request cookies");
}
/**
* Handle Lambda@Edge event:
* * if authentication cookie is present and valid: forward the request
* * if ?code=<grant code> is present: set cookies with new tokens
* * else redirect to the Cognito UserPool to authenticate the user
* @param {Object} event Lambda@Edge event.
* @return {Promise} CloudFront response.
*/
async handle(event) {
this._logger.debug({ msg: 'Handling Lambda@Edge event', event });
const { request } = event.Records[0].cf;
const requestParams = querystring.parse(request.querystring);
const cfDomain = request.headers.host[0].value;
const redirectURI = `https://${cfDomain}`;
try {
const token = this._getIdTokenFromCookie(request.headers.cookie);
this._logger.debug({ msg: 'Verifying token...', token });
const user = await this._jwtVerifier.verify(token);
this._logger.info({ msg: 'Forwarding request', path: request.uri, user });
return request;
} catch (err) {
this._logger.debug("User isn't authenticated: %s", err);
if (requestParams.code) {
return this._fetchTokensFromCode(redirectURI, requestParams.code)
.then(tokens => this._getRedirectResponse(tokens, cfDomain, decodeURIComponent(requestParams.state)));
} else {
let redirectPath = request.uri;
if (request.querystring && request.querystring !== '') {
redirectPath += encodeURIComponent('?' + request.querystring);
}
const userPoolUrl = `https://${this._userPoolDomain}/authorize?redirect_uri=${redirectURI}&response_type=code&client_id=${this._userPoolAppId}&state=${redirectPath}`;
this._logger.debug(`Redirecting user to Cognito User Pool URL ${userPoolUrl}`);
return {
status: 302,
headers: {
location: [{
key: 'Location',
value: userPoolUrl,
}],
},
};
}
}
}
}
module.exports.Authenticator = Authenticator;