mirror of
https://github.com/logto-io/logto.git
synced 2025-01-20 21:32:31 -05:00
refactor: add comments and fix build
This commit is contained in:
parent
1183e66f95
commit
9c04da0ffe
10 changed files with 134 additions and 26 deletions
|
@ -1,5 +1,5 @@
|
|||
import type { LogDto } from '@logto/schemas';
|
||||
import { LogResult } from '@logto/schemas';
|
||||
import type { LogDto } from '@logto/schemas/lib/types/log-legacy.js';
|
||||
import { conditional, conditionalString } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { LogDto, User } from '@logto/schemas';
|
||||
import type { User } from '@logto/schemas';
|
||||
import type { LogDto } from '@logto/schemas/lib/types/log-legacy.js';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
|
|
@ -26,7 +26,7 @@ mockEsm('nanoid', () => ({
|
|||
const koaLog = await pickDefault(import('./koa-audit-log.js'));
|
||||
|
||||
describe('koaLog middleware', () => {
|
||||
const logKey: LogKey = 'SignIn.Username.Passcode.Submit';
|
||||
const logKey: LogKey = 'SignIn.Username.VerificationCode.Submit';
|
||||
const mockPayload: LogPayload = {
|
||||
userId: 'foo',
|
||||
username: 'Bar',
|
||||
|
|
|
@ -13,7 +13,7 @@ const removeUndefinedKeys = (object: Record<string, unknown>) =>
|
|||
|
||||
export type LogPayload = Partial<LogContextPayload> & Record<string, unknown>;
|
||||
|
||||
type LogFunction = {
|
||||
export type LogFunction = {
|
||||
(data: Readonly<LogPayload>): void;
|
||||
setKey: (key: LogKey) => void;
|
||||
};
|
||||
|
@ -25,6 +25,41 @@ export type LogContext = {
|
|||
export type WithLogContext<ContextT extends IRouterParamContext = IRouterParamContext> = ContextT &
|
||||
LogContext;
|
||||
|
||||
/**
|
||||
* The factory to create a new audit log middleware function.
|
||||
* It will inject a {@link LogFunction} property named `log` to the context to enable audit logging.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* You need to explicitly call `ctx.log.setKey()` to set a {@link LogKey} thus the log can be categorized and indexed in database:
|
||||
*
|
||||
* ```ts
|
||||
* ctx.log.setKey('SignIn.Submit'); // Key is typed
|
||||
* ```
|
||||
*
|
||||
* To log data, call `ctx.log()`. It'll use object spread operators to update data (i.e. merge with one-level overwrite and shallow copy).
|
||||
*
|
||||
* ```ts
|
||||
* ctx.log({ applicationId: 'foo' });
|
||||
* ```
|
||||
*
|
||||
* The data has a initial value:
|
||||
*
|
||||
* ```ts
|
||||
* {
|
||||
* key: LogKeyUnknown,
|
||||
* result: LogResult.Success,
|
||||
* ip, // Extract from request
|
||||
* userAgent, // Extract from request
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Note: Both of the functions can be called multiple times.
|
||||
*
|
||||
* @see {@link LogKey} for all available log keys, and {@link LogResult} for result enums.
|
||||
* @see {@link LogContextPayload} for the basic type suggestion of log data.
|
||||
* @returns An audit log middleware function.
|
||||
*/
|
||||
export default function koaAuditLog<
|
||||
StateT,
|
||||
ContextT extends IRouterParamContext,
|
||||
|
|
|
@ -29,7 +29,7 @@ export const sendPasscodeToIdentifier = async (
|
|||
const identifierType =
|
||||
'email' in identifier ? interaction.Identifier.Email : interaction.Identifier.Phone;
|
||||
|
||||
log.setKey(`${event}.${identifierType}.Passcode.Create`);
|
||||
log.setKey(`${event}.${identifierType}.VerificationCode.Create`);
|
||||
log(identifier);
|
||||
|
||||
const passcode = await createPasscode(jti, passcodeType, identifier);
|
||||
|
@ -51,7 +51,7 @@ export const verifyIdentifierByPasscode = async (
|
|||
const identifierType =
|
||||
'email' in identifier ? interaction.Identifier.Email : interaction.Identifier.Phone;
|
||||
|
||||
log.setKey(`${event}.${identifierType}.Passcode.Submit`);
|
||||
log.setKey(`${event}.${identifierType}.VerificationCode.Submit`);
|
||||
|
||||
await verifyPasscode(jti, passcodeType, passcode, identifier);
|
||||
};
|
||||
|
|
|
@ -7,9 +7,8 @@ import { stringifyError } from './format.js';
|
|||
import { isEnum } from './type.js';
|
||||
|
||||
/**
|
||||
* OIDC provider listeners and events
|
||||
* https://github.com/panva/node-oidc-provider/blob/main/docs/README.md#im-getting-a-client-authentication-failed-error-with-no-details
|
||||
* https://github.com/panva/node-oidc-provider/blob/v7.x/docs/events.md
|
||||
* @see {@link https://github.com/panva/node-oidc-provider/blob/v7.x/docs/README.md#im-getting-a-client-authentication-failed-error-with-no-details Getting auth error with no details?}
|
||||
* @see {@link https://github.com/panva/node-oidc-provider/blob/v7.x/docs/events.md OIDC Provider events}
|
||||
*/
|
||||
export const addOidcEventListeners = (provider: Provider) => {
|
||||
provider.addListener('grant.success', grantListener);
|
||||
|
@ -17,6 +16,10 @@ export const addOidcEventListeners = (provider: Provider) => {
|
|||
provider.addListener('grant.revoked', grantRevocationListener);
|
||||
};
|
||||
|
||||
/**
|
||||
* @see {@link https://github.com/panva/node-oidc-provider/blob/v7.x/lib/actions/token.js#L71 Success event emission}
|
||||
* @see {@link https://github.com/panva/node-oidc-provider/blob/v7.x/lib/shared/error_handler.js OIDC Provider error handler}
|
||||
*/
|
||||
export const grantListener = async (
|
||||
ctx: KoaContextWithOIDC & WithLogContext & { body: GrantBody },
|
||||
error?: errors.OIDCProviderError
|
||||
|
@ -47,7 +50,7 @@ export const grantListener = async (
|
|||
});
|
||||
};
|
||||
|
||||
// The grant.revoked event is emitted at https://github.com/panva/node-oidc-provider/blob/564b1095ee869c89381d63dfdb5875c99f870f5f/lib/helpers/revoke.js#L25
|
||||
// The grant.revoked event is emitted at https://github.com/panva/node-oidc-provider/blob/v7.x/lib/helpers/revoke.js#L25
|
||||
export const grantRevocationListener = async (
|
||||
ctx: KoaContextWithOIDC & WithLogContext,
|
||||
grantId: string
|
||||
|
@ -65,9 +68,7 @@ export const grantRevocationListener = async (
|
|||
};
|
||||
|
||||
/**
|
||||
* See https://github.com/panva/node-oidc-provider/tree/main/lib/actions/grants
|
||||
* - https://github.com/panva/node-oidc-provider/blob/564b1095ee869c89381d63dfdb5875c99f870f5f/lib/actions/grants/authorization_code.js#L209
|
||||
* - https://github.com/panva/node-oidc-provider/blob/564b1095ee869c89381d63dfdb5875c99f870f5f/lib/actions/grants/refresh_token.js#L225
|
||||
* @see {@link https://github.com/panva/node-oidc-provider/tree/v7.x/lib/actions/grants grants source code} for predefined grant implementations and types.
|
||||
*/
|
||||
type GrantBody = {
|
||||
access_token?: string;
|
||||
|
@ -91,12 +92,11 @@ const getExchangeByType = (grantType: unknown): token.ExchangeByType => {
|
|||
};
|
||||
|
||||
/**
|
||||
* See [OAuth 2.0 Token Revocation](https://datatracker.ietf.org/doc/html/rfc7009) for RFC reference.
|
||||
*
|
||||
* Note the revocation may revoke related tokens as well. In oidc-provider, it will revoke the whole Grant when revoking Refresh Token.
|
||||
* So we don't assume the token type here.
|
||||
*
|
||||
* See [this function](https://github.com/panva/node-oidc-provider/blob/433d131989558e24c0c74970d2d700af2199485d/lib/actions/revocation.js#L56) for code reference.
|
||||
* @see {@link https://datatracker.ietf.org/doc/html/rfc7009 OAuth 2.0 Token Revocation} for RFC reference.
|
||||
* @see {@link https://github.com/panva/node-oidc-provider/blob/v7.x/lib/actions/revocation.js#L56 this function} for code reference.
|
||||
**/
|
||||
const getRevocationTokenTypes = (oidc: KoaContextWithOIDC['oidc']): token.TokenType[] => {
|
||||
return Object.values(token.TokenType).filter((value) => oidc.entities[value]);
|
||||
|
|
|
@ -47,9 +47,9 @@ export const socialIdentityPayloadGuard = z.object({
|
|||
});
|
||||
export type SocialIdentityPayload = z.infer<typeof socialIdentityPayloadGuard>;
|
||||
|
||||
/**
|
||||
* Interaction Payload Guard
|
||||
*/
|
||||
// Interaction Payload Guard
|
||||
|
||||
/** Interaction flow (main flow) types. */
|
||||
export enum Event {
|
||||
SignIn = 'SignIn',
|
||||
Register = 'Register',
|
||||
|
|
|
@ -7,15 +7,29 @@ import type * as token from './token.js';
|
|||
export * as interaction from './interaction.js';
|
||||
export * as token from './token.js';
|
||||
|
||||
/** Fallback for empty or unrecognized log keys. */
|
||||
export const LogKeyUnknown = 'Unknown';
|
||||
|
||||
export type LogKey = interaction.LogKey | token.LogKey | typeof LogKeyUnknown;
|
||||
/**
|
||||
* The union type of all available log keys.
|
||||
* Note duplicate keys are allowed but should be avoided.
|
||||
*
|
||||
* @see {@link interaction.LogKey} for interaction log keys.
|
||||
* @see {@link token.LogKey} for token log keys.
|
||||
**/
|
||||
export type LogKey = typeof LogKeyUnknown | interaction.LogKey | token.LogKey;
|
||||
|
||||
export enum LogResult {
|
||||
Success = 'Success',
|
||||
Error = 'Error',
|
||||
}
|
||||
|
||||
/**
|
||||
* The basic log context type. It's more about a type hint instead of forcing the log shape.
|
||||
*
|
||||
* Note when setting up a log function, the type of log key in function arguments should be `LogKey`.
|
||||
* Here we use `string` to make it compatible with the Zod guard.
|
||||
**/
|
||||
export type LogContextPayload = {
|
||||
key: string;
|
||||
result: LogResult;
|
||||
|
@ -26,6 +40,7 @@ export type LogContextPayload = {
|
|||
sessionId?: string;
|
||||
};
|
||||
|
||||
/** Type guard for {@link LogContextPayload} */
|
||||
export const logContextGuard: ZodType<LogContextPayload> = z.object({
|
||||
key: z.string(),
|
||||
result: z.nativeEnum(LogResult),
|
||||
|
|
|
@ -4,6 +4,7 @@ import Flow = Event;
|
|||
|
||||
export { Flow };
|
||||
|
||||
/** Identifier to verify. */
|
||||
export enum Identifier {
|
||||
Username = 'Username',
|
||||
Email = 'Email',
|
||||
|
@ -11,29 +12,85 @@ export enum Identifier {
|
|||
SocialId = 'SocialId',
|
||||
}
|
||||
|
||||
/** Method to verify the identifier */
|
||||
export enum Method {
|
||||
Password = 'Password',
|
||||
Passcode = 'Passcode',
|
||||
VerificationCode = 'VerificationCode',
|
||||
Social = 'Social',
|
||||
}
|
||||
|
||||
export enum Action {
|
||||
/** 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',
|
||||
/** Create a new entity. (E.g. create an interaction, create a verification code) */
|
||||
Create = 'Create',
|
||||
}
|
||||
|
||||
/**
|
||||
* The forgot password flow log key type. Format:
|
||||
*
|
||||
* ```ts
|
||||
* `ForgotPassword.${Identifier}.VerificationCode.${Action}`
|
||||
* ```
|
||||
*
|
||||
* Since we can use only verification code for resetting password,
|
||||
* {@link Identifier.SocialId} is excluded and the method is fixed to {@link Method.VerificationCode}.
|
||||
*
|
||||
* {@link Action} definition:
|
||||
*
|
||||
* - {@link Action.Create} indicates the process of generating and sending a verification code.
|
||||
* - {@link Action.Submit} indicates the process of submitting a verification code to get verified.
|
||||
*/
|
||||
export type ForgotPasswordLogKey = `${Flow.ForgotPassword}.${Exclude<
|
||||
Identifier,
|
||||
'SocialId'
|
||||
>}.${Method.Passcode}.${Action}`;
|
||||
>}.${Method.VerificationCode}.${Action}`;
|
||||
|
||||
type SignInRegisterFlow = Exclude<Flow, 'ForgotPassword'>;
|
||||
|
||||
/**
|
||||
* The sign-in / register flow log key type. Format:
|
||||
*
|
||||
* ```ts
|
||||
* `${Flow}.${Identifier}.${Method}.${Action}`
|
||||
* ```
|
||||
*
|
||||
* Restrictions:
|
||||
*
|
||||
* - For social identifier and method, the value can only be `SignIn.SocialId.Social.Submit`.
|
||||
* - For password method, the action can only be `Submit`.
|
||||
*
|
||||
* @see {@link SignInRegisterFlow}, {@link Identifier}, {@link Method}, {@link Action} for all available enums.
|
||||
*/
|
||||
export type SignInRegisterLogKey =
|
||||
| `${Flow.SignIn}.${Identifier.SocialId}.${Method.Social}.${Action.Submit}`
|
||||
| `${SignInRegisterFlow}.${Exclude<Identifier, 'SocialId'>}.${Method.Password}.${Action.Submit}`
|
||||
| `${SignInRegisterFlow}.${Exclude<Identifier, 'SocialId'>}.${Method.Passcode}.${Action}`;
|
||||
| `${SignInRegisterFlow}.${Exclude<Identifier, 'SocialId'>}.${Method.VerificationCode}.${Action}`;
|
||||
|
||||
export type FlowLogKey = `${Flow}.${Action}`;
|
||||
|
||||
/**
|
||||
* The union type of all available log keys for interaction.
|
||||
*
|
||||
* There are two formats of a key:
|
||||
*
|
||||
* ```ts
|
||||
* `${Flow}.${Identifier}.${Method}.${Action}`
|
||||
* `${Flow}.${Action}`
|
||||
* ```
|
||||
*
|
||||
* The key MUST describe an {@link Action}:
|
||||
*
|
||||
* - {@link Action.Submit} (submit updated info to an entity, or submit to the system);
|
||||
* - {@link Action.Create} (create a new entity).
|
||||
*
|
||||
* In an interaction, ONLY the interaction itself and verification codes can be created, i.e.:
|
||||
*
|
||||
* ```ts
|
||||
* `${Flow}.Create`
|
||||
* `${Flow}.${Identifier}.VerificationCode.Create`
|
||||
* ```
|
||||
*
|
||||
* There may be more restrictions, please see the specific type to learn more.
|
||||
*/
|
||||
export type LogKey = ForgotPasswordLogKey | SignInRegisterLogKey | FlowLogKey;
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/** The type of a token flow. */
|
||||
export enum Flow {
|
||||
ExchangeTokenBy = 'ExchangeTokenBy',
|
||||
RevokeToken = 'RevokeToken',
|
||||
}
|
||||
|
||||
/**
|
||||
* Default grant type extracted from [oidc-provider](https://github.com/panva/node-oidc-provider/blob/564b1095ee869c89381d63dfdb5875c99f870f5f/lib/helpers/revoke.js#L13).
|
||||
*/
|
||||
/** Available grant token types extracted from [oidc-provider](https://github.com/panva/node-oidc-provider/blob/564b1095ee869c89381d63dfdb5875c99f870f5f/lib/helpers/revoke.js#L13). */
|
||||
export enum TokenType {
|
||||
AccessToken = 'AccessToken',
|
||||
RefreshToken = 'RefreshToken',
|
||||
|
@ -15,6 +14,7 @@ export enum TokenType {
|
|||
BackchannelAuthenticationRequest = 'BackchannelAuthenticationRequest',
|
||||
}
|
||||
|
||||
/** The credential to request a grant. */
|
||||
export enum ExchangeByType {
|
||||
Unknown = 'Unknown',
|
||||
AuthorizationCode = 'AuthorizationCode',
|
||||
|
|
Loading…
Add table
Reference in a new issue