fix(siwe): normalize Ethereum address to lowercase to prevent duplicate identities#2507
Open
mastermanas805 wants to merge 1 commit intosupabase:masterfrom
Open
Conversation
…te identities Ethereum addresses are protocol-level case-insensitive: 0xABC... and 0xabc... refer to the same wallet. EIP-55 mixed-case is a visual checksum, not a distinct identifier. Before this change, internal/utilities/siwe/parser.go stored the address verbatim from the SIWE message body. internal/api/web3.go then composed the identity provider_id directly from that string, and models.FindIdentityByIdAndProvider does case-sensitive equality on provider_id. The result: the same wallet signing in with different case variants of its address produced two distinct auth.identities rows pointing to two separate users. Fixes supabase#2264. The fix is a single strings.ToLower call at parser entry, immediately after the address pattern matches. Normalizing at the parser layer gives every caller a single canonical form with no extra knowledge required, and matches the existing email lowercase-normalization precedent in internal/models/identity.go (BeforeUpdate). VerifySignature already used strings.EqualFold, so signature recovery is unaffected. Scope is intentionally limited to SIWE (Ethereum). The SIWS (Solana) parser is left untouched: Solana addresses are base58, which is case-sensitive, and lowercasing them would corrupt valid addresses. Operational note: this fix prevents NEW duplicate identities. Existing rows with mixed-case provider_id values are not migrated by this patch; operators who have already accumulated duplicates will need a separate backfill that lowercases auth.identities.provider_id for provider='ethereum' rows and merges the resulting collisions. Flagged as out of scope here. Signed-off-by: Manas Srivastava <mastermanas805@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes #2264.
Ethereum addresses are protocol-level case-insensitive:
0xABC...and0xabc...are the same wallet. The EIP-55 mixed-case form is a visual checksum, not a distinct identifier. The SIWE parser atinternal/utilities/siwe/parser.gostores the address verbatim from the signed message, andweb3GrantEthereumininternal/api/web3.gouses that exact string as part of the identityprovider_id. As a result, the same wallet signing in with two different case representations creates two distinct rows inauth.usersandauth.identities.How to reproduce on master (for reviewers)
This requires signing two SIWE messages with the same Ethereum private key, one with a mixed-case address line, one with a lowercase address line, and posting each to
/token?grant_type=web3. A self-contained Go reproducer is below.Step 1: Bring up auth with Web3 enabled
Step 2: Save this reproducer to a fresh directory
siwe-repro/main.go:siwe-repro/go.mod:Step 3: Run
cd siwe-repro go mod tidy go run main.goYou will see two HTTP 200 responses with two different
user.idvalues, ending with:DIFFERENT user_ids — bug #2264 confirmed.Step 4: Inspect the database
Two rows for the same wallet, distinguished only by case in
provider_id.Root cause
internal/utilities/siwe/parser.govalidates the address with^0x[a-fA-F0-9]{40}$and stores it verbatim on the parsedSIWEMessage. Ininternal/api/web3.go,web3GrantEthereumconstructsproviderId := "web3:" + chain + ":" + parsedMessage.Address, andmodels.FindIdentityByIdAndProviderdoes case-sensitive equality onprovider_id. So0xABC...and0xabc...look like distinct identities to the lookup and each path throughcreateAccountFromExternalIdentitycreates a fresh user.Fix
One-line normalization at the parser entry:
address = strings.ToLower(address)immediately after the regex match succeeds. Single point of truth, no caller changes required. This mirrors the existing email lowercase-normalization pattern inmodels.Identity.Solana SIWS is intentionally untouched: base58 is case-sensitive and lowercasing it would corrupt valid wallet identifiers.
How to verify the fix manually (for reviewers)
After applying this PR's diff:
Step 1: Hot-reload the auth binary
(Or just save the file —
CompileDaemonrebuilds within ~10 seconds.)Step 2: Clean the duplicate rows from your earlier reproduction run
Step 3: Re-run the reproducer
Output now ends with:
SAME user_id — fix is in place.Step 4: Re-inspect the database
Single row for the wallet,
provider_idin the lowercase canonical form.Automated tests
internal/utilities/siwe/parser_test.go(added in this PR):TestParseMessageAddressNormalization— table-driven test with case-variant inputs; asserts they all produce the same lowercaseAddress. Fails on master, passes after this fix.TestParseMessage/positive_example_*cases updated so their expected values use the lowercase form (the parser used to return verbatim case; this is a behavior change in the parser's contract).Run:
go test ./internal/utilities/siwe/ -count=1 -vVerification I ran locally
go test ./internal/utilities/siwe/ -count=1— greengo test ./internal/api/ -run TestWeb3 -count=1 -race— all 15 sub-tests greengo test ./... -count=1 -p 1 -race— full module suite greengofmt -l internal/utilities/siwe/parser.go internal/utilities/siwe/parser_test.go— emptygo vet ./...— emptyOperational note (out of scope; flagging for ops follow-up)
This PR prevents NEW duplicate identities from being created. Existing rows with mixed-case
provider_idforprovider='ethereum'are not migrated by this PR. Operators that have already accumulated duplicates may want a follow-up data migration along the lines of:Plus a strategy for merging the duplicate user rows the case-variant identities point at. That coordination is outside the scope of a parser-level fix.
Why not also touch SIWS (Solana)
Solana addresses are base58-encoded and case-sensitive. Lowercasing them would corrupt valid wallet identifiers. The change is intentionally Ethereum-only.