diff --git a/packages/console/src/consts/logs.ts b/packages/console/src/consts/logs.ts index c9598dc6c..886758a4c 100644 --- a/packages/console/src/consts/logs.ts +++ b/packages/console/src/consts/logs.ts @@ -2,7 +2,7 @@ import type { LogKey } from '@logto/schemas'; type LogEventTitle = Record; -/** @deprecated Don't use or update. */ +/** @deprecated Don't use or update. @simeng-li clean up along with session api */ const logEventTitleLegacy: LogEventTitle = Object.freeze({ RegisterUsernamePassword: 'Register with username and password', RegisterEmailSendPasscode: 'Register with email (send passcode)', @@ -30,4 +30,36 @@ export const logEventTitle: Record & Partial ({ verifyIdentifierPayload: jest.fn(), - verifyIdentifier: jest.fn(), + verifyIdentifier: jest.fn().mockResolvedValue({}), verifyProfile: jest.fn(), validateMandatoryUserProfile: jest.fn(), })); diff --git a/packages/core/src/routes/interaction/index.ts b/packages/core/src/routes/interaction/index.ts index 36c8fc66f..e09895db5 100644 --- a/packages/core/src/routes/interaction/index.ts +++ b/packages/core/src/routes/interaction/index.ts @@ -66,7 +66,10 @@ export default function interactionRoutes( koaInteractionSie(), async (ctx, next) => { const { event, identifier, profile } = ctx.guard.body; - const { signInExperience } = ctx; + const { signInExperience, createLog } = ctx; + + const eventLog = createLog(`Interaction.${event}.Update`); + eventLog.append({ event }); verifySignInModeSettings(event, signInExperience); @@ -84,6 +87,8 @@ export default function interactionRoutes( }), ]; + eventLog.append({ profile, verifiedIdentifier }); + await storeInteractionResult( { event, identifiers: verifiedIdentifier, profile }, ctx, @@ -111,12 +116,17 @@ export default function interactionRoutes( koaInteractionSie(), async (ctx, next) => { const { event } = ctx.guard.body; - const { signInExperience, interactionDetails } = ctx; + const { signInExperience, interactionDetails, createLog } = ctx; + + const eventLog = createLog(`Interaction.${event}.Update`); + eventLog.append({ event }); verifySignInModeSettings(event, signInExperience); const interactionStorage = getInteractionStorage(interactionDetails.result); + eventLog.append({ interactionStorage }); + // Forgot Password specific event interaction storage can't be shared with other types of interactions assertThat( event === InteractionEvent.ForgotPassword @@ -144,9 +154,11 @@ export default function interactionRoutes( koaInteractionSie(), async (ctx, next) => { const identifierPayload = ctx.guard.body; - const { signInExperience, interactionDetails } = ctx; + const { signInExperience, interactionDetails, createLog } = ctx; const interactionStorage = getInteractionStorage(interactionDetails.result); + const log = createLog(`Interaction.${interactionStorage.event}.Update`); + if (interactionStorage.event !== InteractionEvent.ForgotPassword) { verifyIdentifierSettings(identifierPayload, signInExperience); } @@ -158,6 +170,8 @@ export default function interactionRoutes( interactionStorage ); + log.append({ identifier: verifiedIdentifier, interactionStorage }); + const identifiers = mergeIdentifiers(verifiedIdentifier, interactionStorage.identifiers); await storeInteractionResult({ identifiers }, ctx, provider, true); @@ -177,10 +191,14 @@ export default function interactionRoutes( koaInteractionSie(), async (ctx, next) => { const profilePayload = ctx.guard.body; - const { signInExperience, interactionDetails } = ctx; + const { signInExperience, interactionDetails, createLog } = ctx; // Check interaction exists - const { event } = getInteractionStorage(interactionDetails.result); + const interactionStorage = getInteractionStorage(interactionDetails.result); + const { event } = interactionStorage; + + const profileLog = createLog(`Interaction.${event}.Profile.Create`); + profileLog.append({ profile: profilePayload, interactionStorage }); if (event !== InteractionEvent.ForgotPassword) { verifyProfileSettings(profilePayload, signInExperience); @@ -210,10 +228,14 @@ export default function interactionRoutes( koaInteractionSie(), async (ctx, next) => { const profilePayload = ctx.guard.body; - const { signInExperience, interactionDetails } = ctx; + const { signInExperience, interactionDetails, createLog } = ctx; const interactionStorage = getInteractionStorage(interactionDetails.result); + const profileLog = createLog(`Interaction.${interactionStorage.event}.Profile.Update`); + + profileLog.append({ profile: profilePayload, interactionStorage, method: 'PATCH' }); + if (interactionStorage.event !== InteractionEvent.ForgotPassword) { verifyProfileSettings(profilePayload, signInExperience); } @@ -238,9 +260,14 @@ export default function interactionRoutes( // Delete Interaction Profile router.delete(`${interactionPrefix}/profile`, async (ctx, next) => { - const { interactionDetails } = ctx; + const { interactionDetails, createLog } = ctx; const interactionStorage = getInteractionStorage(interactionDetails.result); + + const log = createLog(`Interaction.${interactionStorage.event}.Profile.Delete`); + log.append({ interactionStorage }); + const { profile, ...rest } = interactionStorage; + await storeInteractionResult(rest, ctx, provider); ctx.status = 204; @@ -254,12 +281,19 @@ export default function interactionRoutes( koaInteractionSie(), koaInteractionHooks(provider), async (ctx, next) => { - const { interactionDetails } = ctx; + const { interactionDetails, createLog } = ctx; const interactionStorage = getInteractionStorage(interactionDetails.result); const { event } = interactionStorage; + const log = createLog(`Interaction.${event}.Submit`); + log.append({ interaction: interactionStorage }); + const accountVerifiedInteraction = await verifyIdentifier(ctx, provider, interactionStorage); + if (event !== InteractionEvent.Register) { + log.append({ accountId: accountVerifiedInteraction.accountId }); + } + const verifiedInteraction = await verifyProfile(accountVerifiedInteraction); if (event !== InteractionEvent.ForgotPassword) { @@ -278,10 +312,13 @@ export default function interactionRoutes( koaGuard({ body: socialAuthorizationUrlPayloadGuard }), async (ctx, next) => { // Check interaction exists - getInteractionStorage(ctx.interactionDetails.result); + const { event } = getInteractionStorage(ctx.interactionDetails.result); + const log = ctx.createLog(`Interaction.${event}.Identifier.Social.Create`); const { body: payload } = ctx.guard; + log.append(payload); + const redirectTo = await createSocialAuthorizationUrl(ctx, provider, payload); ctx.body = { redirectTo }; diff --git a/packages/core/src/routes/interaction/utils/passcode-validation.ts b/packages/core/src/routes/interaction/utils/passcode-validation.ts index 5d4c5a518..648eb8989 100644 --- a/packages/core/src/routes/interaction/utils/passcode-validation.ts +++ b/packages/core/src/routes/interaction/utils/passcode-validation.ts @@ -44,7 +44,6 @@ export const verifyIdentifierByPasscode = async ( const { event, passcode, ...identifier } = payload; const messageType = getVerificationCodeTypeByEvent(event); - // TODO: @Simeng maybe we should just log all interaction payload in every request? const log = createLog(`Interaction.${event}.Identifier.VerificationCode.Submit`); log.append(identifier); diff --git a/packages/core/src/routes/interaction/utils/social-verification.test.ts b/packages/core/src/routes/interaction/utils/social-verification.test.ts index f09d38386..208bccd24 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.test.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.test.ts @@ -1,6 +1,7 @@ import { ConnectorType } from '@logto/connector-kit'; import { createMockUtils } from '@logto/shared/esm'; +import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; import createMockContext from '#src/test-utils/jest-koa-mocks/create-mock-context.js'; import { createMockLogContext } from '#src/test-utils/koa-audit-log.js'; import { createMockProvider } from '#src/test-utils/oidc-provider.js'; @@ -23,12 +24,15 @@ mockEsm('#src/connectors.js', () => ({ })); const { verifySocialIdentity } = await import('./social-verification.js'); -const log = createMockLogContext(); describe('social-verification', () => { it('verifySocialIdentity', async () => { const provider = createMockProvider(); - const ctx = { ...createMockContext(), ...log }; + // @ts-expect-error test mock context + const ctx: WithLogContext = { + ...createMockContext(), + ...createMockLogContext(), + }; const connectorId = 'connector'; const connectorData = { authCode: 'code' }; const userInfo = await verifySocialIdentity({ connectorId, connectorData }, ctx, provider); diff --git a/packages/core/src/routes/interaction/utils/social-verification.ts b/packages/core/src/routes/interaction/utils/social-verification.ts index 875527727..f310fe637 100644 --- a/packages/core/src/routes/interaction/utils/social-verification.ts +++ b/packages/core/src/routes/interaction/utils/social-verification.ts @@ -1,13 +1,12 @@ import type { ConnectorSession } from '@logto/connector-kit'; import type { SocialConnectorPayload } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; -import type { Context } from 'koa'; import type { Provider } from 'oidc-provider'; import { getLogtoConnectorById } from '#src/connectors/index.js'; import type { SocialUserInfo } from '#src/connectors/types.js'; import { getUserInfoByAuthCode } from '#src/libraries/social.js'; -import type { LogContext } from '#src/middleware/koa-audit-log.js'; +import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; import { getConnectorSessionResult, assignConnectorSessionResult, @@ -17,7 +16,7 @@ import assertThat from '#src/utils/assert-that.js'; import type { SocialAuthorizationUrlPayload } from '../types/index.js'; export const createSocialAuthorizationUrl = async ( - ctx: Context, + ctx: WithLogContext, provider: Provider, payload: SocialAuthorizationUrlPayload ) => { @@ -41,12 +40,10 @@ export const createSocialAuthorizationUrl = async ( export const verifySocialIdentity = async ( { connectorId, connectorData }: SocialConnectorPayload, - ctx: Context, + ctx: WithLogContext, provider: Provider ): Promise => { - // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-unsafe-assignment - const createLog: LogContext['createLog'] = ctx.createLog; - const log = createLog('Interaction.SignIn.Identifier.Social.Submit'); + const log = ctx.createLog('Interaction.SignIn.Identifier.Social.Submit'); log.append({ connectorId, connectorData }); const userInfo = await getUserInfoByAuthCode(connectorId, connectorData, async () => diff --git a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts index ddf4210b3..0594213ec 100644 --- a/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts +++ b/packages/core/src/routes/interaction/verifications/identifier-payload-verification.ts @@ -4,11 +4,11 @@ import type { SocialConnectorPayload, SocialIdentityPayload, } from '@logto/schemas'; -import type { Context } from 'koa'; import type { Provider } from 'oidc-provider'; import RequestError from '#src/errors/RequestError/index.js'; import { verifyUserPassword } from '#src/libraries/user.js'; +import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; import assertThat from '#src/utils/assert-that.js'; import type { @@ -27,9 +27,15 @@ import { verifyIdentifierByPasscode } from '../utils/passcode-validation.js'; import { verifySocialIdentity } from '../utils/social-verification.js'; const verifyPasswordIdentifier = async ( - identifier: PasswordIdentifierPayload + event: InteractionEvent, + identifier: PasswordIdentifierPayload, + ctx: WithLogContext ): Promise => { const { password, ...identity } = identifier; + + const log = ctx.createLog(`Interaction.${event}.Identifier.Password.Submit`); + log.append({ ...identity }); + const user = await findUserByIdentifier(identity); const verifiedUser = await verifyUserPassword(user, password); @@ -43,7 +49,7 @@ const verifyPasswordIdentifier = async ( const verifyPasscodeIdentifier = async ( event: InteractionEvent, identifier: PasscodeIdentifierPayload, - ctx: Context, + ctx: WithLogContext, provider: Provider ): Promise => { const { jti } = await provider.interactionDetails(ctx.req, ctx.res); @@ -57,7 +63,7 @@ const verifyPasscodeIdentifier = async ( const verifySocialIdentifier = async ( identifier: SocialConnectorPayload, - ctx: Context, + ctx: WithLogContext, provider: Provider ): Promise => { const userInfo = await verifySocialIdentity(identifier, ctx, provider); @@ -67,8 +73,12 @@ const verifySocialIdentifier = async ( const verifySocialIdentityInInteractionRecord = async ( { connectorId, identityType }: SocialIdentityPayload, + ctx: WithLogContext, interactionRecord?: AnonymousInteractionResult ): Promise => { + const log = ctx.createLog(`Interaction.SignIn.Identifier.Social.Submit`); + log.append({ connectorId, identityType }); + // Sign-In with social verified email or phone requires a social identifier in the interaction result const socialIdentifierRecord = interactionRecord?.identifiers?.find( (entity): entity is SocialIdentifier => @@ -86,7 +96,7 @@ const verifySocialIdentityInInteractionRecord = async ( }; export default async function identifierPayloadVerification( - ctx: Context, + ctx: WithLogContext, provider: Provider, identifierPayload: IdentifierPayload, interactionStorage: AnonymousInteractionResult @@ -94,7 +104,7 @@ export default async function identifierPayloadVerification( const { event } = interactionStorage; if (isPasswordIdentifier(identifierPayload)) { - return verifyPasswordIdentifier(identifierPayload); + return verifyPasswordIdentifier(event, identifierPayload, ctx); } if (isPasscodeIdentifier(identifierPayload)) { @@ -106,5 +116,5 @@ export default async function identifierPayloadVerification( } // Sign-In with social verified email or phone - return verifySocialIdentityInInteractionRecord(identifierPayload, interactionStorage); + return verifySocialIdentityInInteractionRecord(identifierPayload, ctx, interactionStorage); } diff --git a/packages/schemas/src/types/log/interaction.ts b/packages/schemas/src/types/log/interaction.ts index 3be379c27..8d14c2d17 100644 --- a/packages/schemas/src/types/log/interaction.ts +++ b/packages/schemas/src/types/log/interaction.ts @@ -25,6 +25,8 @@ export enum Action { Update = 'Update', /** Submit updated info to an entity, or submit to the system. (E.g. submit an interaction, submit a verification code to get verified) */ Submit = 'Submit', + /** Delete a existing entity. (E.g delete profile ) */ + Delete = 'Delete', /** Change an entity to the end state. (E.g. end an interaction) */ End = 'End', } @@ -69,11 +71,14 @@ export enum Action { export type LogKey = | `${Prefix}.${Action.Create | Action.End}` | `${Prefix}.${InteractionEvent}.${Action.Update | Action.Submit}` - | `${Prefix}.${InteractionEvent}.${Field.Profile}.${Action.Update}` - | `${Prefix}.${InteractionEvent}.${Field.Identifier}.${Method.VerificationCode}.${ + | `${Prefix}.${InteractionEvent}.${Field.Profile}.${ + | Action.Update // PATCH profile + | Action.Create // PUT profile + | Action.Delete}` + | `${Prefix}.${InteractionEvent}.${Field.Identifier}.${Method.VerificationCode | Method.Social}.${ | Action.Create | Action.Submit}` | `${Prefix}.${InteractionEvent}.${Field.Identifier}.${Exclude< Method, - Method.VerificationCode + Method.VerificationCode | Method.Social >}.${Action.Submit}`;