'use strict';
const https = require('https');
const crypto = require('crypto');
// ========================================
// Cognito configuration (injected by CloudFormation !Sub)
// ========================================
const CLIENT_ID = '${CognitoUserPoolClientId}';
const HOSTED_UI_DOMAIN = '${CognitoHostedUiDomain}';
const CALLBACK_URL = 'https://${CustomDomainName}/callback';
const CLIENT_SECRET = '${CognitoClientSecret}';
// ========================================
// Utility functions
// ========================================
// POST to Cognito /oauth2/token endpoint to exchange authorization code for tokens
const postTokenEndpoint = (body) => new Promise((resolve, reject) => {
const options = {
hostname: HOSTED_UI_DOMAIN,
path: '/oauth2/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64')
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try { resolve(JSON.parse(data)); }
catch (e) { reject(e); }
});
});
req.on('error', reject);
req.write(body);
req.end();
});
// Parse Cookie header string into { key: value } object
const parseCookies = (header) => {
return (header || '').split(';').reduce((acc, pair) => {
const [key, ...valueParts] = pair.trim().split('=');
if (key) acc[key.trim()] = valueParts.join('=').trim();
return acc;
}, {});
};
// Base64URL decode (for JWT header/payload parsing)
const base64UrlDecode = (str) => {
const padded = str.replace(/-/g, '+').replace(/_/g, '/');
return Buffer.from(padded + '='.repeat((4 - padded.length % 4) % 4), 'base64');
};
// Verify JWT expiration (simple check: no signature verification)
const verifyJwt = async (token) => {
const [, payloadB64] = token.split('.');
const payload = JSON.parse(base64UrlDecode(payloadB64));
if (payload.exp && payload.exp * 1000 < Date.now()) {
throw new Error('expired');
}
return payload;
};
// Generate 302 redirect response (optionally with Set-Cookie headers)
const redirect = (location, cookies) => {
const headers = {
location: [{ value: location }],
'cache-control': [{ value: 'no-cache,no-store' }]
};
if (cookies) {
headers['set-cookie'] = cookies.map((c) => ({ value: c }));
}
return { status: '302', statusDescription: 'Found', headers };
};
// Generate error response
const errorResponse = (status, description, body) => ({
status,
statusDescription: description,
headers: {
'content-type': [{ value: 'text/plain' }],
'cache-control': [{ value: 'no-cache,no-store' }]
},
body
});
// ========================================
// Main handler
// ========================================
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
const cookies = parseCookies((request.headers.cookie || [{}])[0].value);
// Pass through /api/* requests (handled by origin-request)
if (uri.startsWith('/api/')) return request;
// --- Callback handler ---
// Receive authorization code from Cognito Hosted UI, exchange for tokens, set cookies
if (uri === '/callback') {
const params = Object.fromEntries(new URLSearchParams(request.querystring || ''));
if (!params.code) return errorResponse('400', 'Bad Request', 'Missing authorization code');
try {
const body = new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: CALLBACK_URL,
code: params.code
});
const tokens = await postTokenEndpoint(body.toString());
if (!tokens.id_token || !tokens.access_token) throw new Error('token exchange failed');
const maxAge = tokens.expires_in || 3600;
// Set tokens as HttpOnly cookies and redirect to top page
return redirect('/', [
'id_token=' + tokens.id_token + ';Path=/;Secure;HttpOnly;SameSite=Lax;Max-Age=' + maxAge,
'access_token=' + tokens.access_token + ';Path=/;Secure;HttpOnly;SameSite=Lax;Max-Age=' + maxAge
]);
} catch (e) {
console.error('Token exchange error:', e);
return errorResponse('401', 'Unauthorized', 'Authentication failed');
}
}
// --- Logout handler ---
// Clear cookies and redirect to Cognito logout endpoint
if (uri === '/logout') {
return redirect(
'https://' + HOSTED_UI_DOMAIN + '/logout?client_id=' + CLIENT_ID +
'&logout_uri=' + encodeURIComponent('https://${CustomDomainName}/'),
[
'id_token=;Path=/;Secure;HttpOnly;SameSite=Lax;Max-Age=0',
'access_token=;Path=/;Secure;HttpOnly;SameSite=Lax;Max-Age=0'
]
);
}
// --- Authenticated user handler ---
// Verify id_token cookie expiration and apply SPA routing
if (cookies.id_token) {
try {
await verifyJwt(cookies.id_token);
// Rewrite extensionless paths (e.g. /dashboard) to /index.html for SPA routing
if (uri !== '/' && uri.indexOf('.') === -1) request.uri = '/index.html';
return request;
} catch (e) {
console.error('JWT validation error:', e);
// Token expired - fall through to re-authentication
}
}
// --- Unauthenticated user handler ---
// Redirect to Cognito Hosted UI login page
const nonce = crypto.randomBytes(16).toString('hex');
return redirect(
'https://' + HOSTED_UI_DOMAIN + '/oauth2/authorize' +
'?response_type=code&client_id=' + CLIENT_ID +
'&redirect_uri=' + encodeURIComponent(CALLBACK_URL) +
'&scope=openid+email+profile&state=' + nonce
);
};
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme