0
Fork 0
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:
IceHe.xyz 2022-05-19 19:48:34 +08:00 committed by GitHub
parent 97e6bdd8aa
commit 797344f6f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 153 additions and 12 deletions

View file

@ -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));

View file

@ -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();
});
});

View file

@ -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,
});
};