Skip to content

fix: Update dependencies to resolve OIDC warnings.#53

Merged
jamesiarmes merged 2 commits intomainfrom
oidc-settings
Apr 23, 2026
Merged

fix: Update dependencies to resolve OIDC warnings.#53
jamesiarmes merged 2 commits intomainfrom
oidc-settings

Conversation

@jamesiarmes
Copy link
Copy Markdown
Member

Also update path of configurations to match other repositories (configs instead of config).

Also update path of configurations to match other repositories (`configs` instead of `config`).
@github-actions
Copy link
Copy Markdown

Plan output for hosting config


OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place (current -> planned)
-/+ destroy and then create replacement
+/- create replacement and then destroy

OpenTofu will perform the following actions:

  # module.app["dc-sebt-portal"].module.secrets.aws_kms_alias.secrets has moved to module.app["dc-sebt-portal"].module.secrets.aws_kms_alias.secrets["this"]
    resource "aws_kms_alias" "secrets" {
        id             = "alias/sebt-portal/development/secrets"
        name           = "alias/sebt-portal/development/secrets"
        # (4 unchanged attributes hidden)
    }

  # module.app["dc-sebt-portal"].module.secrets.aws_kms_key.secrets has moved to module.app["dc-sebt-portal"].module.secrets.aws_kms_key.secrets["this"]
    resource "aws_kms_key" "secrets" {
        id                                 = "059725fa-3cf5-4f38-ae36-762576b4e33e"
        tags                               = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
        # (14 unchanged attributes hidden)
    }

  # module.app["dc-sebt-portal"].module.service["web"].aws_iam_role.execution must be replaced
+/- resource "aws_iam_role" "execution" {
      ~ arn                   = "arn:aws:iam::816069131564:role/sebt-portal-development-web-execution" -> (known after apply)
      ~ create_date           = "2025-06-12T20:33:02Z" -> (known after apply)
      ~ id                    = "sebt-portal-development-web-execution" -> (known after apply)
      ~ managed_policy_arns   = [
          - "arn:aws:iam::816069131564:policy/sebt-portal-development-web-secrets-access-20250612203316430400000005",
          - "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy",
          - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
        ] -> (known after apply)
      ~ name                  = "sebt-portal-development-web-execution" -> "sebt-portal-development-web-exec" # forces replacement
      + name_prefix           = (known after apply)
        tags                  = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
      ~ unique_id             = "AROA34AMCXUWN7CKIJ5WD" -> (known after apply)
        # (6 unchanged attributes hidden)

      ~ inline_policy {
          + arn                   = (known after apply)
          + assume_role_policy    = (known after apply)
          + create_date           = (known after apply)
          + description           = (known after apply)
          + force_detach_policies = (known after apply)
          + id                    = (known after apply)
          + managed_policy_arns   = (known after apply)
          + max_session_duration  = (known after apply)
          + name                  = (known after apply)
          + name_prefix           = (known after apply)
          + path                  = (known after apply)
          + permissions_boundary  = (known after apply)
          + tags                  = (known after apply)
          + tags_all              = (known after apply)
          + unique_id             = (known after apply)
        } -> (known after apply)
    }

  # module.app["dc-sebt-portal"].module.service["web"].aws_iam_role_policy_attachments_exclusive.execution must be replaced
-/+ resource "aws_iam_role_policy_attachments_exclusive" "execution" {
      ~ role_name   = "sebt-portal-development-web-execution" -> "sebt-portal-development-web-exec" # forces replacement
        # (1 unchanged attribute hidden)
    }

  # module.app["dc-sebt-portal"].module.service["web"].aws_kms_key.fargate will be updated in-place
  ~ resource "aws_kms_key" "fargate" {
        id                                 = "4527d3ac-0df9-4d4d-8f02-7f0f22a32b28"
      ~ policy                             = jsonencode(
            {
              - Id        = "Fargate hosting key policy"
              - Statement = [
                  - {
                      - Action    = "kms:*"
                      - Effect    = "Allow"
                      - Principal = {
                          - AWS = "arn:aws:iam::816069131564:root"
                        }
                      - Resource  = "*"
                      - Sid       = "Enable IAM User Permissions"
                    },
                  - {
                      - Action    = [
                          - "kms:Decrypt",
                          - "kms:Encrypt",
                          - "kms:GenerateDataKey",
                          - "kms:ReEncrypt*",
                        ]
                      - Condition = {
                          - StringEquals = {
                              - "kms:CallerAccount"                 = "816069131564"
                              - "kms:EncryptionContext:aws:ecr:arn" = "arn:aws:ecr:us-east-1:816069131564:repository/sebt-portal-development-web"
                            }
                        }
                      - Effect    = "Allow"
                      - Principal = {
                          - AWS = "*"
                        }
                      - Resource  = "*"
                      - Sid       = "Allow ECR to encrypt container images"
                    },
                  - {
                      - Action    = "kms:GenerateDataKey*"
                      - Effect    = "Allow"
                      - Principal = {
                          - AWS = "*"
                        }
                      - Resource  = "*"
                      - Sid       = "Allow Fargate containers to encrypt objects"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        tags                               = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
        # (13 unchanged attributes hidden)
    }

  # module.app["dc-sebt-portal"].module.service["web"].module.alb["this"].aws_lb_listener.this["https"] will be updated in-place
  ~ resource "aws_lb_listener" "this" {
        id                                   = "arn:aws:elasticloadbalancing:us-east-1:816069131564:listener/app/sebt-portal-development-web/13cd57b9f185027b/06b7c6aa632be5cd"
      ~ ssl_policy                           = "ELBSecurityPolicy-TLS-1-2-2017-01" -> "ELBSecurityPolicy-TLS13-1-2-Res-PQ-2025-09"
        tags                                 = {
            "application"           = "sebt-portal-development"
            "awsApplication"        = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"           = "development"
            "program"               = "safety-net"
            "project"               = "sebt-portal"
            "terraform-aws-modules" = "alb"
        }
        # (8 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.app["dc-sebt-portal"].module.service["web"].module.ecs.aws_ecs_cluster.main will be updated in-place
  ~ resource "aws_ecs_cluster" "main" {
        id       = "arn:aws:ecs:us-east-1:816069131564:cluster/sebt-portal-development-web"
        name     = "sebt-portal-development-web"
        tags     = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
        # (3 unchanged attributes hidden)

      - setting {
          - name  = "containerInsights" -> null
          - value = "enabled" -> null
        }
      + setting {
          + name  = "containerInsights"
          + value = "enhanced"
        }
    }

  # module.app["dc-sebt-portal"].module.database["this"].module.mssql["this"].module.db_instance.aws_db_instance.this[0] will be updated in-place
  ~ resource "aws_db_instance" "this" {
      ~ engine_version                        = "16.00.4215.2.v1" -> "16.00.4245.2.v1"
        id                                    = "db-AQO5FR4MFE4QXEK44UMOZ5DUYE"
      ~ password_wo                           = (write-only attribute)
        tags                                  = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
        # (60 unchanged attributes hidden)
    }

  # module.app["dc-sebt-portal"].module.service["web"].module.ecs_service.module.fargate.aws_ecs_service.main[0] will be updated in-place
  ~ resource "aws_ecs_service" "main" {
      + force_new_deployment               = false
        id                                 = "arn:aws:ecs:us-east-1:816069131564:service/sebt-portal-development-web/sebt-portal-development-web"
        name                               = "sebt-portal-development-web"
        tags                               = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
      ~ task_definition                    = "arn:aws:ecs:us-east-1:816069131564:task-definition/sebt-portal-development-web:35" -> (known after apply)
        # (19 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }

  # module.app["dc-sebt-portal"].module.service["web"].module.ecs_service.module.fargate.module.task.aws_ecs_task_definition.main[0] must be replaced
+/- resource "aws_ecs_task_definition" "main" {
      ~ arn                      = "arn:aws:ecs:us-east-1:816069131564:task-definition/sebt-portal-development-web:35" -> (known after apply)
      ~ arn_without_revision     = "arn:aws:ecs:us-east-1:816069131564:task-definition/sebt-portal-development-web" -> (known after apply)
      ~ container_definitions    = jsonencode(
          ~ [
              ~ {
                  - mountPoints      = []
                    name             = "otel-collector"
                  - portMappings     = []
                  - systemControls   = []
                  - volumesFrom      = []
                    # (8 unchanged attributes hidden)
                },
              ~ {
                  - mountPoints       = []
                    name              = "sebt-portal-development-web"
                  ~ portMappings      = [
                      ~ {
                          - hostPort      = 8080
                          - protocol      = "tcp"
                            # (1 unchanged attribute hidden)
                        },
                    ]
                  - systemControls    = []
                  - volumesFrom       = []
                    # (9 unchanged attributes hidden)
                },
            ]
        )
      ~ execution_role_arn       = "arn:aws:iam::816069131564:role/sebt-portal-development-web-execution" # forces replacement -> (known after apply) # forces replacement
      ~ id                       = "sebt-portal-development-web" -> (known after apply)
      ~ revision                 = 35 -> (known after apply)
        tags                     = {
            "application"    = "sebt-portal-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/sebt-portal-development/0015lbmzv6im0turnaqk9d1eyw"
            "environment"    = "development"
            "program"        = "safety-net"
            "project"        = "sebt-portal"
        }
        # (11 unchanged attributes hidden)
    }

Plan: 3 to add, 5 to change, 3 to destroy.

Changes to Outputs:
  ~ app = {
      ~ dc-sebt-portal = {
          ~ services   = {
              ~ web = {
                  + execution_role_arn         = (known after apply)
                  + task_role_arn              = "arn:aws:iam::816069131564:role/sebt-portal-development-web-task"
                    # (10 unchanged attributes hidden)
                }
            }
            # (1 unchanged attribute hidden)
        }
    }

Warning: Deprecated attribute

  on .terraform/modules/app.secrets/kms.tf line 10, in resource "aws_kms_key" "secrets":
  10:     region : data.aws_region.current.name,

The attribute "name" is deprecated. Refer to the provider documentation for
details.

(and 12 more similar warnings elsewhere)

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    tofu apply "tfplan"

@github-actions
Copy link
Copy Markdown

Plan output for foundation config


No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.

@github-actions
Copy link
Copy Markdown

Plan output for networking config


No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and
found no differences, so no changes are needed.

Warning: Deprecated attribute

  on .terraform/modules/vpc.vpc/vpc-flow-logs.tf line 28, in locals:
  28:     "arn:${data.aws_partition.current[0].partition}:logs:${data.aws_region.current[0].name}:${data.aws_caller_identity.current[0].account_id}:log-group:${log_group.name}:*"

The attribute "name" is deprecated. Refer to the provider documentation for
details.

(and one more similar warning elsewhere)

@github-actions
Copy link
Copy Markdown

Plan output for docs config


Note: Objects have changed outside of OpenTofu

OpenTofu detected the following changes made outside of OpenTofu since the
last "tofu apply" which may have affected this plan:

  # module.docs.local_file.lambda_js has been deleted
  - resource "local_file" "lambda_js" {
      - content_sha256       = "2b8d1273467a00a90d29365741f5295a563f42b18061db9b9abf9bb3d4877cc4" -> null
        id                   = "c67f92072c532b7d904d589efa3ad034e551fd3c"
        # (9 unchanged attributes hidden)
    }

  # module.docs.local_file.pkg_json has been deleted
  - resource "local_file" "pkg_json" {
      - content              = jsonencode(
            {
              - dependencies = {
                  - "@aws-sdk/client-secrets-manager" = "^3.556"
                  - jose                              = "^5.2"
                }
              - description  = "Lambda@Edge function for OIDC authentication"
              - main         = "index.js"
              - name         = "cfa-documentation-development-oidc"
              - type         = "module"
              - version      = "1.0.0"
            }
        ) -> null
        id                   = "e013a0025c0e7abd85fee47f36a77c66c1d7b569"
        # (9 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

OpenTofu used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place (current -> planned)
-/+ destroy and then create replacement
 <= read (data resources)

OpenTofu will perform the following actions:

  # module.docs.data.archive_file.oidc will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "archive_file" "oidc" {
      + id                  = (known after apply)
      + output_base64sha256 = (known after apply)
      + output_base64sha512 = (known after apply)
      + output_md5          = (known after apply)
      + output_path         = "../../modules/docs/dist/oidc-function.zip"
      + output_sha          = (known after apply)
      + output_sha256       = (known after apply)
      + output_sha512       = (known after apply)
      + output_size         = (known after apply)
      + source_dir          = "../../modules/docs/dist/oidc"
      + type                = "zip"
    }

  # module.docs.aws_cloudfront_distribution.endpoint will be updated in-place
  ~ resource "aws_cloudfront_distribution" "endpoint" {
        id                             = "E2ISS554TAP1I2"
        tags                           = {
            "application"    = "cfa-documentation-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/cfa-documentation-development/0exl1gqfy25cyqyqiwbl3kno40"
            "environment"    = "development"
            "program"        = "engineering"
            "project"        = "cfa-documentation"
        }
        # (22 unchanged attributes hidden)

      ~ default_cache_behavior {
            # (12 unchanged attributes hidden)

          - lambda_function_association {
              - event_type   = "viewer-request" -> null
              - include_body = false -> null
              - lambda_arn   = "arn:aws:lambda:us-east-1:816069131564:function:cfa-documentation-development-oidc:21" -> null
            }
          + lambda_function_association {
              + event_type   = "viewer-request"
              + include_body = false
              + lambda_arn   = (known after apply)
            }

            # (1 unchanged block hidden)
        }

        # (4 unchanged blocks hidden)
    }

  # module.docs.aws_lambda_function.oidc will be updated in-place
  ~ resource "aws_lambda_function" "oidc" {
        id                             = "cfa-documentation-development-oidc"
      ~ last_modified                  = "2026-02-10T21:49:12.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:816069131564:function:cfa-documentation-development-oidc:21" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:816069131564:function:cfa-documentation-development-oidc:21/invocations" -> (known after apply)
      ~ source_code_hash               = "vCIynThInHlEaOlUWXcZhdOQWDfHIOYaWmo/Fx3YiIg=" -> (known after apply)
        tags                           = {
            "application"    = "cfa-documentation-development"
            "awsApplication" = "arn:aws:resource-groups:us-east-1:816069131564:group/cfa-documentation-development/0exl1gqfy25cyqyqiwbl3kno40"
            "environment"    = "development"
            "program"        = "engineering"
            "project"        = "cfa-documentation"
            "use"            = "edge-function"
        }
      ~ version                        = "21" -> (known after apply)
        # (19 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

  # module.docs.local_file.lambda_js will be created
  + resource "local_file" "lambda_js" {
      + content              = <<-EOT
            /**
             * Lambda@Edge Function for Okta Authentication
             *
             * This function intercepts requests at a CloudFront edge location to enforce
             * authentication via the OpenID Connect (OIDC) with Okta.
             *
             * How it works:
             *
             * 1. On a viewer request, it checks for a valid session JWT (id_token)
             * 2. If the JWT is missing or invalid, it redirects the user to the Okta login
             *    page
             * 3. After Okta login, the user is redirected back to a /callback URL
             * 4. The function handles the /callback, exchanges the authorization code from
             *    Okta for an ID token, validates the token, and sets a secure cookie
             * 5.  For all later requests, the function allows access to the S3 content
             */
            import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
            import * as jose from 'jose';
            import { randomBytes } from 'crypto';
            import { stringify as stringifyQuery, parse as parseQuery } from 'querystring';
            
            const SECRET_ARN = 'arn:aws:secretsmanager:us-east-1:816069131564:secret:cfa-documentation/development/OIDC_SETTINGS-tNftAV';
            const DOMAIN = "https://codeforamerica.okta.com";
            const CALLBACK_PATH = '/_callback';
            const LOGOUT_PATH = '/_logout';
            const COOKIE_SETTINGS = 'Path=/; Secure; HttpOnly; SameSite=Lax';
            const SESSION_DURATION_SECONDS = 8 * 60 * 60; // 8 hours
            const ID_TOKEN_COOKIE_NAME = 'IdToken';
            const NONCE_COOKIE_NAME = 'Nonce';
            
            const PROTECTED_PATHS = [
              ]
            
            
            // Cache secrets and JWKS in the global scope to reuse across warm invocations.
            let cachedSecrets = null;
            let cachedJwks = null;
            
            /**
             * Fetches secrets from AWS Secrets Manager.
             *
             * Caches the result in a global variable for subsequent invocations.
             *
             * @returns {Promise<object>} The parsed JSON object of secrets.
             */
            async function getSecrets() {
              if (cachedSecrets) {
                console.log('Using cached secrets.');
                return cachedSecrets;
              }
            
              console.log(`Fetching secrets from ARN: ${SECRET_ARN}`);
              const secretsClient = new SecretsManagerClient({ region: 'us-east-1' });
              const command = new GetSecretValueCommand({ SecretId: SECRET_ARN });
            
              try {
                const data = await secretsClient.send(command);
                if ('SecretString' in data) {
                  cachedSecrets = JSON.parse(data.SecretString);
                  return cachedSecrets;
                }
            
                throw new Error('SecretString not found in Secrets Manager response.');
              } catch (error) {
                console.error('Failed to fetch secrets:', error);
                throw new Error('Could not retrieve secrets from AWS Secrets Manager.');
              }
            }
            
            /**
             * Fetches the JSON Web Key Set (JWKS) from the Okta authorization server.
             *
             * Caches the result for reuse.
             *
             * @param {string} oktaDomain The base URL of your Okta domain.
             * @returns {jose.JWKSet} A key set that can be used by the jose library.
             */
            async function getJwks(oktaDomain) {
              if (cachedJwks) {
                console.log('Using cached JWKS.');
                return cachedJwks;
              }
            
              console.log('Fetching JWKS from Okta.');
              const jwksUrl = new URL(`${oktaDomain}/oauth2/v1/keys`);
              cachedJwks = jose.createRemoteJWKSet(jwksUrl);
            
              return cachedJwks;
            }
            
            /**
             * The main handler function executed by Lambda@Edge.
             */
            export const handler = async (event) => {
              const { request } = event.Records[0].cf;
              const domainName = request.headers.host[0].value;
              const requestedUri = `${request.uri}${request.querystring ? '?' + request.querystring : ''}`;
            
              console.log(`Request for URI: ${requestedUri}`);
            
              try {
                const secrets = await getSecrets();
            
                // Route request based on the path.
                switch (request.uri) {
                  case CALLBACK_PATH:
                    return handleAuthCallback(request, secrets, domainName);
                  case LOGOUT_PATH:
                    return handleLogout(secrets, domainName);
                  default:
                    request.uri = rewriteUri(request.uri);
            
                    // If this path matches any of the protected paths, we need to
                    // verify the user's authentication status.
                    if (PROTECTED_PATHS.some(pattern => pattern.test(request.uri))) {
                      console.log('Protected path detected, verifying authentication.');
                      return verifyAuthentication(request, secrets, domainName, requestedUri);
                    }
            
                    return request;
                }
              } catch (error) {
                console.error('Unhandled error in handler:', error);
                return {
                  status: '500',
                  statusDescription: 'Internal Server Error',
                  body: 'An unexpected error occurred. Please check the logs.',
                };
              }
            };
            
            /**
             * Rewrites the request URI to ensure it points to the correct S3 object.
             *
             * @param {string} requestUri The original request URI.
             * @returns {*|string}
             */
            function rewriteUri(requestUri) {
              // If the request is being made to a directory (e.g. / or /docs), we want to
              // append "index.html" so that S3 serves the proper object. If the path
              // doesn't contain a file extension, we assume it's a directory as well.
              if (requestUri.endsWith('/')) {
                return `${requestUri}index.html`;
              } else if (!requestUri.includes('.')) {
                return `${requestUri}/index.html`;
              }
            
              return requestUri;
            }
            
            /**
             * Verifies if the user has a valid session cookie and redirects to Okta login
             * if not.
             *
             * @param {object} request The CloudFront request object.
             * @param {object} secrets The Okta secrets.
             * @param {string} domainName The domain of the request.
             * @param {string} requestedUri The original URI the user requested.
             * @returns {Promise<object>} A request or redirect response.
             */
            async function verifyAuthentication(request, secrets, domainName, requestedUri) {
              const cookies = parseCookies(request.headers);
              const idToken = cookies[ID_TOKEN_COOKIE_NAME];
            
              if (!idToken) {
                console.log('No IdToken cookie found. Redirecting to login.');
                return redirectToLogin(request, secrets, domainName, requestedUri);
              }
            
              try {
                console.log('Verifying IdToken...');
                const jwks = await getJwks(DOMAIN);
                const { payload } = await jose.jwtVerify(idToken, jwks, {
                  issuer: DOMAIN,
                  audience: secrets.client_id,
                });
            
                // The token is valid, allow the request to proceed to the origin.
                console.log(`Token verified for user: ${payload.sub}`);
                return request;
              } catch (err) {
                // The token is invalid or expired; redirect to log in.
                console.error('IdToken verification failed:', err.message);
                return redirectToLogin(request, secrets, domainName, requestedUri);
              }
            }
            
            /**
             * Handles the OIDC callback from Okta.
             *
             * @param {object} request The CloudFront request object.
             * @param {object} secrets The Okta secrets.
             * @param {string} domainName The domain of the request.
             * @returns {Promise<object>} A redirect response.
             */
            async function handleAuthCallback(request, secrets, domainName) {
              const cookies = parseCookies(request.headers);
              const query = parseQuery(request.querystring);
              const { code, state } = query;
            
              // Validate state to prevent CSRF.
              const decodedState = JSON.parse(Buffer.from(state, 'base64').toString());
              if (decodedState.nonce !== cookies[NONCE_COOKIE_NAME]) {
                throw new Error('Invalid state parameter. Possible CSRF attack.');
              }
            
              // Exchange authorization code for tokens.
              console.log('Exchanging authorization code for tokens...');
              const tokenEndpoint = new URL(`${DOMAIN}/oauth2/v1/token`);
              const redirectUri = `https://${domainName}${CALLBACK_PATH}`;
              const basicAuth = Buffer.from(`${secrets.client_id}:${secrets.client_secret}`).toString('base64');
            
              const tokenResponse = await fetch(tokenEndpoint.toString(), {
                method: 'POST',
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded',
                  'Authorization': `Basic ${basicAuth}`,
                },
                body: stringifyQuery({
                  grant_type: 'authorization_code',
                  code,
                  redirect_uri: redirectUri,
                }),
              });
            
              if (!tokenResponse.ok) {
                const errorBody = await tokenResponse.text();
                throw new Error(`Failed to exchange code for token: ${errorBody}`);
                }
            
                const { id_token } = await tokenResponse.json();
            
                // Create redirect response with the session cookie.
                console.log('Token received. Setting session cookie and redirecting .');
                return {
                    status: '302',
                    statusDescription: 'Found',
                    headers: {
                        'location': [{ key: 'Location', value: decodedState.requestedUri }],
                        'set-cookie': [
                            { key: 'Set-Cookie', value: `${ID_TOKEN_COOKIE_NAME}=${id_token}; Max-Age=${SESSION_DURATION_SECONDS}; ${COOKIE_SETTINGS}` },
                            { key: 'Set-Cookie', value: `${NONCE_COOKIE_NAME}=; Max-Age=0; ${COOKIE_SETTINGS}` }, // Clear nonce
                        ],
                    },
                };
            }
            
            /**
             * Generates a redirect response to the Okta login page.
             *
             * @param {object} request The CloudFront request object.
             * @param {object} secrets The Okta secrets.
             * @param {string} domainName The domain of the request.
             * @param {string} requestedUri The original URI the user requested.
             * @returns {object} A redirect response object.
             */
            function redirectToLogin(request, secrets, domainName, requestedUri) {
                const nonce = randomBytes(16).toString('hex');
                const state = Buffer.from(JSON.stringify({ nonce, requestedUri })).toString('base64');
                const redirectUri = `https://${domainName}${CALLBACK_PATH}`;
            
                const loginParams = stringifyQuery({
                    response_type: 'code',
                    client_id: secrets.client_id,
                    redirect_uri: redirectUri,
                    scope: 'openid profile email',
                    state,
                    nonce,
                });
            
                const loginUrl = `${DOMAIN}/oauth2/v1/authorize?${loginParams}`;
            
                return {
                    status: '302',
                    statusDescription: 'Found',
                    headers: {
                        'location': [{ key: 'Location', value: loginUrl }],
                        'set-cookie': [{ key: 'Set-Cookie', value: `${NONCE_COOKIE_NAME}=${nonce}; ${COOKIE_SETTINGS}` }],
                    },
                };
            }
            
            /**
             * Clears session cookies and redirects to the Okta logout URL.
             *
             * @param {object} secrets The Okta secrets.
             * @param {string} domainName The domain of the request.
             * @returns {object} A redirect response object.
             */
            function handleLogout(secrets, domainName) {
                const postLogoutRedirectUri = `https://${domainName}`;
                const logoutUrl = `${DOMAIN}/oauth2/v1/logout?post_logout_redirect_uri=${encodeURIComponent(postLogoutRedirectUri)}`;
            
                console.log('Logging out user.');
                return {
                    status: '302',
                    statusDescription: 'Found',
                    headers: {
                        'location': [{ key: 'Location', value: logoutUrl }],
                        'set-cookie': [
                            { key: 'Set-Cookie', value: `${ID_TOKEN_COOKIE_NAME}=; Max-Age=0; ${COOKIE_SETTINGS}` },
                            { key: 'Set-Cookie', value: `${NONCE_COOKIE_NAME}=; Max-Age=0; ${COOKIE_SETTINGS}` },
                        ],
                    },
                };
            }
            
            /**
             * Parses cookies from request headers.
             *
             * @param {object} headers The request headers object.
             * @returns {object} A key-value map of cookies.
             */
            function parseCookies(headers) {
                const cookies = {};
                if (headers.cookie) {
                    headers.cookie[0].value.split(';').forEach(cookie => {
                        if (cookie) {
                            const parts = cookie.split('=');
                            const name = parts[0].trim();
                            const value = parts.slice(1).join('=').trim();
                            if (name && value) {
                                cookies[name] = value;
                            }
                        }
                    });
                }
            
                return cookies;
            }
        EOT
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "../../modules/docs/dist/oidc/index.js"
      + id                   = (known after apply)
    }

  # module.docs.local_file.pkg_json will be created
  + resource "local_file" "pkg_json" {
      + content              = jsonencode(
            {
              + dependencies = {
                  + "@aws-sdk/client-secrets-manager" = "^3.556"
                  + jose                              = "^5.2"
                }
              + description  = "Lambda@Edge function for OIDC authentication"
              + main         = "index.js"
              + name         = "cfa-documentation-development-oidc"
              + type         = "module"
              + version      = "1.0.0"
            }
        )
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "../../modules/docs/dist/oidc/package.json"
      + id                   = (known after apply)
    }

  # module.docs.null_resource.npm_install must be replaced
-/+ resource "null_resource" "npm_install" {
      ~ id       = "7963066879779498238" -> (known after apply)
      ~ triggers = { # forces replacement
          ~ "lambda_hash"       = "2b8d1273467a00a90d29365741f5295a563f42b18061db9b9abf9bb3d4877cc4" -> (known after apply)
            # (2 unchanged elements hidden)
        }
    }

Plan: 3 to add, 2 to change, 1 to destroy.

Warning: Deprecated attribute

  on .terraform/modules/docs.bucket/locals.tf line 4, in locals:
   4:   region          = data.aws_region.current.name

The attribute "name" is deprecated. Refer to the provider documentation for
details.

(and 7 more similar warnings elsewhere)

Warning: Object attribute is ignored

  on ../../modules/docs/main.tf line 11, in module "secrets":
   9:     OIDC_SETTINGS = {
  10:       description = "OIDC credentials for static documentation hosting"
  11:       type        = "json"
  12:       start_value = jsonencode({
  13:         client_id     = "abc",
  14:         client_secret = "123",
  15:       })
  16:     }

The object type for input variable "secrets" nested value ["OIDC_SETTINGS"]
does not include an attribute named "type", so this definition is unused.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    tofu apply "tfplan"

@jamesiarmes jamesiarmes merged commit 1bc71f6 into main Apr 23, 2026
15 checks passed
@jamesiarmes jamesiarmes deleted the oidc-settings branch April 23, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant