Skip to content

fix(external): only fire after-user-created hook on account creation - #2756

Open
Mpradeep-dev wants to merge 4 commits into
supabase:masterfrom
Mpradeep-dev:fix/after-user-created-external-signin
Open

fix(external): only fire after-user-created hook on account creation#2756
Mpradeep-dev wants to merge 4 commits into
supabase:masterfrom
Mpradeep-dev:fix/after-user-created-external-signin

Conversation

@Mpradeep-dev

Copy link
Copy Markdown

What

The after-user-created hook is documented to run once, after a new user is
created. On the external OAuth callback path it instead runs on every
sign-in.

In internalExternalProviderCallback (internal/api/external.go), the account
creation branch throws away the AccountLinkingDecision returned by
createAccountFromExternalIdentity and hard-codes createdUser = true:

} else {
    createdUser = true
    if _, user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional); terr != nil {
        return terr
    }
}

That branch handles every external OAuth sign-in, so the hook also fires for
AccountExists (a returning user) and LinkAccount (a new identity linked to
an existing user). The equivalent call sites in token_oidc.go, web3.go and
samlacs.go all gate the hook on decision == models.CreateAccount.

Fix

Capture the decision and gate createdUser on it, matching the other three
sign-in paths:

} else {
    var decision models.AccountLinkingDecision
    if decision, user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType, emailOptional); terr != nil {
        return terr
    }
    createdUser = decision == models.CreateAccount
}

Testing

New regression test TestExternal/TestExternalCallbackAfterUserCreatedHookOnlyOnSignup
in internal/api/external_test.go:

  • stands up a counting after-user-created HTTP hook and runs the GitHub OAuth
    callback twice for the same identity;
  • asserts the hook fires once (on account creation) and not on the second,
    returning-user sign-in.

It fails before this change (hook fires twice) and passes after. Verified
locally with -race:

  • go test ./internal/api/ -run TestExternal -race — pass
  • go test ./internal/api/ -run 'TestHooks|TestE2EHooks|TestOIDC|Web3|SAML' -race — pass

One unrelated pre-existing failure in this package on my machine
(TestAuth/TestMaybeLoadUserOrSession/Valid_Session_ID_Claim, a time.Local
vs time.UTC comparison) reproduces on a clean master checkout and is not
touched by this change.

Closes #2726

The external OAuth callback discarded the AccountLinkingDecision returned
by createAccountFromExternalIdentity and unconditionally set
createdUser = true, so the after-user-created hook ran on every sign-in
for GitHub, Google and other external providers - including returning
users (AccountExists) and identity links (LinkAccount).

Gate createdUser on decision == models.CreateAccount, matching the OIDC,
Web3 and SAML sign-in paths and the documented "runs after a user is
created" contract.
@Mpradeep-dev
Mpradeep-dev requested a review from a team as a code owner August 28, 2026 08:40
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.

after-user-created hook fires on every external OAuth sign-in, not just on account creation

1 participant