From f773957ad4ef64e9fb3ceac3b97bbf8538249846 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 23 Jul 2024 14:57:54 +0800 Subject: [PATCH] refactor(core): log app secret name --- packages/core/src/event-listeners/grant.ts | 3 +- packages/core/src/event-listeners/utils.ts | 4 ++- .../koa-app-secret-transpilation.ts | 13 +++++-- .../api/oidc/client-authentication.test.ts | 34 ++++++++++++++++++- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/packages/core/src/event-listeners/grant.ts b/packages/core/src/event-listeners/grant.ts index b12b591b5..d767eea03 100644 --- a/packages/core/src/event-listeners/grant.ts +++ b/packages/core/src/event-listeners/grant.ts @@ -1,6 +1,7 @@ import { GrantType, LogResult, token } from '@logto/schemas'; import type { errors, KoaContextWithOIDC } from 'oidc-provider'; +import { type WithAppSecretContext } from '#src/middleware/koa-app-secret-transpilation.js'; import type { WithLogContext } from '#src/middleware/koa-audit-log.js'; import { stringifyError } from '../utils/format.js'; @@ -13,7 +14,7 @@ import { extractInteractionContext } from './utils.js'; * @see {@link https://github.com/panva/node-oidc-provider/blob/v7.x/lib/shared/error_handler.js OIDC Provider error handler} */ export const grantListener = ( - ctx: KoaContextWithOIDC & WithLogContext & { body: GrantBody }, + ctx: KoaContextWithOIDC & WithLogContext & WithAppSecretContext & { body: GrantBody }, error?: errors.OIDCProviderError ) => { const { params } = ctx.oidc; diff --git a/packages/core/src/event-listeners/utils.ts b/packages/core/src/event-listeners/utils.ts index 1cdb66763..7359ae0e8 100644 --- a/packages/core/src/event-listeners/utils.ts +++ b/packages/core/src/event-listeners/utils.ts @@ -1,10 +1,11 @@ import type { IRouterParamContext } from 'koa-router'; import type { KoaContextWithOIDC } from 'oidc-provider'; +import { type WithAppSecretContext } from '#src/middleware/koa-app-secret-transpilation.js'; import type { LogPayload } from '#src/middleware/koa-audit-log.js'; export const extractInteractionContext = ( - ctx: IRouterParamContext & KoaContextWithOIDC + ctx: IRouterParamContext & KoaContextWithOIDC & WithAppSecretContext ): LogPayload => { const { entities: { Account, Session, Client, Interaction }, @@ -13,6 +14,7 @@ export const extractInteractionContext = ( return { applicationId: Client?.clientId, + applicationSecret: ctx.appSecret, sessionId: Session?.jti, interactionId: Interaction?.jti, userId: Account?.accountId, diff --git a/packages/core/src/middleware/koa-app-secret-transpilation.ts b/packages/core/src/middleware/koa-app-secret-transpilation.ts index 19201383a..1e852a4b6 100644 --- a/packages/core/src/middleware/koa-app-secret-transpilation.ts +++ b/packages/core/src/middleware/koa-app-secret-transpilation.ts @@ -1,5 +1,5 @@ import type { Nullable } from '@silverhand/essentials'; -import type { MiddlewareType } from 'koa'; +import type { Context, MiddlewareType } from 'koa'; import { errors } from 'oidc-provider'; import type Queries from '#src/tenants/Queries.js'; @@ -14,6 +14,14 @@ function decodeAuthToken(token: string) { return authToken; } +export type WithAppSecretContext = ContextT & { + /** The application secret that has been transpiled during the middleware execution. */ + appSecret?: { + /** The application secret name of the application (client). */ + name: string; + }; +}; + /** @import { getConstantClientMetadata } from '#src/oidc/utils.js'; */ /** * Create a middleware function that reads the app secret from the request and check if it matches @@ -39,7 +47,7 @@ function decodeAuthToken(token: string) { */ export default function koaAppSecretTranspilation( queries: Queries -): MiddlewareType> { +): MiddlewareType, Nullable> { return async (ctx, next) => { type ClientCredentials = Partial<{ clientId: string; @@ -127,6 +135,7 @@ export default function koaAppSecretTranspilation + getAuditLogs( + new URLSearchParams({ + logKey: `${token.Type.ExchangeTokenBy}.${token.ExchangeByType.ClientCredentials}`, + }) + ); + +const expectLog = (applicationId: string, secretName: string) => + expect.objectContaining({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + payload: expect.objectContaining({ + applicationId, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + applicationSecret: expect.objectContaining({ name: secretName }), + }), + }); + afterAll(async () => { await deleteApplication(application.id).catch(noop); }); @@ -155,11 +173,14 @@ devFeatureTest.describe('client authentication', () => { }); it('should pass when client credentials are valid in authorization header', async () => { + const application = await createApplication('application', ApplicationType.MachineToMachine); const secret = await createApplicationSecret({ applicationId: application.id, name: randomString(), }); + const beforeLogs = await getLogs(); + expect(beforeLogs).not.toContainEqual(expectLog(application.id, secret.name)); await expect( post({ authorization: `Basic ${Buffer.from(`${application.id}:${secret.value}`).toString( @@ -170,14 +191,21 @@ devFeatureTest.describe('client authentication', () => { ).resolves.toMatchObject({ token_type: 'Bearer', }); + + const logs = await getLogs(); + expect(logs).toContainEqual(expectLog(application.id, secret.name)); + await deleteApplication(application.id); }); it('should pass when client credentials are valid in body', async () => { + const application = await createApplication('application', ApplicationType.MachineToMachine); const secret = await createApplicationSecret({ applicationId: application.id, name: randomString(), }); + const beforeLogs = await getLogs(); + expect(beforeLogs).not.toContainEqual(expectLog(application.id, secret.name)); await expect( post({ body: { @@ -206,5 +234,9 @@ devFeatureTest.describe('client authentication', () => { token_type: 'Bearer', }); } + + const logs = await getLogs(); + expect(logs).toContainEqual(expectLog(application.id, secret.name)); + await deleteApplication(application.id); }); });