-
-
Notifications
You must be signed in to change notification settings - Fork 438
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: automatic social account linking (#5881)
* feat: automatic social account linking * chore: add integration tests * chore: add changeset
- Loading branch information
Showing
13 changed files
with
204 additions
and
16 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
--- | ||
"@logto/experience": patch | ||
"@logto/console": patch | ||
"@logto/phrases": patch | ||
--- | ||
|
||
allow skipping manual account linking during sign-in | ||
|
||
You can find this configuration in Console -> Sign-in experience -> Sign-up and sign-in -> Social sign-in -> Automatic account linking. | ||
|
||
When switched on, if a user signs in with a social identity that is new to the system, and there is exactly one existing account with the same identifier (e.g., email), Logto will automatically link the account with the social identity instead of prompting the user for account linking. |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
114 changes: 114 additions & 0 deletions
114
packages/integration-tests/src/tests/experience/automatic-account-linking.test.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { ConnectorType } from '@logto/connector-kit'; | ||
import { SignInIdentifier } from '@logto/schemas'; | ||
|
||
import { createUser, deleteUser } from '#src/api/admin-user.js'; | ||
import { updateSignInExperience } from '#src/api/sign-in-experience.js'; | ||
import { demoAppUrl } from '#src/constants.js'; | ||
import { clearConnectorsByTypes, setSocialConnector } from '#src/helpers/connector.js'; | ||
import ExpectExperience from '#src/ui-helpers/expect-experience.js'; | ||
import { generateEmail, randomString } from '#src/utils.js'; | ||
|
||
describe('automatic account linking', () => { | ||
beforeAll(async () => { | ||
await clearConnectorsByTypes([ConnectorType.Social, ConnectorType.Email, ConnectorType.Sms]); | ||
await setSocialConnector(); | ||
await updateSignInExperience({ | ||
termsOfUseUrl: null, | ||
privacyPolicyUrl: null, | ||
signUp: { identifiers: [], password: true, verify: false }, | ||
signIn: { | ||
methods: [ | ||
{ | ||
identifier: SignInIdentifier.Username, | ||
password: true, | ||
verificationCode: false, | ||
isPasswordPrimary: true, | ||
}, | ||
], | ||
}, | ||
singleSignOnEnabled: true, | ||
socialSignInConnectorTargets: ['mock-social'], | ||
}); | ||
}); | ||
|
||
it('should automatically link account', async () => { | ||
await updateSignInExperience({ | ||
termsOfUseUrl: null, | ||
privacyPolicyUrl: null, | ||
socialSignIn: { automaticAccountLinking: true }, | ||
}); | ||
const socialUserId = 'foo_' + randomString(); | ||
const user = await createUser({ primaryEmail: generateEmail() }); | ||
const experience = new ExpectExperience(await browser.newPage()); | ||
|
||
await experience.navigateTo(demoAppUrl.href); | ||
await experience.toProcessSocialSignIn({ | ||
socialUserId, | ||
socialEmail: user.primaryEmail!, | ||
}); | ||
|
||
experience.toMatchUrl(demoAppUrl); | ||
await experience.toMatchElement('div', { text: `User ID: ${user.id}` }); | ||
await experience.toClick('div[role=button]', /sign out/i); | ||
await experience.page.close(); | ||
|
||
await deleteUser(user.id); | ||
}); | ||
|
||
it('should automatically link account with terms of use and privacy policy', async () => { | ||
await updateSignInExperience({ | ||
termsOfUseUrl: 'https://example.com/terms', | ||
privacyPolicyUrl: 'https://example.com/privacy', | ||
socialSignIn: { automaticAccountLinking: true }, | ||
}); | ||
const socialUserId = 'foo_' + randomString(); | ||
const user = await createUser({ primaryEmail: generateEmail() }); | ||
const experience = new ExpectExperience(await browser.newPage()); | ||
|
||
await experience.navigateTo(demoAppUrl.href); | ||
await experience.toProcessSocialSignIn({ | ||
socialUserId, | ||
socialEmail: user.primaryEmail!, | ||
}); | ||
|
||
// Should have popped up the terms of use and privacy policy dialog | ||
await experience.toMatchElement('div', { text: /terms of use/i }); | ||
await experience.toClick('button', /agree/i); | ||
|
||
experience.toMatchUrl(demoAppUrl); | ||
await experience.toMatchElement('div', { text: `User ID: ${user.id}` }); | ||
await experience.toClick('div[role=button]', /sign out/i); | ||
await experience.page.close(); | ||
|
||
await deleteUser(user.id); | ||
}); | ||
|
||
it('should not automatically link account', async () => { | ||
await updateSignInExperience({ | ||
termsOfUseUrl: null, | ||
privacyPolicyUrl: null, | ||
socialSignIn: { automaticAccountLinking: false }, | ||
}); | ||
const socialUserId = 'foo_' + randomString(); | ||
const user = await createUser({ primaryEmail: generateEmail() }); | ||
const experience = new ExpectExperience(await browser.newPage()); | ||
|
||
await experience.navigateTo(demoAppUrl.href); | ||
await experience.toProcessSocialSignIn({ | ||
socialUserId, | ||
socialEmail: user.primaryEmail!, | ||
}); | ||
|
||
await experience.toClick('button', /create account without linking/i); | ||
experience.toMatchUrl(demoAppUrl); | ||
try { | ||
await experience.toMatchElement('div', { text: `User ID: ${user.id}`, timeout: 100 }); | ||
throw new Error('User ID should not be displayed'); | ||
} catch {} | ||
|
||
await experience.toClick('div[role=button]', /sign out/i); | ||
await experience.page.close(); | ||
|
||
await deleteUser(user.id); | ||
}); | ||
}); |
This file contains 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
This file contains 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
18 changes: 18 additions & 0 deletions
18
packages/schemas/alterations/next-1717567857-social-sign-in-linking.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { sql } from '@silverhand/slonik'; | ||
|
||
import type { AlterationScript } from '../lib/types/alteration.js'; | ||
|
||
const alteration: AlterationScript = { | ||
up: async (pool) => { | ||
await pool.query(sql` | ||
alter table sign_in_experiences add column social_sign_in jsonb not null default '{}'::jsonb; | ||
`); | ||
}, | ||
down: async (pool) => { | ||
await pool.query(sql` | ||
alter table sign_in_experiences drop column social_sign_in; | ||
`); | ||
}, | ||
}; | ||
|
||
export default alteration; |
This file contains 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
This file contains 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