Skip to content

feat:[NEXT-1932]: Authorizer Lambda for Download.#2391

Closed
shaikcyber wants to merge 5 commits into
masterfrom
feat/download-authorizer
Closed

feat:[NEXT-1932]: Authorizer Lambda for Download.#2391
shaikcyber wants to merge 5 commits into
masterfrom
feat/download-authorizer

Conversation

@shaikcyber
Copy link
Copy Markdown

@shaikcyber shaikcyber commented Mar 11, 2026

Description

Please include a summary of the changes and the related issues. Please also include relevant motivation and context. List
any dependencies that are required for this change.

Problem

Solution

Fixes # (issue if any)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Chore (no code changes)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also
list any relevant details for your test configuration

  • Test A
  • Test B

Checklist:

  • My code follows the style guidelines of this project
  • My commit message/PR follows the contribution guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Other Information:

List any documentation updates that are needed for the Wiki

Summary by CodeRabbit

  • New Features

    • Added JWT-based authorization for protected downloads with JWKS caching and RS256 token validation.
    • Enforced tenant-based access control by verifying request domain and folder against tenant configuration.
    • Returns clear 401 responses for malformed, missing, or unauthorized tokens.
  • Tests

    • Added a local test harness to simulate CloudFront requests and validate authorization behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 11, 2026

Walkthrough

Adds a Lambda@Edge-style Node.js module under subsystem/auth-download/ that validates Bearer RS256 JWTs against cached Cognito JWKS, extracts a tenantId claim, verifies tenant S3/distribution config from DynamoDB, and returns the original request or a 401 Unauthorized response.

Changes

Cohort / File(s) Summary
Auth Handler Implementation
subsystem/auth-download/index.mjs
New Lambda@Edge handler: parses CloudFront request (domain, folder, Authorization header), fetches & caches Cognito JWKS, converts JWK→PEM, verifies RS256 JWT (checks token_use = id), extracts tenantId, queries DynamoDB tenant-config, compares s3_folder_name and distribution_domain_name, and returns the request or a 401 via an in-file unauthorized() helper.
Dependencies & Manifest
subsystem/auth-download/package.json
Adds package manifest (ESM) with metadata and dependencies: @aws-sdk/client-dynamodb, jsonwebtoken, jwk-to-pem.
Local Test Harness
subsystem/auth-download/test.mjs
Adds a local test script that imports handler from index.mjs, builds a CloudFront-like event (GET /download-report/report.csv, Host and Authorization header), invokes the handler, and logs the result.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant CF as CloudFront
    participant Handler as AuthHandler
    participant JWKS as CognitoJWKS
    participant DB as DynamoDB

    Client->>CF: GET /download-report/report.csv\nAuthorization: Bearer {token}
    CF->>Handler: Lambda@Edge event (request)
    Handler->>Handler: Parse domain, folder, Authorization header
    Handler->>JWKS: Fetch (or use cached) JWKS
    Handler->>Handler: Convert JWK -> PEM, verify JWT (RS256), check token_use, extract tenantId
    Handler->>DB: Query `tenant-config` for tenant_id
    Handler->>Handler: Compare s3_folder_name & distribution_domain_name with request
    alt valid
        Handler->>CF: Return original request (allow)
        CF->>Client: Serve resource
    else invalid
        Handler->>CF: Return 401 Unauthorized response
        CF->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hop through keys beneath the moonlit trees,
I fetch the JWKS on a midnight breeze.
I peek at tokens, nibble claims with care,
I ask the DB if tenants may be there.
A twitch — allow, a sniff — deny, I share. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is an empty template with no actual content filled in—no problem statement, solution, testing details, dependencies, or checklist items are completed. Complete the PR description by filling in: the Problem and Solution sections, listing dependencies, specifying what tests were performed, and checking relevant items in the Type of change section.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references a specific ticket (NEXT-1932) and describes the main change: an authorizer Lambda function for downloads.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/download-authorizer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@shaikcyber shaikcyber requested a review from jaysinh-cp March 11, 2026 16:53
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
subsystem/auth-download/index.mjs (3)

104-107: Null-safe access can mask missing configuration.

If s3_folder_name or distribution_domain_name is missing from the DynamoDB item, the comparison will be undefined === folderName, which fails silently. Consider explicit validation and logging for debugging.

Proposed improvement
     const s3FolderName = result.Item.s3_folder_name?.S
     const s3DistributionDomainName = result.Item.distribution_domain_name?.S
 
+    if (!s3FolderName || !s3DistributionDomainName) {
+        console.error("Tenant config incomplete:", { tenantId, s3FolderName, s3DistributionDomainName })
+        return false
+    }
+
     return (s3FolderName === folderName && s3DistributionDomainName === domainName)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 104 - 107, The current
equality check uses optional chaining (s3FolderName and
s3DistributionDomainName) which masks missing keys; update the logic that
computes s3FolderName and s3DistributionDomainName (reading from result.Item) to
explicitly validate presence (e.g., check result.Item.s3_folder_name and
result.Item.distribution_domain_name exist and their .S values are non-empty),
log a clear error or warning including the missing field names when absent, and
then return a deterministic false (or throw) instead of relying on undefined ===
folderName; ensure the final comparison still uses the validated
s3FolderName/s3DistributionDomainName variables against folderName and
domainName.

8-13: Hardcoded configuration should be externalized.

The Cognito pool ID, region, and table name are hardcoded. For Lambda@Edge, environment variables are not supported in viewer request/response triggers, but you can:

  1. Use origin request/response triggers which support environment variables
  2. Inject configuration at build/deployment time
  3. Use AWS Systems Manager Parameter Store with caching

The current approach makes it difficult to deploy across environments (dev/staging/prod).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 8 - 13, Replace the hardcoded
CONFIG object (region, jwksURL, tableName, accessClaim) with injected
configuration: read from environment variables (e.g., process.env.AWS_REGION,
process.env.COGNITO_JWKS_URL, process.env.TENANT_TABLE_NAME,
process.env.ACCESS_CLAIM) with sensible fallbacks, and add an alternative loader
that fetches values from AWS Systems Manager Parameter Store with an in-memory
TTL cache to support Lambda@Edge constraints; update any code using CONFIG (look
for references to CONFIG.region, CONFIG.jwksURL, CONFIG.tableName,
CONFIG.accessClaim) to call the new config loader so you can support build-time
injection or SSM-backed retrieval depending on deployment environment.

89-89: DynamoDB client instantiated on every request - consider reusing.

Creating a new DynamoDBClient instance per invocation adds latency. In standard Lambda, the client should be instantiated outside the handler for connection reuse. For Lambda@Edge, this optimization still applies.

Proposed fix
+const dynamoClient = new DynamoDBClient({ region: CONFIG.region })
+
 // Validate access from DynamoDB
 const validateAccess = async (tenantId, region, domainName, folderName) => {
-
-    const client = new DynamoDBClient({ region })
 
     const command = new GetItemCommand({
         TableName: CONFIG.tableName,
         Key: {
             tenant_id: { S: tenantId }
         }
     })
 
-    const result = await client.send(command)
+    const result = await dynamoClient.send(command)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` at line 89, The DynamoDBClient is being
created per request (const client = new DynamoDBClient({ region })) which
prevents connection reuse; move the instantiation to module scope so the single
client is reused across invocations—create the client once at top-level (using
the same region determination logic), remove the in-handler/new-per-request
creation, and ensure all handler code (that currently references client) uses
this top-level DynamoDBClient instance.
subsystem/auth-download/test.mjs (1)

21-26: Ensure real tokens are never committed.

The empty value: "" placeholder is correct. Consider adding a .gitignore entry or using environment variables to prevent accidental token commits during local testing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/test.mjs` around lines 21 - 26, The authorization
entry currently uses a literal placeholder value ("value: \"\"") which risks
accidental token commits; replace this with a runtime-sourced secret (e.g., read
from an environment variable such as process.env.AUTH_TOKEN or a local .env
loaded at test runtime) and ensure any local secrets file is added to
.gitignore; retain the authorization array and the "Authorization" key but
remove any real token literals so tests pull the token from the environment
instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 16-34: The cachedKeys object used by getPublicKeys currently never
expires, causing stale PEMs after key rotation; change cachedKeys to store an
entry object like {pems, fetchedAt} and add a configurable TTL (e.g.,
KEY_TTL_MS) at module scope, then in getPublicKeys check cachedKeys[jwksURL] and
if (Date.now() - entry.fetchedAt) < KEY_TTL_MS return entry.pems, otherwise
fetch new keys, build pems, and replace cachedKeys[jwksURL] with {pems,
fetchedAt: Date.now()}; ensure the code path for a cache miss and an expired
entry both perform the same fetch-and-store logic so keys are refreshed when TTL
has passed.
- Around line 22-23: The fetch and JSON parsing for JWKS (const res = await
fetch(jwksURL); const { keys } = await res.json()) lack error handling; wrap the
fetch/res.json logic in a try/catch, check res.ok before parsing (and handle
non-2xx responses), validate that the parsed object contains a keys array before
using it, and on any failure log a clear, non-sensitive error and return/throw a
controlled error so the Lambda doesn't crash or leak stack traces; reference
jwksURL, res, res.json, and keys when implementing these checks and logs.
- Around line 50-52: The code verifies the token and checks decoded.token_use
but omits issuer validation; add a check after jwt.verify (the decoded variable)
to ensure decoded.iss exactly matches the expected issuer URL constructed from
your CONFIG (use CONFIG.region and a new CONFIG.userPoolId) e.g.
"https://cognito-idp.{region}.amazonaws.com/{userPoolId}", returning null (or
rejecting) if it does not match; update CONFIG to include userPoolId and use
that in the validation near the existing jwt.verify/decoded.token_use logic.

In `@subsystem/auth-download/package.json`:
- Line 5: The package.json "main" entry currently points to "index.js" but the
package's actual module file is "index.mjs"; update the "main" field in
package.json to "index.mjs" and ensure the package's module type is declared
(add or verify "type": "module") so Node resolves ES modules correctly when
importing the package.

---

Nitpick comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 104-107: The current equality check uses optional chaining
(s3FolderName and s3DistributionDomainName) which masks missing keys; update the
logic that computes s3FolderName and s3DistributionDomainName (reading from
result.Item) to explicitly validate presence (e.g., check
result.Item.s3_folder_name and result.Item.distribution_domain_name exist and
their .S values are non-empty), log a clear error or warning including the
missing field names when absent, and then return a deterministic false (or
throw) instead of relying on undefined === folderName; ensure the final
comparison still uses the validated s3FolderName/s3DistributionDomainName
variables against folderName and domainName.
- Around line 8-13: Replace the hardcoded CONFIG object (region, jwksURL,
tableName, accessClaim) with injected configuration: read from environment
variables (e.g., process.env.AWS_REGION, process.env.COGNITO_JWKS_URL,
process.env.TENANT_TABLE_NAME, process.env.ACCESS_CLAIM) with sensible
fallbacks, and add an alternative loader that fetches values from AWS Systems
Manager Parameter Store with an in-memory TTL cache to support Lambda@Edge
constraints; update any code using CONFIG (look for references to CONFIG.region,
CONFIG.jwksURL, CONFIG.tableName, CONFIG.accessClaim) to call the new config
loader so you can support build-time injection or SSM-backed retrieval depending
on deployment environment.
- Line 89: The DynamoDBClient is being created per request (const client = new
DynamoDBClient({ region })) which prevents connection reuse; move the
instantiation to module scope so the single client is reused across
invocations—create the client once at top-level (using the same region
determination logic), remove the in-handler/new-per-request creation, and ensure
all handler code (that currently references client) uses this top-level
DynamoDBClient instance.

In `@subsystem/auth-download/test.mjs`:
- Around line 21-26: The authorization entry currently uses a literal
placeholder value ("value: \"\"") which risks accidental token commits; replace
this with a runtime-sourced secret (e.g., read from an environment variable such
as process.env.AUTH_TOKEN or a local .env loaded at test runtime) and ensure any
local secrets file is added to .gitignore; retain the authorization array and
the "Authorization" key but remove any real token literals so tests pull the
token from the environment instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3280b900-82b3-4777-8863-6697302dae3d

📥 Commits

Reviewing files that changed from the base of the PR and between 22a1a7a and 9459a55.

📒 Files selected for processing (3)
  • subsystem/auth-download/index.mjs
  • subsystem/auth-download/package.json
  • subsystem/auth-download/test.mjs

Comment on lines +16 to +34
async function getPublicKeys(jwksURL) {

if (cachedKeys[jwksURL]) {
return cachedKeys[jwksURL]
}

const res = await fetch(jwksURL)
const { keys } = await res.json()

const pems = {}

keys.forEach(k => {
pems[k.kid] = jwkToPem(k)
})

cachedKeys[jwksURL] = pems

return pems
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JWKS cache never expires - stale keys can cause auth failures.

The cachedKeys object caches JWKS indefinitely. When Cognito rotates signing keys, cached PEMs become stale, causing valid tokens to be rejected. Implement TTL-based cache invalidation.

Proposed fix with TTL
-let cachedKeys = {}
+let cachedKeys = {}
+let cacheTimestamp = 0
+const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour
 
 // Fetch Cognito JWKS
 async function getPublicKeys(jwksURL) {
+    const now = Date.now()
+    if (cachedKeys[jwksURL] && (now - cacheTimestamp) < CACHE_TTL_MS) {
+        return cachedKeys[jwksURL]
+    }
 
-    if (cachedKeys[jwksURL]) {
-        return cachedKeys[jwksURL]
-    }
-
     const res = await fetch(jwksURL)
     const { keys } = await res.json()
 
     const pems = {}
 
     keys.forEach(k => {
         pems[k.kid] = jwkToPem(k)
     })
 
     cachedKeys[jwksURL] = pems
+    cacheTimestamp = now
 
     return pems
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 16 - 34, The cachedKeys
object used by getPublicKeys currently never expires, causing stale PEMs after
key rotation; change cachedKeys to store an entry object like {pems, fetchedAt}
and add a configurable TTL (e.g., KEY_TTL_MS) at module scope, then in
getPublicKeys check cachedKeys[jwksURL] and if (Date.now() - entry.fetchedAt) <
KEY_TTL_MS return entry.pems, otherwise fetch new keys, build pems, and replace
cachedKeys[jwksURL] with {pems, fetchedAt: Date.now()}; ensure the code path for
a cache miss and an expired entry both perform the same fetch-and-store logic so
keys are refreshed when TTL has passed.

Comment thread subsystem/auth-download/index.mjs Outdated
Comment thread subsystem/auth-download/index.mjs Outdated
Comment thread subsystem/auth-download/package.json Outdated
"name": "lambdanode",
"version": "1.0.0",
"description": "",
"main": "index.js",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mismatch between main entry and actual module file.

The main field points to index.js, but the actual handler is in index.mjs. For ES modules, this should be updated.

Proposed fix
-  "main": "index.js",
+  "main": "index.mjs",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"main": "index.js",
"main": "index.mjs",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/package.json` at line 5, The package.json "main"
entry currently points to "index.js" but the package's actual module file is
"index.mjs"; update the "main" field in package.json to "index.mjs" and ensure
the package's module type is declared (add or verify "type": "module") so Node
resolves ES modules correctly when importing the package.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
subsystem/auth-download/index.mjs (1)

87-98: Reuse DynamoDBClient across invocations.

Creating a new SDK client at Line 89 for every request adds avoidable latency to the authorizer hot path and drops connection reuse. Hoist it to module scope once the config is finalized.

♻️ Suggested refactor
 let cachedKeys = {}
+let dynamoClient
@@
 const validateAccess = async (tenantId, region, domainName, folderName) => {
-
-    const client = new DynamoDBClient({ region })
+    dynamoClient ??= new DynamoDBClient({ region })
@@
-    const result = await client.send(command)
+    const result = await dynamoClient.send(command)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 87 - 98, The validateAccess
function creates a new DynamoDBClient on every call which prevents connection
reuse; move the DynamoDBClient instantiation out of validateAccess to module
scope (after CONFIG is finalized) so a single client instance is reused, update
references to use the hoisted client (replace local client variable in
validateAccess), and ensure any config-dependent initialization happens once
before the function is invoked; keep the existing GetItemCommand usage and key
construction (tenant_id) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 8-13: The CONFIG constant is hardcoded with empty strings (region,
jwksURL, tableName) causing authorization to always fail; update the module to
populate CONFIG from environment or a config source and validate it at startup:
read process.env (e.g., REGION, JWKS_URL, TABLE_NAME) into CONFIG.region,
CONFIG.jwksURL, CONFIG.tableName (or load from SSM/Parameter Store if
preferred), add a startup check that logs and exits or throws when any required
CONFIG field is missing, and ensure validateAccess() and the request handler use
these populated values instead of empty strings so DynamoDB queries and JWKS
validation run correctly.
- Around line 8-13: The code currently builds CONFIG without an audience and
never validates the Cognito token's aud claim; add a new CONFIG field (e.g.,
clientId or audience) and pass it as the audience option to jsonwebtoken.verify
(or verify from jsonwebtoken) so verify enforces the token's aud matches your
expected app client ID. Update any code that calls jsonwebtoken.verify(token,
key, ...) to include { audience: CONFIG.clientId } (or the chosen field name)
and ensure tests/consumers provide the correct client ID in CONFIG.

---

Nitpick comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 87-98: The validateAccess function creates a new DynamoDBClient on
every call which prevents connection reuse; move the DynamoDBClient
instantiation out of validateAccess to module scope (after CONFIG is finalized)
so a single client instance is reused, update references to use the hoisted
client (replace local client variable in validateAccess), and ensure any
config-dependent initialization happens once before the function is invoked;
keep the existing GetItemCommand usage and key construction (tenant_id) intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4d3c30e1-fe0a-4aa0-b871-969f6765c3be

📥 Commits

Reviewing files that changed from the base of the PR and between 9459a55 and 9d49bf1.

📒 Files selected for processing (2)
  • subsystem/auth-download/index.mjs
  • subsystem/auth-download/test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • subsystem/auth-download/test.mjs

Comment on lines +8 to +13
const CONFIG = {
region: "",
jwksURL: "",
tableName: "",
accessClaim: "custom:accessId"
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "### auth-download files ###"
fd -t f . subsystem/auth-download | while read -r f; do
  printf '\n--- %s ---\n' "$f"
  sed -n '1,240p' "$f"
done

echo
echo "### Look for config injection / templating references ###"
rg -n -C2 'auth-download|jwksURL|tableName|custom:accessId|region:\s*""|jwksURL:\s*""|tableName:\s*""|envsubst|templatefile|replace|terraform|cloudformation|serverless' .

Repository: PaladinCloud/CE

Length of output: 50372


CONFIG object has hardcoded empty strings that prevent any authorization.

The CONFIG object (lines 8-13) declares region, jwksURL, and tableName as empty strings with no mechanism to populate them. The handler returns 401 for every request at line 117-119 because CONFIG.region and CONFIG.jwksURL are falsy. Even if those were somehow set, CONFIG.tableName remains empty and would cause validateAccess() to fail at line 92 when querying DynamoDB.

There is no code reading from environment variables, configuration files, or any other external source to hydrate these values. Either:

  1. Add code to read configuration from environment variables or AWS Systems Manager Parameter Store, or
  2. Accept that this module requires a deployment-time code injection mechanism and document it explicitly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 8 - 13, The CONFIG constant
is hardcoded with empty strings (region, jwksURL, tableName) causing
authorization to always fail; update the module to populate CONFIG from
environment or a config source and validate it at startup: read process.env
(e.g., REGION, JWKS_URL, TABLE_NAME) into CONFIG.region, CONFIG.jwksURL,
CONFIG.tableName (or load from SSM/Parameter Store if preferred), add a startup
check that logs and exits or throws when any required CONFIG field is missing,
and ensure validateAccess() and the request handler use these populated values
instead of empty strings so DynamoDB queries and JWKS validation run correctly.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Amazon Cognito place the app client ID in the audclaim for ID tokens, and doesjsonwebtoken.verify()support enforcing that with theaudience option?

💡 Result:

Yes.

  • Amazon Cognito (User Pools) ID tokens include an aud claim whose value should match the app client ID (the client that requested the token). [1]
  • jsonwebtoken.verify() supports enforcing this via the audience option, which validates the token’s aud claim (and fails verification on mismatch). [2]

Sources:
[1] AWS Cognito Developer Guide – Verifying a JWT (ID token aud matches app client ID) (docs.aws.amazon.com)
[2] auth0/node-jsonwebtoken README – jwt.verify(..., { audience: ... }) (github.com)

Citations:


🏁 Script executed:

cat -n subsystem/auth-download/index.mjs | head -60

Repository: PaladinCloud/CE

Length of output: 1762


Bind JWT verification to the expected app client.

The code accepts any valid Cognito ID token from the same pool because it never validates the aud claim. Every ID token from Cognito includes aud set to the app client ID, and jsonwebtoken.verify() can enforce this. Add audience validation to prevent tokens minted for different app clients from being accepted.

Suggested fix
 const CONFIG  = {
     region: "",
     jwksURL: "",
     tableName: "",
+    audience: "",
     accessClaim: "custom:accessId"
 }
 
-        const decoded = jwt.verify(token, pem, { algorithms: ['RS256'] })
+        const decoded = jwt.verify(token, pem, {
+            algorithms: ['RS256'],
+            audience: CONFIG.audience
+        })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 8 - 13, The code currently
builds CONFIG without an audience and never validates the Cognito token's aud
claim; add a new CONFIG field (e.g., clientId or audience) and pass it as the
audience option to jsonwebtoken.verify (or verify from jsonwebtoken) so verify
enforces the token's aud matches your expected app client ID. Update any code
that calls jsonwebtoken.verify(token, key, ...) to include { audience:
CONFIG.clientId } (or the chosen field name) and ensure tests/consumers provide
the correct client ID in CONFIG.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
subsystem/auth-download/index.mjs (2)

51-53: ⚠️ Potential issue | 🟠 Major

Bind JWT verification to the expected app client.

This still accepts any valid ID token from the same user pool because aud is never checked. The Go authorizer already validates audience in subsystem/auth/services/svc-api-authorizer/utils/jwt/utils_jwt.go:139-146; this handler should do the same.

Suggested fix
 const CONFIG  = {
     region: process.env.REGION ?? "",
     jwksURL: process.env.JWKS_URL ?? "",
     issuer: process.env.ISSUER ?? "",
+    audience: process.env.AUDIENCE ?? "",
     tableName: process.env.TABLE_NAME ?? "",
     accessClaim: "custom:accessId"
 }
 
-        const decoded = jwt.verify(token, pem, { algorithms: ['RS256'], issuer: CONFIG.issuer })
+        const decoded = jwt.verify(token, pem, {
+            algorithms: ['RS256'],
+            issuer: CONFIG.issuer,
+            audience: CONFIG.audience
+        })
Does jsonwebtoken.verify() support enforcing the JWT `aud` claim via the `audience` option, and do Amazon Cognito ID tokens set `aud` to the app client ID?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 51 - 53, The JWT verification
currently only checks token_use; update the jwt.verify call to enforce the
expected audience by passing the audience option (e.g., jwt.verify(token, pem, {
algorithms: ['RS256'], issuer: CONFIG.issuer, audience: CONFIG.clientId })) or,
if you prefer explicit post-checking, after decoding verify decoded.aud ===
CONFIG.clientId in addition to decoded.token_use === 'id'; use the existing
symbols jwt.verify, token, pem, CONFIG.clientId and
decoded.token_use/decoded.aud to locate and implement the change.

8-14: ⚠️ Potential issue | 🔴 Critical

Hydrate and validate all required config before serving requests.

CONFIG is still initialized with empty strings, and the handler only guards region/jwksURL. With jwt.verify(..., { issuer: CONFIG.issuer }), an empty issuer rejects every valid token, and an empty tableName pushes the DynamoDB path into a runtime failure instead of a clear startup error.

Suggested fix
 const CONFIG  = {
-    region: "",
-    jwksURL: "",
-    issuer: "",
-    tableName: "",
+    region: process.env.REGION ?? "",
+    jwksURL: process.env.JWKS_URL ?? "",
+    issuer: process.env.ISSUER ?? "",
+    tableName: process.env.TABLE_NAME ?? "",
     accessClaim: "custom:accessId"
 }
+
+const REQUIRED_CONFIG_KEYS = ["region", "jwksURL", "issuer", "tableName"]
 
 // Lambda@Edge handler
 export const handler = async (event) => {
     try {
-        if (!CONFIG.region || !CONFIG.jwksURL) {
+        const missing = REQUIRED_CONFIG_KEYS.filter((key) => !CONFIG[key])
+        if (missing.length) {
+            console.error("Missing auth-download config:", missing.join(", "))
             return unauthorized()
         }

Also applies to: 118-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 8 - 14, CONFIG is being
initialized with empty strings and not fully validated, causing jwt.verify(...,
{ issuer: CONFIG.issuer }) to always fail and DynamoDB usage to error at
runtime; update startup to hydrate CONFIG from env/config source and validate
required fields (at minimum region, jwksURL, issuer, tableName) before serving
requests, throwing a clear startup error if any are missing or empty, and ensure
code paths that call jwt.verify (using CONFIG.issuer) and DynamoDB access (using
CONFIG.tableName) rely on the validated CONFIG; also consider validating
accessClaim if your code expects a specific format.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 88-99: CONFIG is declared but never populated from environment
variables, causing CONFIG.tableName, CONFIG.region, and CONFIG.jwksURL to be
empty when validateAccess and other functions run; populate CONFIG at module
initialization (before validateAccess is used) by reading from process.env
(e.g., TABLE_NAME, REGION, JWKS_URL) and assign to CONFIG.tableName,
CONFIG.region, CONFIG.jwksURL (with optional defaults or throw if missing) so
that validateAccess and DynamoDBClient creation use the real values.

---

Duplicate comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 51-53: The JWT verification currently only checks token_use;
update the jwt.verify call to enforce the expected audience by passing the
audience option (e.g., jwt.verify(token, pem, { algorithms: ['RS256'], issuer:
CONFIG.issuer, audience: CONFIG.clientId })) or, if you prefer explicit
post-checking, after decoding verify decoded.aud === CONFIG.clientId in addition
to decoded.token_use === 'id'; use the existing symbols jwt.verify, token, pem,
CONFIG.clientId and decoded.token_use/decoded.aud to locate and implement the
change.
- Around line 8-14: CONFIG is being initialized with empty strings and not fully
validated, causing jwt.verify(..., { issuer: CONFIG.issuer }) to always fail and
DynamoDB usage to error at runtime; update startup to hydrate CONFIG from
env/config source and validate required fields (at minimum region, jwksURL,
issuer, tableName) before serving requests, throwing a clear startup error if
any are missing or empty, and ensure code paths that call jwt.verify (using
CONFIG.issuer) and DynamoDB access (using CONFIG.tableName) rely on the
validated CONFIG; also consider validating accessClaim if your code expects a
specific format.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 22251242-f21f-4488-891e-0d30d573c853

📥 Commits

Reviewing files that changed from the base of the PR and between 9d49bf1 and a43ed76.

📒 Files selected for processing (2)
  • subsystem/auth-download/index.mjs
  • subsystem/auth-download/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • subsystem/auth-download/package.json

Comment on lines +88 to +99
const validateAccess = async (tenantId, region, domainName, folderName) => {

const client = new DynamoDBClient({ region })

const command = new GetItemCommand({
TableName: CONFIG.tableName,
Key: {
tenant_id: { S: tenantId }
}
})

const result = await client.send(command)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "### auth-download deployment references ###"
rg -n -C2 'auth-download|tenant-config|TABLE_NAME|DYNAMODB_TABLE_NAME|requireDynamoDB|GetItemCommand|DynamoDBClient|JWKS_URL|ISSUER|AUDIENCE' .

echo
echo "### manifests mentioning auth-download or DynamoDB ###"
fd 'package\.json|serverless|template|yaml|yml|tf|json' . | while read -r f; do
  if rg -q 'auth-download|tenant-config|DynamoDB|TABLE_NAME|requireDynamoDB' "$f"; then
    printf '\n--- %s ---\n' "$f"
    sed -n '1,220p' "$f"
  fi
done

Repository: PaladinCloud/CE

Length of output: 41496


🏁 Script executed:

#!/bin/bash
# Search for auth-download deployment config
echo "### Files in auth-download directory ###"
fd . subsystem/auth-download/ --type f

echo
echo "### package.json or serverless config in auth-download ###"
cat subsystem/auth-download/package.json 2>/dev/null || echo "No package.json found"

echo
echo "### Checking for TABLE_NAME or DYNAMODB config in auth-download directory ###"
rg -n 'TABLE_NAME|DYNAMODB_TABLE|CONFIG\.' subsystem/auth-download/ || echo "No matches"

echo
echo "### Full content of auth-download index.mjs ###"
head -120 subsystem/auth-download/index.mjs | tail -40

Repository: PaladinCloud/CE

Length of output: 2376


🏁 Script executed:

#!/bin/bash
# Check where CONFIG is initialized in index.mjs
echo "### CONFIG initialization in auth-download/index.mjs ###"
head -60 subsystem/auth-download/index.mjs

echo
echo "### Search for environment variable loading ###"
rg -n 'process\.env|CONFIG\s*=' subsystem/auth-download/index.mjs

Repository: PaladinCloud/CE

Length of output: 1506


🏁 Script executed:

#!/bin/bash
# Check entire file for CONFIG population or environment variable references
echo "### Entire auth-download/index.mjs file ###"
cat subsystem/auth-download/index.mjs

Repository: PaladinCloud/CE

Length of output: 4086


CONFIG object is never initialized from environment variables; this prevents deployment.

The code initializes CONFIG as an object with empty string values (lines 8-13) but contains no code to populate these properties from environment variables. When the handler executes, CONFIG.tableName, CONFIG.region, and CONFIG.jwksURL remain empty strings, causing the DynamoDB query at line 93 and the validation check at line 118 to fail. Add code to read environment variables into CONFIG (e.g., via process.env.TABLE_NAME) before the handler is invoked, or configure these values through Lambda initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 88 - 99, CONFIG is declared
but never populated from environment variables, causing CONFIG.tableName,
CONFIG.region, and CONFIG.jwksURL to be empty when validateAccess and other
functions run; populate CONFIG at module initialization (before validateAccess
is used) by reading from process.env (e.g., TABLE_NAME, REGION, JWKS_URL) and
assign to CONFIG.tableName, CONFIG.region, CONFIG.jwksURL (with optional
defaults or throw if missing) so that validateAccess and DynamoDBClient creation
use the real values.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
subsystem/auth-download/index.mjs (1)

20-48: ⚠️ Potential issue | 🔴 Critical

Cache TTL is broken: cacheTimestamp is never updated after fetching keys.

The TTL check at line 23 uses cacheTimestamp, but after successfully fetching and caching keys (lines 45-47), cacheTimestamp is never updated from its initial value of 0. This means:

  1. cachedKeys[jwksURL] gets populated
  2. cacheTimestamp stays 0
  3. On next call: (now - 0) is always >> CACHE_TTL_MS, so the condition fails
  4. Keys are re-fetched on every invocation, negating the cache entirely
Proposed fix
     cachedKeys[jwksURL] = pems
+    cacheTimestamp = now
 
     return pems
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 20 - 48, The cache TTL never
works because cacheTimestamp is never updated after getPublicKeys fetches keys;
fix by recording the fetch time when you populate cachedKeys so the TTL check
uses a fresh timestamp—either set cacheTimestamp = Date.now() immediately after
assigning cachedKeys[jwksURL] in getPublicKeys or (better) maintain a per-URL
timestamp map (e.g., cacheTimestamps[jwksURL] = Date.now()) and update the code
that checks CACHE_TTL_MS to use that per-URL timestamp; ensure you reference and
update the same timestamp variable you use in the initial TTL comparison.
🧹 Nitpick comments (1)
subsystem/auth-download/index.mjs (1)

100-106: DynamoDBClient instantiated on every invocation degrades performance.

Creating a new DynamoDBClient per request adds latency and connection overhead. Lambda@Edge reuses execution contexts across warm invocations, so the client should be instantiated once at module scope to benefit from connection reuse.

Proposed fix (after CONFIG is populated from env vars)
 import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"
 
 let cachedKeys = {}
 let cacheTimestamp = 0
 const CACHE_TTL_MS = 60 * 60 * 1000 // 1 hour
+
+// Initialize client at module scope for connection reuse
+let dynamoClient = null
+const getDynamoClient = (region) => {
+    if (!dynamoClient) {
+        dynamoClient = new DynamoDBClient({ region })
+    }
+    return dynamoClient
+}
 
 // ...
 
 const validateAccess = async (tenantId, region, domainName, folderName) => {
-    const client = new DynamoDBClient({ region })
+    const client = getDynamoClient(region)
 
     const command = new GetItemCommand({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 100 - 106, The DynamoDBClient
is being created inside validateAccess on every invocation; move the client
instantiation to module scope so Lambda warm starts can reuse the connection.
Create a single shared DynamoDBClient (e.g., const dynamoClient = new
DynamoDBClient({ region: CONFIG.region || process.env.AWS_REGION })) after
CONFIG is populated from env vars, then update validateAccess to use that shared
dynamoClient instead of new DynamoDBClient({ region }) while keeping
GetItemCommand and existing parameter usage intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 131-133: The initial config check currently returns unauthorized()
when CONFIG.region or CONFIG.jwksURL are missing but omits CONFIG.tableName;
update the validation to also verify CONFIG.tableName is present (non-empty)
before proceeding so validateAccess() won't attempt a DynamoDB query with a
missing table name. Locate the validation block that checks CONFIG.region and
CONFIG.jwksURL and add a check for CONFIG.tableName (used by validateAccess())
and return unauthorized() if it's missing.

---

Duplicate comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 20-48: The cache TTL never works because cacheTimestamp is never
updated after getPublicKeys fetches keys; fix by recording the fetch time when
you populate cachedKeys so the TTL check uses a fresh timestamp—either set
cacheTimestamp = Date.now() immediately after assigning cachedKeys[jwksURL] in
getPublicKeys or (better) maintain a per-URL timestamp map (e.g.,
cacheTimestamps[jwksURL] = Date.now()) and update the code that checks
CACHE_TTL_MS to use that per-URL timestamp; ensure you reference and update the
same timestamp variable you use in the initial TTL comparison.

---

Nitpick comments:
In `@subsystem/auth-download/index.mjs`:
- Around line 100-106: The DynamoDBClient is being created inside validateAccess
on every invocation; move the client instantiation to module scope so Lambda
warm starts can reuse the connection. Create a single shared DynamoDBClient
(e.g., const dynamoClient = new DynamoDBClient({ region: CONFIG.region ||
process.env.AWS_REGION })) after CONFIG is populated from env vars, then update
validateAccess to use that shared dynamoClient instead of new DynamoDBClient({
region }) while keeping GetItemCommand and existing parameter usage intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: de10f845-d9bf-4627-ad01-c0d782b38741

📥 Commits

Reviewing files that changed from the base of the PR and between a43ed76 and 988c621.

📒 Files selected for processing (1)
  • subsystem/auth-download/index.mjs

Comment on lines +131 to +133
if (!CONFIG.region || !CONFIG.jwksURL) {
return unauthorized()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add CONFIG.tableName to the initial configuration validation.

The check validates CONFIG.region and CONFIG.jwksURL but omits CONFIG.tableName, which is required by validateAccess() at line 106. If tableName is empty, the DynamoDB query will fail with an error instead of returning a clean 401.

Proposed fix
-        if (!CONFIG.region || !CONFIG.jwksURL) {
+        if (!CONFIG.region || !CONFIG.jwksURL || !CONFIG.tableName) {
             return unauthorized()
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!CONFIG.region || !CONFIG.jwksURL) {
return unauthorized()
}
if (!CONFIG.region || !CONFIG.jwksURL || !CONFIG.tableName) {
return unauthorized()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@subsystem/auth-download/index.mjs` around lines 131 - 133, The initial config
check currently returns unauthorized() when CONFIG.region or CONFIG.jwksURL are
missing but omits CONFIG.tableName; update the validation to also verify
CONFIG.tableName is present (non-empty) before proceeding so validateAccess()
won't attempt a DynamoDB query with a missing table name. Locate the validation
block that checks CONFIG.region and CONFIG.jwksURL and add a check for
CONFIG.tableName (used by validateAccess()) and return unauthorized() if it's
missing.

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.

2 participants