0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat: continue social sign in (#223)

* feat: social sign in

* feat: social register

* feat: continue social sign in
This commit is contained in:
Wang Sijie 2022-02-15 17:40:47 +08:00 committed by GitHub
parent e8cbe00d3a
commit 34e540d3ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 20 deletions

View file

@ -1,5 +1,6 @@
import { Languages } from '@logto/phrases';
import { ConnectorConfig, Connector, PasscodeType } from '@logto/schemas';
import { z } from 'zod';
export enum ConnectorType {
SMS = 'SMS',
@ -80,10 +81,12 @@ export type GetAccessToken = (code: string) => Promise<string>;
export type GetUserInfo = (accessToken: string) => Promise<SocialUserInfo>;
export interface SocialUserInfo {
id: string;
email?: string;
phone?: string;
name?: string;
avatar?: string;
}
export const socialUserInfoGuard = z.object({
id: z.string(),
email: z.string().optional(),
phone: z.string().optional(),
name: z.string().optional(),
avatar: z.string().optional(),
});
export type SocialUserInfo = z.infer<typeof socialUserInfoGuard>;

View file

@ -15,15 +15,29 @@ import assertThat from '@/utils/assert-that';
import { emailRegEx, phoneRegEx } from '@/utils/regex';
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
import { getUserInfoByConnectorCode } from './social';
import { getUserInfoByConnectorCode, SocialUserInfoSession } from './social';
import { encryptUserPassword, generateUserId } from './user';
const assignRegistrationResult = async (ctx: Context, provider: Provider, userId: string) => {
const redirectTo = await provider.interactionResult(
ctx.req,
ctx.res,
{ login: { accountId: userId } },
{ mergeWithLastSubmission: false }
);
ctx.body = { redirectTo };
};
const saveUserInfoToSession = async (
ctx: Context,
provider: Provider,
socialUserInfo: SocialUserInfoSession
) => {
const redirectTo = await provider.interactionResult(
ctx.req,
ctx.res,
{
login: { accountId: userId },
socialUserInfo,
},
{ mergeWithLastSubmission: false }
);
@ -159,13 +173,13 @@ export const registerWithSocial = async (
) => {
const userInfo = await getUserInfoByConnectorCode(connectorId, code);
assertThat(
!(await hasUserWithIdentity(connectorId, userInfo.id)),
new RequestError({
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({

View file

@ -1,6 +1,6 @@
import { PasscodeType, UserLogType } from '@logto/schemas';
import { Context } from 'koa';
import { Provider } from 'oidc-provider';
import { InteractionResults, Provider } from 'oidc-provider';
import { getSocialConnectorInstanceById } from '@/connectors';
import RequestError from '@/errors/RequestError';
@ -18,7 +18,7 @@ import assertThat from '@/utils/assert-that';
import { emailRegEx, phoneRegEx } from '@/utils/regex';
import { createPasscode, sendPasscode, verifyPasscode } from './passcode';
import { getUserInfoByConnectorCode } from './social';
import { getUserInfoByConnectorCode, getUserInfoFromInteractionResult } from './social';
import { findUserByUsernameAndPassword } from './user';
const assignSignInResult = async (ctx: Context, provider: Provider, userId: string) => {
@ -122,12 +122,15 @@ export const assignRedirectUrlForSocial = async (
export const signInWithSocial = async (
ctx: WithUserLogContext<Context>,
provider: Provider,
{ connectorId, code }: { connectorId: string; code: string }
{ connectorId, code, result }: { connectorId: string; code: string; result?: InteractionResults }
) => {
ctx.userLog.connectorId = connectorId;
ctx.userLog.type = UserLogType.SignInSocial;
const userInfo = await getUserInfoByConnectorCode(connectorId, code);
const userInfo =
code === 'session'
? await getUserInfoFromInteractionResult(connectorId, result)
: await getUserInfoByConnectorCode(connectorId, code);
assertThat(
await hasUserWithIdentity(connectorId, userInfo.id),

View file

@ -1,6 +1,15 @@
import { InteractionResults } from 'oidc-provider';
import { z } from 'zod';
import { getSocialConnectorInstanceById } from '@/connectors';
import { SocialUserInfo } from '@/connectors/types';
import { SocialUserInfo, socialUserInfoGuard } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import assertThat from '@/utils/assert-that';
export interface SocialUserInfoSession {
connectorId: string;
userInfo: SocialUserInfo;
}
const getConnector = async (connectorId: string) => {
try {
@ -27,3 +36,21 @@ export const getUserInfoByConnectorCode = async (
return connector.getUserInfo(accessToken);
};
export const getUserInfoFromInteractionResult = async (
connectorId: string,
interactionResult?: InteractionResults
): Promise<SocialUserInfo> => {
const result = z
.object({
socialUserInfo: z.object({
connectorId: z.string(),
userInfo: socialUserInfoGuard,
}),
})
.parse(interactionResult);
assertThat(result.socialUserInfo.connectorId === connectorId, 'session.insufficient_info');
return result.socialUserInfo.userInfo;
};

View file

@ -48,6 +48,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
jti,
prompt: { name },
result,
} = interaction;
if (name === 'consent') {
@ -62,7 +63,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
if (connectorId && state && !code) {
await assignRedirectUrlForSocial(ctx, connectorId, state);
} else if (connectorId && code) {
await signInWithSocial(ctx, provider, { connectorId, code });
await signInWithSocial(ctx, provider, { connectorId, code, result });
} else if (email && !code) {
await sendSignInWithEmailPasscode(ctx, jti, email);
} else if (email && code) {

View file

@ -64,6 +64,7 @@ const errors = {
invalid_sign_in_method: 'Current sign-in method is not available.',
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: {
not_found: 'Cannot find any available connector for type: {{type}}.',

View file

@ -65,6 +65,7 @@ const errors = {
invalid_sign_in_method: '当前登录方式不可用。',
insufficient_info: '登录信息缺失,请检查您的输入。',
invalid_connector_id: '无法找到 ID 为 {{connectorId}} 的可用连接器。',
connector_id_mismatch: '传入的 connectorId 与 session 中保存的记录不一致。',
},
connector: {
not_found: '找不到可用的 {{type}} 类型的连接器。',