mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -05:00
feat(core): grantErrorListener for logging token exchange error (#894)
* feat(core): grantErrorListener for logging token exchange error * refactor(core): extract getLogType * refactor(core): oidc provider event listeners will skip logging if found unexpected grant_type * test(core): oidc provider event listeners will skip logging if found unexpected grant_type
This commit is contained in:
parent
97e6bdd8aa
commit
797344f6f5
3 changed files with 153 additions and 12 deletions
|
@ -12,7 +12,7 @@ import { isOriginAllowed, validateCustomClientMetadata } from '@/oidc/utils';
|
|||
import { findResourceByIndicator } from '@/queries/resource';
|
||||
import { findUserById } from '@/queries/user';
|
||||
import { routes } from '@/routes/consts';
|
||||
import { grantSuccessListener } from '@/utils/oidc-provider-event-listener';
|
||||
import { grantErrorListener, grantSuccessListener } from '@/utils/oidc-provider-event-listener';
|
||||
|
||||
export default async function initOidc(app: Koa): Promise<Provider> {
|
||||
const { issuer, privateKey, defaultIdTokenTtl, defaultRefreshTokenTtl } = envSet.values.oidc;
|
||||
|
@ -118,6 +118,7 @@ export default async function initOidc(app: Koa): Promise<Provider> {
|
|||
});
|
||||
|
||||
oidc.on('grant.success', grantSuccessListener);
|
||||
oidc.on('grant.error', grantErrorListener);
|
||||
|
||||
app.use(mount('/oidc', oidc.app));
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { grantSuccessListener } from '@/utils/oidc-provider-event-listener';
|
||||
import { LogResult } from '@logto/schemas';
|
||||
|
||||
import { grantErrorListener, grantSuccessListener } from '@/utils/oidc-provider-event-listener';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
const addLogContext = jest.fn();
|
||||
const log = jest.fn();
|
||||
|
||||
describe('grantSuccessListener', () => {
|
||||
const userId = 'userIdValue';
|
||||
const sessionId = 'sessionIdValue';
|
||||
|
@ -11,9 +16,6 @@ describe('grantSuccessListener', () => {
|
|||
Client: { clientId: applicationId },
|
||||
};
|
||||
|
||||
const addLogContext = jest.fn();
|
||||
const log = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -95,4 +97,96 @@ describe('grantSuccessListener', () => {
|
|||
userId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not log when it found unexpected grant_type', async () => {
|
||||
const parameters = { grant_type: 'client_credentials' };
|
||||
const ctx = {
|
||||
...createContextWithRouteParameters(),
|
||||
addLogContext,
|
||||
log,
|
||||
oidc: { entities, params: parameters },
|
||||
body: {},
|
||||
};
|
||||
|
||||
// @ts-expect-error pass complex type check to mock ctx directly
|
||||
await grantSuccessListener(ctx);
|
||||
expect(addLogContext).not.toHaveBeenCalled();
|
||||
expect(log).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('grantErrorListener', () => {
|
||||
const applicationId = 'applicationIdValue';
|
||||
const entities = { Client: { clientId: applicationId } };
|
||||
const errorMessage = 'invalid grant';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should log type CodeExchangeToken when grant type is authorization_code', async () => {
|
||||
const parameters = { grant_type: 'authorization_code', code: 'codeValue' };
|
||||
const ctx = {
|
||||
...createContextWithRouteParameters(),
|
||||
addLogContext,
|
||||
log,
|
||||
oidc: { entities, params: parameters },
|
||||
body: {
|
||||
access_token: 'newAccessTokenValue',
|
||||
refresh_token: 'newRefreshTokenValue',
|
||||
id_token: 'newIdToken',
|
||||
scope: 'openid offline-access',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error pass complex type check to mock ctx directly
|
||||
await grantErrorListener(ctx, new Error(errorMessage));
|
||||
expect(addLogContext).toHaveBeenCalledWith({ applicationId });
|
||||
expect(log).toHaveBeenCalledWith('CodeExchangeToken', {
|
||||
result: LogResult.Error,
|
||||
error: `Error: ${errorMessage}`,
|
||||
params: parameters,
|
||||
});
|
||||
});
|
||||
|
||||
it('should log type RefreshTokenExchangeToken when grant type is refresh_code', async () => {
|
||||
const parameters = { grant_type: 'refresh_token', refresh_token: 'refreshTokenValue' };
|
||||
const ctx = {
|
||||
...createContextWithRouteParameters(),
|
||||
addLogContext,
|
||||
log,
|
||||
oidc: { entities, params: parameters },
|
||||
body: {
|
||||
access_token: 'newAccessTokenValue',
|
||||
refresh_token: 'newRefreshTokenValue',
|
||||
id_token: 'newIdToken',
|
||||
scope: 'openid offline-access',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error pass complex type check to mock ctx directly
|
||||
await grantErrorListener(ctx, new Error(errorMessage));
|
||||
expect(addLogContext).toHaveBeenCalledWith({ applicationId });
|
||||
expect(log).toHaveBeenCalledWith('RefreshTokenExchangeToken', {
|
||||
result: LogResult.Error,
|
||||
error: `Error: ${errorMessage}`,
|
||||
params: parameters,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not log when it found unexpected grant_type', async () => {
|
||||
const parameters = { grant_type: 'client_credentials' };
|
||||
const ctx = {
|
||||
...createContextWithRouteParameters(),
|
||||
addLogContext,
|
||||
log,
|
||||
oidc: { entities, params: parameters },
|
||||
body: {},
|
||||
};
|
||||
|
||||
// @ts-expect-error pass complex type check to mock ctx directly
|
||||
await grantErrorListener(ctx, new Error(errorMessage));
|
||||
expect(addLogContext).not.toHaveBeenCalled();
|
||||
expect(log).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { GrantType, IssuedTokenType, LogType } from '@logto/schemas';
|
||||
import { GrantType, IssuedTokenType, LogResult } from '@logto/schemas';
|
||||
import { notFalsy } from '@silverhand/essentials';
|
||||
import { KoaContextWithOIDC } from 'oidc-provider';
|
||||
import { errors, KoaContextWithOIDC } from 'oidc-provider';
|
||||
|
||||
import { WithLogContext } from '@/middleware/koa-log';
|
||||
|
||||
|
@ -14,9 +14,24 @@ interface GrantBody {
|
|||
access_token?: string;
|
||||
refresh_token?: string;
|
||||
id_token?: string;
|
||||
scope?: string;
|
||||
scope?: string; // AccessToken.scope
|
||||
}
|
||||
|
||||
const getLogType = (grantType: unknown) => {
|
||||
if (
|
||||
!grantType ||
|
||||
![GrantType.AuthorizationCode, GrantType.RefreshToken].includes(grantType as GrantType)
|
||||
) {
|
||||
console.error('Unexpected grant_type:', grantType);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return grantType === GrantType.AuthorizationCode
|
||||
? 'CodeExchangeToken'
|
||||
: 'RefreshTokenExchangeToken';
|
||||
};
|
||||
|
||||
export const grantSuccessListener = async (
|
||||
ctx: KoaContextWithOIDC & WithLogContext & { body: GrantBody }
|
||||
) => {
|
||||
|
@ -27,6 +42,13 @@ export const grantSuccessListener = async (
|
|||
},
|
||||
body,
|
||||
} = ctx;
|
||||
|
||||
const logType = getLogType(params?.grant_type);
|
||||
|
||||
if (!logType) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.addLogContext({
|
||||
applicationId: client?.clientId,
|
||||
sessionId: grant?.jti,
|
||||
|
@ -39,13 +61,37 @@ export const grantSuccessListener = async (
|
|||
id_token && 'idToken',
|
||||
].filter((value): value is IssuedTokenType => notFalsy(value));
|
||||
|
||||
const grantType = params?.grant_type;
|
||||
const type: LogType =
|
||||
grantType === GrantType.AuthorizationCode ? 'CodeExchangeToken' : 'RefreshTokenExchangeToken';
|
||||
ctx.log(type, {
|
||||
ctx.log(logType, {
|
||||
userId: account?.accountId,
|
||||
params,
|
||||
issued,
|
||||
scope,
|
||||
});
|
||||
};
|
||||
|
||||
export const grantErrorListener = async (
|
||||
ctx: KoaContextWithOIDC & WithLogContext & { body: GrantBody },
|
||||
error: errors.OIDCProviderError
|
||||
) => {
|
||||
const {
|
||||
oidc: {
|
||||
entities: { Client: client },
|
||||
params,
|
||||
},
|
||||
} = ctx;
|
||||
|
||||
const logType = getLogType(params?.grant_type);
|
||||
|
||||
if (!logType) {
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.addLogContext({
|
||||
applicationId: client?.clientId,
|
||||
});
|
||||
ctx.log(logType, {
|
||||
result: LogResult.Error,
|
||||
error: String(error),
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue