mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
fix: Output required OAuth error fields
This commit is contained in:
parent
7eb938044d
commit
63fd062f16
@ -20,7 +20,9 @@ import { BasicRepresentation } from '../../http/representation/BasicRepresentati
|
|||||||
import { getLoggerFor } from '../../logging/LogUtil';
|
import { getLoggerFor } from '../../logging/LogUtil';
|
||||||
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
|
import type { HttpError } from '../../util/errors/HttpError';
|
||||||
import { InternalServerError } from '../../util/errors/InternalServerError';
|
import { InternalServerError } from '../../util/errors/InternalServerError';
|
||||||
|
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
||||||
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
import { RedirectHttpError } from '../../util/errors/RedirectHttpError';
|
||||||
import { guardStream } from '../../util/GuardedStream';
|
import { guardStream } from '../../util/GuardedStream';
|
||||||
import { joinUrl } from '../../util/PathUtil';
|
import { joinUrl } from '../../util/PathUtil';
|
||||||
@ -379,7 +381,8 @@ export class IdentityProviderFactory implements ProviderFactory {
|
|||||||
// Doesn't really matter which type it is since all relevant fields are optional
|
// Doesn't really matter which type it is since all relevant fields are optional
|
||||||
const oidcError = error as errors.OIDCProviderError;
|
const oidcError = error as errors.OIDCProviderError;
|
||||||
|
|
||||||
let detailedError = error.message;
|
// Create a more detailed error message for logging and to show is `showStackTrace` is enabled.
|
||||||
|
let detailedError = oidcError.message;
|
||||||
if (oidcError.error_description) {
|
if (oidcError.error_description) {
|
||||||
detailedError += ` - ${oidcError.error_description}`;
|
detailedError += ` - ${oidcError.error_description}`;
|
||||||
}
|
}
|
||||||
@ -389,13 +392,21 @@ export class IdentityProviderFactory implements ProviderFactory {
|
|||||||
|
|
||||||
this.logger.warn(`OIDC request failed: ${detailedError}`);
|
this.logger.warn(`OIDC request failed: ${detailedError}`);
|
||||||
|
|
||||||
// OIDC library hides extra details in these fields
|
// Convert to our own error object.
|
||||||
|
// This ensures serializing the error object will generate the correct output later on.
|
||||||
|
// We specifically copy the fields instead of passing the object to contain the `oidc-provider` dependency
|
||||||
|
// to the current file.
|
||||||
|
let resultingError: HttpError = new OAuthHttpError(out, oidcError.name, oidcError.statusCode, oidcError.message);
|
||||||
|
// Keep the original stack to make debugging easier
|
||||||
|
resultingError.stack = oidcError.stack;
|
||||||
|
|
||||||
if (this.showStackTrace) {
|
if (this.showStackTrace) {
|
||||||
error.message = detailedError;
|
// Expose more information if `showStackTrace` is enabled
|
||||||
|
resultingError.message = detailedError;
|
||||||
|
|
||||||
// Also change the error message in the stack trace
|
// Also change the error message in the stack trace
|
||||||
if (error.stack) {
|
if (resultingError.stack) {
|
||||||
error.stack = error.stack.replace(/.*/u, `${error.name}: ${error.message}`);
|
resultingError.stack = resultingError.stack.replace(/.*/u, `${oidcError.name}: ${oidcError.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,11 +422,11 @@ export class IdentityProviderFactory implements ProviderFactory {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
unknownClientError.stack = error.stack;
|
unknownClientError.stack = oidcError.stack;
|
||||||
error = unknownClientError;
|
resultingError = unknownClientError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await this.errorHandler.handleSafe({ error, request: guardStream(ctx.req) });
|
const result = await this.errorHandler.handleSafe({ error: resultingError, request: guardStream(ctx.req) });
|
||||||
await this.responseWriter.handleSafe({ response: ctx.res, result });
|
await this.responseWriter.handleSafe({ response: ctx.res, result });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -406,6 +406,7 @@ export * from './util/errors/MethodNotAllowedHttpError';
|
|||||||
export * from './util/errors/MovedPermanentlyHttpError';
|
export * from './util/errors/MovedPermanentlyHttpError';
|
||||||
export * from './util/errors/NotFoundHttpError';
|
export * from './util/errors/NotFoundHttpError';
|
||||||
export * from './util/errors/NotImplementedHttpError';
|
export * from './util/errors/NotImplementedHttpError';
|
||||||
|
export * from './util/errors/OAuthHttpError';
|
||||||
export * from './util/errors/PreconditionFailedHttpError';
|
export * from './util/errors/PreconditionFailedHttpError';
|
||||||
export * from './util/errors/RedirectHttpError';
|
export * from './util/errors/RedirectHttpError';
|
||||||
export * from './util/errors/SystemError';
|
export * from './util/errors/SystemError';
|
||||||
|
@ -2,6 +2,7 @@ import { BasicRepresentation } from '../../http/representation/BasicRepresentati
|
|||||||
import type { Representation } from '../../http/representation/Representation';
|
import type { Representation } from '../../http/representation/Representation';
|
||||||
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
|
import { APPLICATION_JSON, INTERNAL_ERROR } from '../../util/ContentTypes';
|
||||||
import { HttpError } from '../../util/errors/HttpError';
|
import { HttpError } from '../../util/errors/HttpError';
|
||||||
|
import { OAuthHttpError } from '../../util/errors/OAuthHttpError';
|
||||||
import { getSingleItem } from '../../util/StreamUtil';
|
import { getSingleItem } from '../../util/StreamUtil';
|
||||||
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
import { BaseTypedRepresentationConverter } from './BaseTypedRepresentationConverter';
|
||||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||||
@ -22,6 +23,11 @@ export class ErrorToJsonConverter extends BaseTypedRepresentationConverter {
|
|||||||
message: error.message,
|
message: error.message,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OAuth errors responses require additional fields
|
||||||
|
if (OAuthHttpError.isInstance(error)) {
|
||||||
|
Object.assign(result, error.mandatoryFields);
|
||||||
|
}
|
||||||
|
|
||||||
if (HttpError.isInstance(error)) {
|
if (HttpError.isInstance(error)) {
|
||||||
result.statusCode = error.statusCode;
|
result.statusCode = error.statusCode;
|
||||||
result.errorCode = error.errorCode;
|
result.errorCode = error.errorCode;
|
||||||
|
36
src/util/errors/OAuthHttpError.ts
Normal file
36
src/util/errors/OAuthHttpError.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import type { HttpErrorOptions } from './HttpError';
|
||||||
|
import { HttpError } from './HttpError';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These are the fields that can occur in an OAuth error response as described in RFC 6749, §4.1.2.1.
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
|
||||||
|
*
|
||||||
|
* This interface is identical to the ErrorOut interface of the `oidc-provider` library,
|
||||||
|
* but having our own version reduces the part of the codebase that is dependent on that library.
|
||||||
|
*/
|
||||||
|
export interface OAuthErrorFields {
|
||||||
|
error: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
error_description?: string | undefined;
|
||||||
|
scope?: string | undefined;
|
||||||
|
state?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents on OAuth error that is being thrown.
|
||||||
|
* OAuth error responses have additional fields that need to be present in the JSON response,
|
||||||
|
* as described in RFC 6749, §4.1.2.1.
|
||||||
|
*/
|
||||||
|
export class OAuthHttpError extends HttpError {
|
||||||
|
public readonly mandatoryFields: OAuthErrorFields;
|
||||||
|
|
||||||
|
public constructor(mandatoryFields: OAuthErrorFields, name?: string, statusCode?: number, message?: string,
|
||||||
|
options?: HttpErrorOptions) {
|
||||||
|
super(statusCode ?? 500, name ?? 'OAuthHttpError', message, options);
|
||||||
|
this.mandatoryFields = mandatoryFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isInstance(error: unknown): error is OAuthHttpError {
|
||||||
|
return HttpError.isInstance(error) && Boolean((error as OAuthHttpError).mandatoryFields);
|
||||||
|
}
|
||||||
|
}
|
@ -628,6 +628,7 @@ describe('A Solid server with IDP', (): void => {
|
|||||||
expect(json.message).toBe(`invalid_request - unrecognized route or not allowed method (GET on /.oidc/foo)`);
|
expect(json.message).toBe(`invalid_request - unrecognized route or not allowed method (GET on /.oidc/foo)`);
|
||||||
expect(json.statusCode).toBe(404);
|
expect(json.statusCode).toBe(404);
|
||||||
expect(json.stack).toBeDefined();
|
expect(json.stack).toBeDefined();
|
||||||
|
expect(json.error).toBe('invalid_request');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@ import type { Interaction, InteractionHandler } from '../../../../src/identity/i
|
|||||||
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
|
import type { AdapterFactory } from '../../../../src/identity/storage/AdapterFactory';
|
||||||
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
|
import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage';
|
||||||
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
import { FoundHttpError } from '../../../../src/util/errors/FoundHttpError';
|
||||||
|
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
jest.mock('oidc-provider', (): any => ({
|
jest.mock('oidc-provider', (): any => ({
|
||||||
@ -229,14 +230,16 @@ describe('An IdentityProviderFactory', (): void => {
|
|||||||
error.error_description = 'more info';
|
error.error_description = 'more info';
|
||||||
error.error_detail = 'more details';
|
error.error_detail = 'more details';
|
||||||
|
|
||||||
|
const oAuthError = new OAuthHttpError(error, error.name, 500, 'bad data - more info - more details');
|
||||||
|
|
||||||
await expect((config.renderError as any)(ctx, {}, error)).resolves.toBeUndefined();
|
await expect((config.renderError as any)(ctx, {}, error)).resolves.toBeUndefined();
|
||||||
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(errorHandler.handleSafe)
|
expect(errorHandler.handleSafe)
|
||||||
.toHaveBeenLastCalledWith({ error, request: ctx.req });
|
.toHaveBeenLastCalledWith({ error: oAuthError, request: ctx.req });
|
||||||
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
|
expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }});
|
||||||
expect(error.message).toBe('bad data - more info - more details');
|
expect(oAuthError.message).toBe('bad data - more info - more details');
|
||||||
expect(error.stack).toContain('Error: bad data - more info - more details');
|
expect(oAuthError.stack).toContain('Error: bad data - more info - more details');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws a specific error for unknown clients.', async(): Promise<void> => {
|
it('throws a specific error for unknown clients.', async(): Promise<void> => {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
|
||||||
import { ErrorToJsonConverter } from '../../../../src/storage/conversion/ErrorToJsonConverter';
|
import { ErrorToJsonConverter } from '../../../../src/storage/conversion/ErrorToJsonConverter';
|
||||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||||
|
import type { OAuthErrorFields } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
|
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
import { readJsonStream } from '../../../../src/util/StreamUtil';
|
import { readJsonStream } from '../../../../src/util/StreamUtil';
|
||||||
|
|
||||||
describe('An ErrorToJsonConverter', (): void => {
|
describe('An ErrorToJsonConverter', (): void => {
|
||||||
@ -47,6 +49,35 @@ describe('An ErrorToJsonConverter', (): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds OAuth fields if present.', async(): Promise<void> => {
|
||||||
|
const out: OAuthErrorFields = {
|
||||||
|
error: 'error',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
error_description: 'error_description',
|
||||||
|
scope: 'scope',
|
||||||
|
state: 'state',
|
||||||
|
};
|
||||||
|
const error = new OAuthHttpError(out, 'InvalidRequest', 400, 'error text');
|
||||||
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
|
const prom = converter.handle({ identifier, representation, preferences });
|
||||||
|
await expect(prom).resolves.toBeDefined();
|
||||||
|
const result = await prom;
|
||||||
|
expect(result.binary).toBe(true);
|
||||||
|
expect(result.metadata.contentType).toBe('application/json');
|
||||||
|
await expect(readJsonStream(result.data)).resolves.toEqual({
|
||||||
|
name: 'InvalidRequest',
|
||||||
|
message: 'error text',
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'H400',
|
||||||
|
stack: error.stack,
|
||||||
|
error: 'error',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
error_description: 'error_description',
|
||||||
|
scope: 'scope',
|
||||||
|
state: 'state',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not copy the details if they are not serializable.', async(): Promise<void> => {
|
it('does not copy the details if they are not serializable.', async(): Promise<void> => {
|
||||||
const error = new BadRequestHttpError('error text', { details: { object: BigInt(1) }});
|
const error = new BadRequestHttpError('error text', { details: { object: BigInt(1) }});
|
||||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||||
|
24
test/unit/util/errors/OAuthHttpError.test.ts
Normal file
24
test/unit/util/errors/OAuthHttpError.test.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { OAuthHttpError } from '../../../../src/util/errors/OAuthHttpError';
|
||||||
|
|
||||||
|
describe('An OAuthHttpError', (): void => {
|
||||||
|
it('contains relevant information.', async(): Promise<void> => {
|
||||||
|
const error = new OAuthHttpError({ error: 'error!' }, 'InvalidRequest', 400, 'message!');
|
||||||
|
expect(error.mandatoryFields.error).toBe('error!');
|
||||||
|
expect(error.name).toBe('InvalidRequest');
|
||||||
|
expect(error.statusCode).toBe(400);
|
||||||
|
expect(error.message).toBe('message!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has optional fields.', async(): Promise<void> => {
|
||||||
|
const error = new OAuthHttpError({ error: 'error!' });
|
||||||
|
expect(error.mandatoryFields.error).toBe('error!');
|
||||||
|
expect(error.name).toBe('OAuthHttpError');
|
||||||
|
expect(error.statusCode).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can identify OAuth errors.', async(): Promise<void> => {
|
||||||
|
const error = new OAuthHttpError({ error: 'error!' });
|
||||||
|
expect(OAuthHttpError.isInstance('apple')).toBe(false);
|
||||||
|
expect(OAuthHttpError.isInstance(error)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user