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:
parent
bd693fe4d9
commit
5189f6ab9f
8 changed files with 115 additions and 31 deletions
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
}));
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () =>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}`;
|
||||
|
|
Loading…
Add table
Reference in a new issue