Skip to content

fix(core): explicitly persist tokens on OAuth2 re-auth#582

Merged
d-klotz merged 2 commits into
nextfrom
fix/core-persist-tokens-on-oauth-reauth
May 5, 2026
Merged

fix(core): explicitly persist tokens on OAuth2 re-auth#582
d-klotz merged 2 commits into
nextfrom
fix/core-persist-tokens-on-oauth-reauth

Conversation

@d-klotz
Copy link
Copy Markdown
Contributor

@d-klotz d-klotz commented May 5, 2026

Summary

ProcessAuthorizationCallback relied solely on the DLGT_TOKEN_UPDATE notification chain (setTokensnotifyModule.onTokenUpdateupsertCredential) to persist freshly-issued OAuth tokens. In production this chain has been observed to silently no-op: the OAuth provider issues new tokens (and invalidates the prior refresh_token), but the new tokens never land in the DB. The user-visible /api/authorize call appears to succeed, the existing credential is reused with stale token data, and downstream API calls fail with 401 once the provider rotates the old refresh_token.

This was diagnosed in a Pipedrive integration: re-installs created new Integration rows pointing at a 3-month-old credential whose tokens had been retired by Pipedrive during today's OAuth flow, but never replaced in our DB. The auth Lambda logs were silent because none of the existing code logs at the relevant boundaries.

Changes

  • Bootstrap the Module with the existing entityprocess-authorization-callback.js now looks up the existing entity via findEntitiesByUserIdAndModuleName and passes it to new Module(...) so the API requester is preloaded with prior tokens. Replaces the long-standing // todo: check if we need to pass entity to Module, right now it's null TODO.
  • Belt-and-suspenders explicit persistence on the OAuth2 path — explicit await this.onTokenUpdate(module, moduleDefinition, userId) after getToken. The notification chain stays in place; this ensures persistence happens even when the chain silently no-ops. Idempotent against the notification path because upsertCredential is keyed by externalId.
  • Diagnostic logging at entry, after getToken, after persistence, and after restore — so future re-auth bugs aren't invisible in CloudWatch.
  • Wrap POST /api/authorize with try/catch + console.error so handler failures surface instead of returning silently.
  • restoreIntegrationsForEntity returns the count of flipped integrations for logging.

Test plan

  • Existing 5 ProcessAuthorizationCallback re-auth status restoration tests pass
  • New test: persists credentials explicitly on OAuth2 re-auth (belt-and-suspenders) — locks in that upsertCredential is called exactly once on the OAuth2 path
  • Smoke-test in dev: re-install a Pipedrive integration; confirm CloudWatch shows the new [Frigg] processAuthorizationCallback ... log lines and that the credential's updatedAt matches the OAuth callback time (not a later auth-flip)
  • Verify a re-auth on a credential currently in ERROR flips it back to ENABLED and the integration resumes syncing

🤖 Generated with Claude Code

Version

Published prerelease version: v2.0.0-next.82

Changelog

🚀 Enhancement

  • @friggframework/core, @friggframework/devtools, @friggframework/eslint-config, @friggframework/prettier-config, @friggframework/schemas, @friggframework/serverless-plugin, @friggframework/test, @friggframework/ui
    • fix(core): add per-request timeout to Requester to catch silent fetch hangs #579 (@d-klotz)

🐛 Bug Fix

  • @friggframework/core
    • fix(core): explicitly persist tokens on OAuth2 re-auth #582 (@d-klotz)

Authors: 1

ProcessAuthorizationCallback relied solely on the DLGT_TOKEN_UPDATE
notification chain (setTokens -> notify -> Module.onTokenUpdate ->
upsertCredential) to persist freshly-issued OAuth tokens. In production
the chain has been observed to silently no-op: callers complete OAuth
on the provider side (issuing new tokens and invalidating the prior
refresh_token), but the new tokens never land in the DB. The user-
visible /api/authorize call appears to succeed, the existing credential
is reused with stale token data, and downstream API calls fail with
401 once the provider rotates the old refresh_token.

Changes:
- Bootstrap the Module with the existing entity (looked up via
  findEntitiesByUserIdAndModuleName) so the API requester is preloaded
  with prior tokens. Replaces the `entity = null` TODO.
- Belt-and-suspenders: explicitly call onTokenUpdate after getToken on
  the OAuth2 path. The notification chain remains in place; this
  ensures persistence happens even when the chain doesn't fire.
- Add diagnostic logging at entry, after getToken, after persistence,
  and after restore so future investigations are not blind.
- Wrap POST /api/authorize handler with try/catch + console.error so
  failures surface in CloudWatch instead of returning silently.
- restoreIntegrationsForEntity returns the count of flipped
  integrations for logging.

Tests: existing re-auth status restoration tests pass; new test locks
in that upsertCredential is called once on the OAuth2 path.

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

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

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db461ccd7f

ℹ️ 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".

Comment on lines +55 to +58
const existingEntity =
existingEntities && existingEntities.length > 0
? existingEntities[0]
: null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Select the re-auth target entity deterministically

Using existingEntities[0] makes OAuth2 re-auth pick an arbitrary prior entity when a user has multiple entities for the same module, because repository findMany calls do not enforce ordering. That arbitrary entity's credential is then preloaded into Module, and OAuth2Requester.setTokens preserves an existing refresh_token when the provider does not return one, so a re-auth for account B can persist account A's refresh token and break subsequent refreshes for the updated credential.

Useful? React with 👍 / 👎.

Comment on lines 508 to 511
const module = await getModuleInstanceFromType.execute(
userId,
params.entityType,
{ state: req.query.state }
params.entityType
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Forward OAuth state when building authorization requirements

The GET /api/authorize path no longer forwards req.query.state to getModuleInstanceFromType.execute, so modules that rely on the optional state round-trip cannot include the caller-provided value in their authorization URL. This regresses CSRF/callback correlation flows that depend on a custom state token being propagated into the OAuth requester.

Useful? React with 👍 / 👎.

Reinstates the `{ state: req.query.state }` third argument to
getModuleInstanceFromType.execute in the GET /api/authorize handler.
Was inadvertently dropped when applying this branch's changes from a
working tree based on a commit predating f660549 (feat(core): forward
OAuth state from /api/authorize to module API).

Without this, modules that hardcode &state=${this.state} in their
authorize URL (e.g. api-module-hubspot) interpolate state=null again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@d-klotz d-klotz added the release Create a release when this pr is merged label May 5, 2026
@d-klotz d-klotz merged commit 9b98ac9 into next May 5, 2026
5 of 6 checks passed
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

@d-klotz d-klotz deleted the fix/core-persist-tokens-on-oauth-reauth branch May 5, 2026 16:39
@seanspeaks
Copy link
Copy Markdown
Contributor

🚀 PR was released in v2.0.0-next.82 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prerelease This change is available in a prerelease. release Create a release when this pr is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants