[APPS] Invalidate unusable cached OAuth tokens by sdkennedy2 · Pull Request #434 · DataDog/build-plugins · GitHub
Skip to content

[APPS] Invalidate unusable cached OAuth tokens#434

Open
sdkennedy2 wants to merge 1 commit into
masterfrom
sdkennedy2/apps-oauth-token-hardening
Open

[APPS] Invalidate unusable cached OAuth tokens#434
sdkennedy2 wants to merge 1 commit into
masterfrom
sdkennedy2/apps-oauth-token-hardening

Conversation

@sdkennedy2

@sdkennedy2 sdkennedy2 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Apps OAuth relies on cached tokens from the OS credential store so repeated uploads do not need a browser authorization flow. When that cache contains a bad token, the command can get stuck reusing it instead of recovering.

This PR handles the cases where the cached value is malformed, belongs to a different site/client, has an empty access token, cannot be refreshed, or is rejected by Datadog with an OAuth auth failure.

This is stacked on #435, which defers persistence of newly authorized tokens until the first authenticated request succeeds. This PR adds the follow-up hardening for tokens that already exist in the cache or become unusable later.

Changes

  • Validate cached OAuth credential shape before using it:
    • invalid JSON is deleted from keychain
    • empty access tokens are rejected
    • site/client mismatches are rejected
    • optional token fields must have the expected primitive types
  • Delete cached OAuth tokens when refresh fails, so the next command starts browser authorization instead of retrying a stale refresh token.
  • Add RequestError with HTTP status metadata to the shared request helper.
  • When an OAuth-backed request fails with 401 or 403, clear both the OS credential-store entry and the in-process token memo.
  • Keep [APPS] Defer OAuth token persistence until first request succeeds #435's deferred persistence behavior: refreshed or freshly authorized tokens are persisted only after the OAuth-authenticated request succeeds.
  • Update the Apps README to describe that first-time OAuth tokens are saved only after Datadog accepts a plugin request.

QA Instructions

No manual QA required. The changed behavior is covered by focused unit tests for the OAuth and request helpers.

Validated with:

yarn workspace @dd/tests test:unit packages/core/src/helpers/oauth-request.test.ts packages/core/src/helpers/request.test.ts --runInBand

Blast Radius

This affects Apps plugin OAuth authentication paths that use the shared core OAuth request helper. API-key and app-key authentication are unchanged.

Expected customer-visible behavior: users with malformed, revoked, wrong-site, wrong-client, expired-unrefreshable, or API-rejected cached OAuth tokens should be prompted to authorize again instead of staying stuck on the unusable cached token.

Documentation

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-oauth-token-hardening branch 2 times, most recently from dd5653f to 5f9109c Compare June 25, 2026 14:02
@sdkennedy2 sdkennedy2 changed the base branch from master to sdkennedy2/defer-oauth-token-persistence June 25, 2026 14:02
@sdkennedy2 sdkennedy2 changed the title [APPS] Harden OAuth token caching [APPS] Invalidate unusable cached OAuth tokens Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This refactor lets token reads and explicit token deletes share the same keychain deletion behavior. deleteOAuthTokenFromKeychain still ignores missing entries, but the deletion logic now lives in deleteOAuthCredentialEntry so readOAuthTokenFromKeychain can also clean up malformed or invalid cached credentials without duplicating the NoEntry handling. The split also keeps failures to create the keychain entry distinct from failures while deleting the stored password.

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-oauth-token-hardening branch from 5f9109c to 19ac473 Compare June 25, 2026 14:16
}

if (!response.ok) {
// Not instantiating the error here, as it will make Jest throw in the tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We shouldn't remove this.

});
};

export const getOAuthToken = async (

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This was unused.

Comment on lines +415 to +416
value.token.clientId === options.clientId &&
value.token.site === site &&

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

If the cached toke is for a different client id or site, we invalidate the cached token.

Base automatically changed from sdkennedy2/defer-oauth-token-persistence to master June 25, 2026 14:24

return result;
} catch (error) {
if (isOAuthAuthError(error)) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We now invalidate the stored token on a 401 and 403

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could this be too simple a heuristic? 401/403 could also be caused by granular access issues

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-oauth-token-hardening branch from 19ac473 to 226674c Compare June 25, 2026 18:03
@sdkennedy2 sdkennedy2 marked this pull request as ready for review July 1, 2026 12:15
@sdkennedy2 sdkennedy2 requested review from a team as code owners July 1, 2026 12:15
@sdkennedy2 sdkennedy2 requested review from tyffical and removed request for a team July 1, 2026 12:15
@oliverli

oliverli commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

await deleteOAuthToken(site, options, log);

P2 Badge Preserve refresh tokens on transient refresh failures

When an expired cached token has a refresh token and the refresh call fails for a transient reason (for example a token-endpoint 5xx or network error), this catch still deletes the cached credential. refreshOAuthToken performs the network request to the token endpoint, so not every thrown error proves the refresh token is invalid; after this deletion a non-interactive run falls into browser auth and a later retry cannot use the still-valid refresh token after the service recovers. Please only evict on refresh errors that indicate the token is unusable, such as an OAuth invalid_grant/auth failure.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


return result;
} catch (error) {
if (isOAuthAuthError(error)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

could this be too simple a heuristic? 401/403 could also be caused by granular access issues

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Tell the user to try again (to trigger a new login with oauth)

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