0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CloudFrontのview-requestのサンプル

0
Posted at
          '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
            );
          };

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?