Skip to content

Commit

Permalink
refactor(core): improve oidc error handling (#4573)
Browse files Browse the repository at this point in the history
* refactor(core): improve oidc error handling

* refactor(core): fix tests
  • Loading branch information
gao-sun authored Sep 25, 2023
1 parent 7b43dd5 commit b8e592d
Show file tree
Hide file tree
Showing 25 changed files with 247 additions and 167 deletions.
4 changes: 3 additions & 1 deletion packages/cli/src/commands/translate/sync-keys/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ export const parseLocaleFiles = (filePath: string): ParsedTuple => {
// eslint-disable-next-line @silverhand/fp/no-mutation
nestedObject[key] = [value, false];
} else {
consoleLog.fatal('Unsupported comment:', comment);
// eslint-disable-next-line @silverhand/fp/no-mutation
nestedObject[key] = [value, true];
consoleLog.warn('Unsupported comment:', comment);
}
} else {
// eslint-disable-next-line @silverhand/fp/no-mutation
Expand Down
9 changes: 9 additions & 0 deletions packages/core/jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
* Setup environment variables for unit test
*/

import en from '@logto/phrases/lib/locales/en/index.js';
import { createMockUtils } from '@logto/shared/esm';
import { init } from 'i18next';

const { jest } = import.meta;
const { mockEsm, mockEsmDefault } = createMockUtils(jest);
Expand Down Expand Up @@ -32,3 +34,10 @@ mockEsmDefault('#src/env-set/oidc.js', () => () => ({
// Logger is not considered in all test cases
// eslint-disable-next-line unicorn/consistent-function-scoping
mockEsm('koa-logger', () => ({ default: () => (_, next) => next() }));

// Init i18next and load en locale only
await init({
fallbackLng: 'en',
supportedLngs: ['en'],
resources: { en },
});
2 changes: 1 addition & 1 deletion packages/core/src/libraries/hook/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('testHook', () => {
await expect(testHook(hook.id, [HookEvent.PostSignIn], hook.config)).rejects.toThrowError(
new RequestError({
code: 'hook.send_test_payload_failed',
message: 'test error',
message: 'Error: test error',
status: 422,
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('validate sign-up', () => {
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Sms,
type: ConnectorType.Email,
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ describe('koaConnectorErrorHandler middleware', () => {
{
code: 'connector.general',
status: 400,
errorDescription: JSON.stringify({ message }),
},
{ message }
)
Expand All @@ -235,7 +236,7 @@ describe('koaConnectorErrorHandler middleware', () => {
{
code: 'connector.general',
status: 400,
errorDescription: '\nMock General connector errors',
errorDescription: 'Mock General connector errors',
},
message
)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/middleware/koa-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function koaErrorHandler<StateT, ContextT, BodyT>(): Middleware<
try {
await next();
} catch (error: unknown) {
if (!EnvSet.values.isProduction) {
if (!EnvSet.values.isUnitTest && !EnvSet.values.isProduction) {
consoleLog.error(error);
}

Expand Down
136 changes: 58 additions & 78 deletions packages/core/src/middleware/koa-oidc-error-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,118 +1,98 @@
import i18next from 'i18next';
import { type Context } from 'koa';
import { errors } from 'oidc-provider';

import RequestError from '#src/errors/RequestError/index.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';

import koaOIDCErrorHandler from './koa-oidc-error-handler.js';
import koaOidcErrorHandler, { errorOut } from './koa-oidc-error-handler.js';

const { jest } = import.meta;

describe('koaOIDCErrorHandler middleware', () => {
describe('koaOidcErrorHandler middleware', () => {
const next = jest.fn();
const ctx = createContextWithRouteParameters();

const expectErrorResponse = async (
ctx: Context,
error: errors.OIDCProviderError,
extraMatch?: Record<string, unknown>
) => {
next.mockImplementationOnce(() => {
throw error;
});

const out = errorOut(error);
const code = `oidc.${out.error}`;
await koaOidcErrorHandler()(ctx, next);
expect(ctx.status).toBe(error.statusCode);
expect(ctx.body).toMatchObject({
...out,
code,
message: i18next.t('errors:' + code),
...extraMatch,
});
};

it('should throw no errors if no errors are caught', async () => {
await expect(koaOIDCErrorHandler()(ctx, next)).resolves.not.toThrow();
const ctx = createContextWithRouteParameters();
await expect(koaOidcErrorHandler()(ctx, next)).resolves.not.toThrow();
});

it('should throw original error if error type is not OIDCProviderError', async () => {
const ctx = createContextWithRouteParameters();
const error = new Error('err');

next.mockImplementationOnce(() => {
throw error;
});

await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(error);
await expect(koaOidcErrorHandler()(ctx, next)).rejects.toMatchError(error);
});

it('Invalid Scope', async () => {
it('should recognize invalid scope error', async () => {
const ctx = createContextWithRouteParameters();
const error_description = 'Mock scope is invalid';
const mockScope = 'read:foo';
const error = new errors.InvalidScope(error_description, mockScope);
next.mockImplementationOnce(() => {
throw error;
});

await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'oidc.invalid_scope',
status: error.status,
expose: true,
scope: mockScope,
},
{ error_description }
)
);
await expectErrorResponse(ctx, new errors.InvalidScope(error_description, mockScope));
});

it('Session Not Found', async () => {
const error_description = 'session not found';
const error = new errors.SessionNotFound('session not found');
it('should transform session not found error code', async () => {
const ctx = createContextWithRouteParameters();

next.mockImplementationOnce(() => {
throw error;
await expectErrorResponse(ctx, new errors.SessionNotFound('session not found'), {
code: 'session.not_found',
message: i18next.t('errors:session.not_found'),
});

await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'session.not_found',
status: error.status,
expose: true,
},
{
error_description,
}
)
);
});

it('Insufficient Scope', async () => {
it('should add scope in response when needed', async () => {
const ctx = createContextWithRouteParameters();
const error_description = 'Insufficient scope for access_token';
const scope = 'read:foo';

const error = new errors.InsufficientScope(error_description, scope);

next.mockImplementationOnce(() => {
throw error;
await expectErrorResponse(ctx, new errors.InsufficientScope(error_description, scope), {
scope,
});

await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'oidc.insufficient_scope',
status: error.status,
expose: true,
scopes: scope,
},
{
error_description,
}
)
);
});

it('Unhandled OIDCProvider Error', async () => {
const error = new errors.AuthorizationPending();
it('should add error uri when available', async () => {
const ctx = createContextWithRouteParameters();
const error = new errors.InvalidGrant('invalid grant');

next.mockImplementationOnce(() => {
throw error;
await expectErrorResponse(ctx, error, {
error_uri: 'https://openid.sh/debug/invalid_grant',
});
});

it('should handle unrecognized oidc error', async () => {
const ctx = createContextWithRouteParameters();
const unrecognizedError = { error: 'some_error', error_description: 'some error description' };

ctx.status = 500;
ctx.body = unrecognizedError;

await koaOidcErrorHandler()(ctx, next);

await expect(koaOIDCErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'oidc.provider_error',
status: error.status,
expose: true,
message: error.message,
},
{
error_description: error.error_description,
error_detail: error.error_detail,
}
)
);
expect(ctx.status).toBe(500);
expect(ctx.body).toMatchObject({ ...unrecognizedError, code: 'oidc.some_error' });
});
});
Loading

0 comments on commit b8e592d

Please sign in to comment.