Skip to content

OAuth connect overwrote existing ones instead of adding a second connection #1540

Description

@baggiiiie

issue

adding a second OAuth account for the same integration, (e.g. gmail) silently replaced the first account's connection row instead of creating a new one. The connections list shows one connection

reproduce

  • add a gmail integration
  • add a connection for one gmail account (shows up correctly)
  • add a second connection for another gmail account (does not show up)

cause

had a look into the root cause, the connection name is derived by connectionIdentifier, which turns a human label into a identifier-safe, camelCase name. On an OAuth connect the name passes through this function twice:

  • client: derives the name from the label the user typed and sends it as the name on POST /oauth/start.
  • server: mintOAuthConnection re-normalizes the incoming name with connectionIdentifier before writing the connection row
  • in between, oauth.start runs a free-name guard: when newConnection is set it calls connectionNameTaken and appends a 2 suffix if the name already exists, so a second connect mints fooBar2 instead of re-minting fooBar.

but connectionIdentifier was not idempotent, it lower-cased the entire input before tokenizing:

const words = input.toLowerCase().match(/[a-z0-9]+/g);

Lower-casing first destroys camelCase word boundaries on a second pass, so
f(f(x)) !== f(x):

input f(input) f(f(input)) stable?
"Local Name" "localName" "localname" no
"local name" "localName" "localname" no
"localName" "localname" "localname" yes

The client sent "localName"; the server re-normalized it to "localname".

The guard checked the raw name; the mint stored the re-normalized name. connectionNameTakenfindConnectionRow matches rows by an exact, case-sensitive SQL comparison on name. oauth.start fed it the raw client string:

let name = input.name;
if (input.newConnection === true) {
let suffix = 2;
while (
yield* deps.connectionNameTaken({
owner: input.owner,
integration: input.integration,
name,
})
) {
if (suffix > 1000) {
return yield* new OAuthStartError({
message: `No free connection name derivable from ${input.name}.`,
});
}
name = ConnectionName.make(`${String(input.name)}${suffix}`);
suffix++;
}

So the guard compared "localName" against the stored row "localname", found no match, declared the name free, and skipped the suffix. The mint then re-normalized to "localname", matched the existing row, and issued an UPDATE — overwriting the first account's credentials, scopes, and label.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions