0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(core): add interaction api logs (#2794)

This commit is contained in:
simeng-li 2022-12-31 18:18:35 +08:00 committed by GitHub
parent bd693fe4d9
commit 5189f6ab9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 115 additions and 31 deletions

View file

@ -2,7 +2,7 @@ import type { LogKey } from '@logto/schemas';
type LogEventTitle = Record<string, string>;
/** @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<string, string | undefined> & Partial<Record<
'ExchangeTokenBy.RefreshToken': 'Exchange token by refresh token',
'Interaction.Create': 'Interaction started',
'Interaction.End': 'Interaction ended',
'Interaction.SignIn.Update': 'Update sign-in interaction',
'Interaction.SignIn.Submit': 'Submit sign-in interaction',
'Interaction.Register.Update': 'Update register interaction',
'Interaction.Register.Submit': 'Submit register interaction',
'Interaction.ForgotPassword.Update': 'Update forgot-password interaction',
'Interaction.ForgotPassword.Submit': 'Submit forgot-password interaction',
'Interaction.SignIn.Profile.Update': 'Patch Update sign-in interaction profile',
'Interaction.SignIn.Profile.Create': 'Put new sign-in interaction profile',
'Interaction.SignIn.Profile.Delete': 'Delete sign-in interaction profile',
'Interaction.Register.Profile.Update': 'Patch update register interaction profile',
'Interaction.Register.Profile.Create': 'Put new register interaction profile',
'Interaction.Register.Profile.Delete': 'Delete register interaction profile',
'Interaction.ForgotPassword.Profile.Update': 'Patch update forgot-password interaction profile',
'Interaction.ForgotPassword.Profile.Create': 'Put new forgot-password interaction profile',
'Interaction.ForgotPassword.Profile.Delete': 'Delete forgot-password interaction profile',
'Interaction.SignIn.Identifier.Password.Submit': 'Submit sign-in identifier with password',
'Interaction.ForgotPassword.Identifier.Password.Submit':
'Submit forgot-password identifier with password',
'Interaction.SignIn.Identifier.VerificationCode.Create':
'Create and send sign-in verification code',
'Interaction.SignIn.Identifier.VerificationCode.Submit':
'Submit and verify sign-in identifier with verification code',
'Interaction.SignIn.Identifier.Social.Create': 'Create social sign-in authorization-url',
'Interaction.SignIn.Identifier.Social.Submit': 'Authenticate and submit social identifier',
'Interaction.Register.Identifier.VerificationCode.Create':
'Create and send register identifier with verification code',
'Interaction.Register.Identifier.VerificationCode.Submit':
'Submit and verify register verification code',
'Interaction.ForgotPassword.Identifier.VerificationCode.Create':
'Create and send forgot-password verification code',
'Interaction.ForgotPassword.Identifier.VerificationCode.Submit':
'Submit and verify forgot-password verification code',
});

View file

@ -68,7 +68,7 @@ const submitInteraction = mockEsmDefault('./actions/submit-interaction.js', () =
const { verifyIdentifierPayload, verifyIdentifier, verifyProfile, validateMandatoryUserProfile } =
await mockEsmWithActual('./verifications/index.js', () => ({
verifyIdentifierPayload: jest.fn(),
verifyIdentifier: jest.fn(),
verifyIdentifier: jest.fn().mockResolvedValue({}),
verifyProfile: jest.fn(),
validateMandatoryUserProfile: jest.fn(),
}));

View file

@ -66,7 +66,10 @@ export default function interactionRoutes<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
}),
];
eventLog.append({ profile, verifiedIdentifier });
await storeInteractionResult(
{ event, identifiers: verifiedIdentifier, profile },
ctx,
@ -111,12 +116,17 @@ export default function interactionRoutes<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
// 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<T extends AnonymousRouter>(
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<T extends AnonymousRouter>(
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 };

View file

@ -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);

View file

@ -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);

View file

@ -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<SocialUserInfo> => {
// 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 () =>

View file

@ -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<AccountIdIdentifier> => {
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<VerifiedEmailIdentifier | VerifiedPhoneIdentifier> => {
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<SocialIdentifier> => {
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<VerifiedEmailIdentifier | VerifiedPhoneIdentifier> => {
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);
}

View file

@ -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}`;