mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
Merge pull request #6312 from logto-io/gao-log-app-secret
refactor(core): log app secret name
This commit is contained in:
commit
98dbead1bb
4 changed files with 49 additions and 5 deletions
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = Context> = 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<StateT, ContextT, ResponseBodyT>(
|
||||
queries: Queries
|
||||
): MiddlewareType<StateT, ContextT, Nullable<ResponseBodyT>> {
|
||||
): MiddlewareType<StateT, WithAppSecretContext<ContextT>, Nullable<ResponseBodyT>> {
|
||||
return async (ctx, next) => {
|
||||
type ClientCredentials = Partial<{
|
||||
clientId: string;
|
||||
|
@ -127,6 +135,7 @@ export default function koaAppSecretTranspilation<StateT, ContextT, ResponseBody
|
|||
ctx.query.client_secret = result.originalSecret;
|
||||
}
|
||||
|
||||
ctx.appSecret = { name: result.name };
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { ApplicationType, token } from '@logto/schemas';
|
||||
import { noop, removeUndefinedKeys } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'ky';
|
||||
|
||||
|
@ -18,6 +18,7 @@ import {
|
|||
createApplicationSecret,
|
||||
deleteApplication,
|
||||
} from '#src/api/application.js';
|
||||
import { getAuditLogs } from '#src/api/index.js';
|
||||
import { createResource } from '#src/api/resource.js';
|
||||
import { devFeatureTest, randomString, waitFor } from '#src/utils.js';
|
||||
|
||||
|
@ -33,6 +34,23 @@ const [application, resource] = await Promise.all([
|
|||
createResource(),
|
||||
]);
|
||||
|
||||
const getLogs = async () =>
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue