diff --git a/packages/core/src/lib/register.ts b/packages/core/src/lib/register.ts
index 96707715c..243ccd5a4 100644
--- a/packages/core/src/lib/register.ts
+++ b/packages/core/src/lib/register.ts
@@ -2,20 +2,14 @@ import { PasscodeType, UserLogType } from '@logto/schemas';
 import { Context } from 'koa';
 import { Provider } from 'oidc-provider';
 
+import { SocialUserInfo } from '@/connectors/types';
 import RequestError from '@/errors/RequestError';
 import { WithUserLogContext } from '@/middleware/koa-user-log';
-import {
-  hasUser,
-  hasUserWithEmail,
-  hasUserWithPhone,
-  hasUserWithIdentity,
-  insertUser,
-} from '@/queries/user';
+import { hasUser, hasUserWithEmail, hasUserWithPhone, insertUser } from '@/queries/user';
 import assertThat from '@/utils/assert-that';
 import { emailRegEx, phoneRegEx } from '@/utils/regex';
 
 import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
-import { getUserInfoByConnectorCode, SocialUserInfoSession } from './social';
 import { encryptUserPassword, generateUserId } from './user';
 
 const assignRegistrationResult = async (ctx: Context, provider: Provider, userId: string) => {
@@ -28,22 +22,6 @@ const assignRegistrationResult = async (ctx: Context, provider: Provider, userId
   ctx.body = { redirectTo };
 };
 
-const saveUserInfoToSession = async (
-  ctx: Context,
-  provider: Provider,
-  socialUserInfo: SocialUserInfoSession
-) => {
-  const redirectTo = await provider.interactionResult(
-    ctx.req,
-    ctx.res,
-    {
-      socialUserInfo,
-    },
-    { mergeWithLastSubmission: true }
-  );
-  ctx.body = { redirectTo };
-};
-
 export const registerWithUsernameAndPassword = async (
   ctx: WithUserLogContext<Context>,
   provider: Provider,
@@ -169,18 +147,9 @@ export const registerWithPhoneAndPasscode = async (
 export const registerWithSocial = async (
   ctx: WithUserLogContext<Context>,
   provider: Provider,
-  { connectorId, code }: { connectorId: string; code: string }
+  connectorId: string,
+  userInfo: SocialUserInfo
 ) => {
-  const userInfo = await getUserInfoByConnectorCode(connectorId, code);
-
-  if (await hasUserWithIdentity(connectorId, userInfo.id)) {
-    await saveUserInfoToSession(ctx, provider, { connectorId, userInfo });
-    throw new RequestError({
-      code: 'user.identity_exists',
-      status: 422,
-    });
-  }
-
   const id = await generateUserId();
   await insertUser({
     id,
diff --git a/packages/core/src/lib/sign-in.ts b/packages/core/src/lib/sign-in.ts
index 23e7be441..044b4f7dc 100644
--- a/packages/core/src/lib/sign-in.ts
+++ b/packages/core/src/lib/sign-in.ts
@@ -3,6 +3,7 @@ import { Context } from 'koa';
 import { InteractionResults, Provider } from 'oidc-provider';
 
 import { getSocialConnectorInstanceById } from '@/connectors';
+import { SocialUserInfo } from '@/connectors/types';
 import RequestError from '@/errors/RequestError';
 import { WithUserLogContext } from '@/middleware/koa-user-log';
 import {
@@ -18,7 +19,11 @@ import assertThat from '@/utils/assert-that';
 import { emailRegEx, phoneRegEx } from '@/utils/regex';
 
 import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
-import { getUserInfoByConnectorCode, getUserInfoFromInteractionResult } from './social';
+import {
+  findSocialRelatedUser,
+  getUserInfoFromInteractionResult,
+  SocialUserInfoSession,
+} from './social';
 import { findUserByUsernameAndPassword } from './user';
 
 const assignSignInResult = async (ctx: Context, provider: Provider, userId: string) => {
@@ -119,26 +124,42 @@ export const assignRedirectUrlForSocial = async (
   ctx.body = { redirectTo };
 };
 
+const saveUserInfoToSession = async (
+  ctx: Context,
+  provider: Provider,
+  socialUserInfo: SocialUserInfoSession
+) => {
+  const redirectTo = await provider.interactionResult(
+    ctx.req,
+    ctx.res,
+    {
+      socialUserInfo,
+    },
+    { mergeWithLastSubmission: true }
+  );
+  ctx.body = { redirectTo };
+};
+
 export const signInWithSocial = async (
   ctx: WithUserLogContext<Context>,
   provider: Provider,
-  { connectorId, code, result }: { connectorId: string; code: string; result?: InteractionResults }
+  connectorId: string,
+  userInfo: SocialUserInfo
 ) => {
   ctx.userLog.connectorId = connectorId;
   ctx.userLog.type = UserLogType.SignInSocial;
 
-  const userInfo =
-    code === 'session'
-      ? await getUserInfoFromInteractionResult(connectorId, result)
-      : await getUserInfoByConnectorCode(connectorId, code);
-
-  assertThat(
-    await hasUserWithIdentity(connectorId, userInfo.id),
-    new RequestError({
-      code: 'user.identity_not_exists',
-      status: 422,
-    })
-  );
+  if (!(await hasUserWithIdentity(connectorId, userInfo.id))) {
+    await saveUserInfoToSession(ctx, provider, { connectorId, userInfo });
+    const relatedInfo = await findSocialRelatedUser(userInfo);
+    throw new RequestError(
+      {
+        code: 'user.identity_not_exists',
+        status: 422,
+      },
+      relatedInfo && { relatedUser: relatedInfo[0] }
+    );
+  }
 
   const { id, identities } = await findUserByIdentity(connectorId, userInfo.id);
   // Update social connector's user info
@@ -148,3 +169,25 @@ export const signInWithSocial = async (
   ctx.userLog.userId = id;
   await assignSignInResult(ctx, provider, id);
 };
+
+export const signInWithSocialRelatedUser = async (
+  ctx: WithUserLogContext<Context>,
+  provider: Provider,
+  { connectorId, result }: { connectorId: string; result: InteractionResults }
+) => {
+  ctx.userLog.connectorId = connectorId;
+  ctx.userLog.type = UserLogType.SignInSocial;
+
+  const userInfo = await getUserInfoFromInteractionResult(connectorId, result);
+  const relatedInfo = await findSocialRelatedUser(userInfo);
+
+  assertThat(relatedInfo, 'session.connector_session_not_found');
+
+  const { id, identities } = relatedInfo[1];
+
+  await updateUserById(id, {
+    identities: { ...identities, [connectorId]: { userId: userInfo.id, details: userInfo } },
+  });
+  ctx.userLog.userId = id;
+  await assignSignInResult(ctx, provider, id);
+};
diff --git a/packages/core/src/lib/social.ts b/packages/core/src/lib/social.ts
index ebeec8974..ffe79ddab 100644
--- a/packages/core/src/lib/social.ts
+++ b/packages/core/src/lib/social.ts
@@ -1,9 +1,16 @@
+import { User } from '@logto/schemas';
 import { InteractionResults } from 'oidc-provider';
 import { z } from 'zod';
 
 import { getSocialConnectorInstanceById } from '@/connectors';
 import { SocialUserInfo, socialUserInfoGuard } from '@/connectors/types';
 import RequestError from '@/errors/RequestError';
+import {
+  findUserByEmail,
+  findUserByPhone,
+  hasUserWithEmail,
+  hasUserWithPhone,
+} from '@/queries/user';
 import assertThat from '@/utils/assert-that';
 
 export interface SocialUserInfoSession {
@@ -27,12 +34,12 @@ const getConnector = async (connectorId: string) => {
   }
 };
 
-export const getUserInfoByConnectorCode = async (
+export const getUserInfoByAuthCode = async (
   connectorId: string,
-  code: string
+  authCode: string
 ): Promise<SocialUserInfo> => {
   const connector = await getConnector(connectorId);
-  const accessToken = await connector.getAccessToken(code);
+  const accessToken = await connector.getAccessToken(authCode);
 
   return connector.getUserInfo(accessToken);
 };
@@ -41,16 +48,46 @@ export const getUserInfoFromInteractionResult = async (
   connectorId: string,
   interactionResult?: InteractionResults
 ): Promise<SocialUserInfo> => {
-  const result = z
+  const parse = z
     .object({
       socialUserInfo: z.object({
         connectorId: z.string(),
         userInfo: socialUserInfoGuard,
       }),
     })
-    .parse(interactionResult);
+    .safeParse(interactionResult);
 
+  if (!parse.success) {
+    throw new RequestError('session.connector_session_not_found');
+  }
+
+  const result = parse.data;
   assertThat(result.socialUserInfo.connectorId === connectorId, 'session.connector_id_mismatch');
 
   return result.socialUserInfo.userInfo;
 };
+
+/**
+ * Find user by phone/email from social user info.
+ * if both phone and email exist, take phone for priority.
+ *
+ * @param info SocialUserInfo
+ * @returns null | [string, User] the first string idicating phone or email
+ */
+export const findSocialRelatedUser = async (
+  info: SocialUserInfo
+): Promise<null | [string, User]> => {
+  if (info.phone && (await hasUserWithPhone(info.phone))) {
+    const user = await findUserByPhone(info.phone);
+
+    return [info.phone, user];
+  }
+
+  if (info.email && (await hasUserWithEmail(info.email))) {
+    const user = await findUserByEmail(info.email);
+
+    return [info.email, user];
+  }
+
+  return null;
+};
diff --git a/packages/core/src/routes/session.ts b/packages/core/src/routes/session.ts
index eaa4e7e8b..0459b80c5 100644
--- a/packages/core/src/routes/session.ts
+++ b/packages/core/src/routes/session.ts
@@ -1,7 +1,9 @@
 import path from 'path';
 
 import { LogtoErrorCode } from '@logto/phrases';
+import { userInfoSelectFields } from '@logto/schemas';
 import { conditional } from '@silverhand/essentials';
+import pick from 'lodash.pick';
 import { Provider } from 'oidc-provider';
 import { object, string } from 'zod';
 
@@ -21,8 +23,11 @@ import {
   signInWithEmailAndPasscode,
   signInWithPhoneAndPasscode,
   signInWithUsernameAndPassword,
+  signInWithSocialRelatedUser,
 } from '@/lib/sign-in';
+import { getUserInfoByAuthCode, getUserInfoFromInteractionResult } from '@/lib/social';
 import koaGuard from '@/middleware/koa-guard';
+import { findUserById, hasUserWithIdentity, updateUserById } from '@/queries/user';
 import assertThat from '@/utils/assert-that';
 
 import { AnonymousRouter } from './types';
@@ -95,7 +100,6 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
       body: object({ connectorId: string(), code: string().optional(), state: string() }),
     }),
     async (ctx, next) => {
-      const { result } = await provider.interactionDetails(ctx.req, ctx.res);
       const { connectorId, code, state } = ctx.guard.body;
 
       if (!code) {
@@ -105,7 +109,25 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
         return next();
       }
 
-      await signInWithSocial(ctx, provider, { connectorId, code, result });
+      const userInfo = await getUserInfoByAuthCode(connectorId, code);
+      await signInWithSocial(ctx, provider, connectorId, userInfo);
+
+      return next();
+    }
+  );
+
+  router.post(
+    '/session/sign-in/social-related-user',
+    koaGuard({
+      body: object({ connectorId: string() }),
+    }),
+    async (ctx, next) => {
+      const { connectorId } = ctx.guard.body;
+
+      const { result } = await provider.interactionDetails(ctx.req, ctx.res);
+      assertThat(result, 'session.connector_session_not_found');
+
+      await signInWithSocialRelatedUser(ctx, provider, { connectorId, result });
 
       return next();
     }
@@ -206,21 +228,50 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
     koaGuard({
       body: object({
         connectorId: string(),
-        code: string().optional(),
-        state: string().optional(),
       }),
     }),
     async (ctx, next) => {
-      const { connectorId, code, state } = ctx.guard.body;
+      const { connectorId } = ctx.guard.body;
+      const { result } = await provider.interactionDetails(ctx.req, ctx.res);
 
-      if (!code) {
-        assertThat(state, 'session.insufficient_info');
-        await assignRedirectUrlForSocial(ctx, connectorId, state);
+      // User can not regsiter with social directly,
+      // need to try to sign in with social first, then confirm to register and continue,
+      // so the result is expected to be exists.
+      assertThat(result, 'session.connector_session_not_found');
 
-        return next();
-      }
+      const userInfo = await getUserInfoFromInteractionResult(connectorId, result);
+      assertThat(!(await hasUserWithIdentity(connectorId, userInfo.id)), 'user.identity_exists');
 
-      await registerWithSocial(ctx, provider, { connectorId, code });
+      await registerWithSocial(ctx, provider, connectorId, userInfo);
+
+      return next();
+    }
+  );
+
+  router.post(
+    '/session/bind-social',
+    koaGuard({
+      body: object({
+        connectorId: string(),
+      }),
+    }),
+    async (ctx, next) => {
+      const { connectorId } = ctx.guard.body;
+      const { result } = await provider.interactionDetails(ctx.req, ctx.res);
+      assertThat(result, 'session.connector_session_not_found');
+      assertThat(result.login?.accountId, 'session.unauthorized');
+
+      const userInfo = await getUserInfoFromInteractionResult(connectorId, result);
+      const user = await findUserById(result.login.accountId);
+
+      const updatedUser = await updateUserById(user.id, {
+        identities: {
+          ...user.identities,
+          [connectorId]: { userId: userInfo.id, details: userInfo },
+        },
+      });
+
+      ctx.body = pick(updatedUser, ...userInfoSelectFields);
 
       return next();
     }
diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts
index dc8cd86d0..bbd1c2884 100644
--- a/packages/phrases/src/locales/en.ts
+++ b/packages/phrases/src/locales/en.ts
@@ -65,6 +65,8 @@ const errors = {
     invalid_connector_id: 'Unable to find available connector with id {{connectorId}}.',
     insufficient_info: 'Insufficent sign-in info.',
     connector_id_mismatch: 'The connectorId is mismatched with session record.',
+    connector_session_not_found: 'Connector session not found. Please go back and sign in again.',
+    unauthorized: 'Please sign in first.',
   },
   connector: {
     general: 'An unexpected error occured in connector.',
diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts
index 2c3acd402..fd5ac277a 100644
--- a/packages/phrases/src/locales/zh-cn.ts
+++ b/packages/phrases/src/locales/zh-cn.ts
@@ -66,6 +66,8 @@ const errors = {
     insufficient_info: '登录信息缺失,请检查您的输入。',
     invalid_connector_id: '无法找到 ID 为 {{connectorId}} 的可用连接器。',
     connector_id_mismatch: '传入的 connectorId 与 session 中保存的记录不一致。',
+    connector_session_not_found: '无法找到 connector 登录信息,请尝试重新登录。',
+    unauthorized: '请先登录。',
   },
   connector: {
     general: 'Connector 发生未知错误。',