Skip to content

feat(auth): improve error messaging with combined strategy failure summary#2698

Open
amikofalvy wants to merge 2 commits intomainfrom
implement/auth-error-messaging
Open

feat(auth): improve error messaging with combined strategy failure summary#2698
amikofalvy wants to merge 2 commits intomainfrom
implement/auth-error-messaging

Conversation

@amikofalvy
Copy link
Collaborator

Summary

  • Normalize tryTempJwtAuth, tryBypassAuth, and tryApiKeyAuth return types from AuthResult | null to AuthAttempt with descriptive failure messages
  • Modify authenticateRequest() to collect failure reasons from all attempted strategies and return a combined summary message
  • Add DEBUG-level structured logging when all auth strategies are exhausted

Before: Invalid team agent token: signature verification failed (only the last strategy's error)
After: Authentication failed. Tried: JWT temp token (not a JWT), bypass secret (no match), Slack user JWT (not a Slack token), API key (not found), team agent token (signature verification failed)

Test plan

  • Updated 3 existing test assertions to match new combined error format
  • Added new test: Slack definitive failure short-circuits (returns Slack-specific error, not combined)
  • All 47 auth tests pass (33 api-key-auth + 14 app-credential-auth)
  • Typecheck passes
  • Lint passes
  • App credential auth path unchanged
  • HTTPException short-circuits preserved (403, 503)

Closes ENG-6268

🤖 Generated with Claude Code

amikofalvy and others added 2 commits March 15, 2026 00:58
All auth strategies now return AuthAttempt instead of bare AuthResult|null,
providing failure reasons. authenticateRequest() collects failures from all
strategies and returns a combined summary message when all fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update existing tests to assert the new combined error message format.
Add test verifying Slack definitive failure short-circuits without
trying further strategies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Mar 15, 2026 8:09am
agents-manage-ui Ready Ready Preview, Comment Mar 15, 2026 8:09am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Mar 15, 2026 8:09am

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Mar 15, 2026

⚠️ No Changeset found

Latest commit: 2e354f5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

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

Functionally correct — the Slack failure path logic, type consistency across all five strategies, and test coverage all look good. One security concern worth addressing before merge.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

Comment on lines +667 to +670
const summary =
failures.length > 0
? `Authentication failed. Tried: ${failures.map((f) => `${f.strategy} (${f.reason})`).join(', ')}`
: 'Authentication failed: no valid credentials found';
Copy link
Contributor

Choose a reason for hiding this comment

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

Information leakage in 401 response: The combined error message exposes internal infrastructure details to external API consumers — e.g., no bypass secret configured, no public key configured, strategy names like bypass secret. An attacker can fingerprint deployment configuration and identify which auth mechanisms are enabled/disabled.

Consider using generic failure reasons in the user-facing message (e.g., not applicable) while keeping the detailed reasons only in the logger.debug call on line 672, where they're already logged.

Comment on lines +667 to +670
const summary =
failures.length > 0
? `Authentication failed. Tried: ${failures.map((f) => `${f.strategy} (${f.reason})`).join(', ')}`
: 'Authentication failed: no valid credentials found';
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: the failures.length > 0 else branch ('Authentication failed: no valid credentials found') is dead code. At this point apiKey is guaranteed truthy (line 632), so tryTempJwtAuth always pushes at least one failure. Low priority but worth removing for clarity.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(4) Total Issues | Risk: Low

🟡 Minor (3) 🟡

🟡 1) runAuth.ts:454 & runAuth.ts:171 Info leakage in configuration-dependent failure messages

files: runAuth.ts:454, runAuth.ts:171

Issue: Failure messages like 'no bypass secret configured' and 'no public key configured' reveal whether specific environment variables are set, informing attackers about your auth configuration.

Why: While low-severity (doesn't enable direct exploitation), revealing configuration state helps attackers narrow down which authentication vectors are available and which are not configured. This follows an anti-pattern in authentication error handling. Compare with 'no match' at line 458 which correctly indicates the credential was wrong without revealing config state.

Fix: Use uniform failure messages that don't distinguish between "not configured" and "no match" states:

  • 'no bypass secret configured''not applicable'
  • 'no public key configured''not applicable'

The DEBUG-level log at line 672 already captures detailed failure reasons for operators who need them.

Refs:

🟡 2) api-key-auth.test.ts:1075 Slack test assertion doesn't verify full error format

Issue: The new test "should short-circuit with Slack-specific error when token is identified as Slack but fails verification" only checks that the body contains 'Invalid Slack user token' but doesn't verify the specific error reason ('signature verification failed') is included.

Why: If the error message format changes to omit the specific error reason, users would lose diagnostic information about why their Slack token failed, making debugging harder.

Fix: Strengthen the assertion to verify the complete error message:

expect(body).toContain('Invalid Slack user token: signature verification failed');

Refs:

Inline Comments:

  • 🟡 Minor: runAuth.ts:454 Info leakage - bypass secret config state
  • 🟡 Minor: runAuth.ts:171 Info leakage - public key config state
  • 🟡 Minor: api-key-auth.test.ts:1073-1076 Test doesn't verify full error format

💭 Consider (1) 💭

💭 1) runAuth.ts:650-653 Asymmetric Slack auth handling breaks normalization pattern

Issue: trySlackUserJwtAuth returns { authResult: null } without a failureMessage when the token isn't a Slack token (line 276), unlike all other strategies that were normalized to return failure messages. This requires special handling with a hardcoded reason at line 653.

Why: The PR's stated goal was to normalize auth strategy return types to AuthAttempt with failure messages. This creates a coupling where the caller knows implementation details of the callee.

Fix: Consider modifying trySlackUserJwtAuth line 276 to return { authResult: null, failureMessage: 'not a Slack token' }, then handle it uniformly like other strategies.

Refs:

Inline Comments:

  • 💭 Consider: runAuth.ts:650-653 Asymmetric Slack handling

💡 APPROVE WITH SUGGESTIONS

Summary: This PR successfully improves auth error messaging by normalizing return types and combining strategy failures into a helpful summary. The implementation is clean and well-tested. The minor findings above are suggestions for hardening (info leakage) and consistency (Slack handling symmetry) — none are blocking. The Slack short-circuit behavior is correctly preserved for definitive failures. Good work! 🎉

Discarded (5)
Location Issue Reason Discarded
runAuth.ts:667-674 Combined error reveals full auth strategy enumeration Accepted product decision for debugging; INFO level, not actionable
runAuth.ts:191 Removed clarifying comments reduce maintainability AGENTS.md states "No Comments" guideline; cleanup is acceptable
api-key-auth.test.ts:149 Missing JWT verification failure test path Low confidence; existing tests provide adequate coverage
api-key-auth.test.ts:967 Missing short-circuit assertion for missing headers The test validates correct behavior; additional assertion is optional
Strategy capitalization Inconsistent casing ("bypass secret" vs "API key") Cosmetic nitpick; doesn't affect functionality
Reviewers (6)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-security-iam 2 1 0 0 2 0 1
pr-review-tests 5 1 0 0 1 0 3
pr-review-consistency 4 0 1 0 1 0 2
pr-review-precision 2 0 0 0 0 0 2
pr-review-errors 1 0 0 0 0 0 1
pr-review-standards 0 0 0 0 0 0 0
Total 14 2 1 0 4 0 9

function tryBypassAuth(apiKey: string, reqData: RequestData): AuthAttempt {
if (!env.INKEEP_AGENTS_RUN_API_BYPASS_SECRET) {
return null;
return { authResult: null, failureMessage: 'no bypass secret configured' };
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Minor: Info leakage - failure message reveals server configuration state

Issue: The failure message 'no bypass secret configured' reveals whether the INKEEP_AGENTS_RUN_API_BYPASS_SECRET environment variable is set, informing attackers that this auth path is unavailable.

Why: While low severity (doesn't enable direct exploitation), revealing configuration state helps attackers narrow down which authentication vectors are available. Compare with line 458 which correctly uses 'no match' without revealing config state.

Fix: Use a uniform failure message that doesn't distinguish between "not configured" and "no match":

Suggested change
return { authResult: null, failureMessage: 'no bypass secret configured' };
return { authResult: null, failureMessage: 'not applicable' };

Refs:

}

if (!env.INKEEP_AGENTS_TEMP_JWT_PUBLIC_KEY) {
return { authResult: null, failureMessage: 'no public key configured' };
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Minor: Info leakage - failure message reveals server configuration state

Issue: The failure message 'no public key configured' reveals whether INKEEP_AGENTS_TEMP_JWT_PUBLIC_KEY is set.

Why: Same concern as the bypass secret — revealing configuration state helps attackers understand your auth setup.

Fix: Use a generic message:

Suggested change
return { authResult: null, failureMessage: 'no public key configured' };
return { authResult: null, failureMessage: 'not applicable' };

Refs:

Comment on lines 650 to +653
const slackAttempt = await trySlackUserJwtAuth(apiKey, reqData);
if (slackAttempt.authResult) return { authResult: slackAttempt.authResult };
if (slackAttempt.authResult) return slackAttempt;
if (slackAttempt.failureMessage) return slackAttempt;
failures.push({ strategy: 'Slack user JWT', reason: 'not a Slack token' });
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 Consider: Asymmetric Slack auth handling breaks normalization pattern

Issue: trySlackUserJwtAuth returns { authResult: null } without a failureMessage when the token isn't a Slack token (line 276), unlike all other strategies that were normalized to return failure messages. This requires special handling here with a hardcoded reason.

Why: This creates a coupling where the caller knows implementation details of the callee, and breaks the symmetry of the PR's normalization goal.

Fix: Consider modifying trySlackUserJwtAuth line 276 to return:

return { authResult: null, failureMessage: 'not a Slack token' };

Then this block can follow the same pattern as other strategies:

const slackAttempt = await trySlackUserJwtAuth(apiKey, reqData);
if (slackAttempt.authResult) return slackAttempt;
if (slackAttempt.failureMessage) {
  // Definitive Slack failures (token IS Slack but invalid) still short-circuit
  if (isSlackUserToken(apiKey)) return slackAttempt;
  failures.push({ strategy: 'Slack user JWT', reason: slackAttempt.failureMessage });
}

Refs:

Comment on lines +1073 to +1076
expect(res.status).toBe(401);
const body = await res.text();
expect(body).toContain('Invalid Slack user token');
expect(body).not.toContain('Authentication failed. Tried:');
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Minor: Test doesn't verify complete error message format

Issue: The assertion only checks for 'Invalid Slack user token' but doesn't verify the specific error reason is included (e.g., 'signature verification failed').

Why: If the error message format changes to omit the specific error reason, users would lose diagnostic information. The implementation at line 285 of runAuth.ts constructs: `Invalid Slack user token: ${result.error || 'Invalid token'}`

Fix:

Suggested change
expect(res.status).toBe(401);
const body = await res.text();
expect(body).toContain('Invalid Slack user token');
expect(body).not.toContain('Authentication failed. Tried:');
expect(res.status).toBe(401);
const body = await res.text();
expect(body).toContain('Invalid Slack user token: signature verification failed');
expect(body).not.toContain('Authentication failed. Tried:');

Refs:

@github-actions github-actions bot deleted a comment from claude bot Mar 15, 2026
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