diff --git a/src/identity/configuration/IdentityProviderFactory.ts b/src/identity/configuration/IdentityProviderFactory.ts index 10bf782ad..ea442ab21 100644 --- a/src/identity/configuration/IdentityProviderFactory.ts +++ b/src/identity/configuration/IdentityProviderFactory.ts @@ -19,6 +19,7 @@ import type { ResponseWriter } from '../../http/output/ResponseWriter'; import { BasicRepresentation } from '../../http/representation/BasicRepresentation'; import { getLoggerFor } from '../../logging/LogUtil'; import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage'; +import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError'; import { InternalServerError } from '../../util/errors/InternalServerError'; import { RedirectHttpError } from '../../util/errors/RedirectHttpError'; import { guardStream } from '../../util/GuardedStream'; @@ -398,6 +399,22 @@ export class IdentityProviderFactory implements ProviderFactory { } } + // A client not being found is quite often the result of cookies being stored by the authn client, + // so we want to provide a more detailed error message explaining what to do. + if (oidcError.error_description === 'client is invalid' && oidcError.error_detail === 'client not found') { + const unknownClientError = new BadRequestHttpError( + 'Unknown client, you might need to clear the local storage on the client.', { + errorCode: 'E0003', + details: { + client_id: ctx.request.query.client_id, + redirect_uri: ctx.request.query.redirect_uri, + }, + }, + ); + unknownClientError.stack = error.stack; + error = unknownClientError; + } + const result = await this.errorHandler.handleSafe({ error, request: guardStream(ctx.req) }); await this.responseWriter.handleSafe({ response: ctx.res, result }); }; diff --git a/templates/error/descriptions/E0003.md.hbs b/templates/error/descriptions/E0003.md.hbs new file mode 100644 index 000000000..1bd8513e3 --- /dev/null +++ b/templates/error/descriptions/E0003.md.hbs @@ -0,0 +1,26 @@ +# Authenticating with unknown client +You are trying to log in to an application, +but we can't proceed +because the app is using invalid settings. + +To force the app to send us the right details, +delete the local storage in your browser for the site that sent you here. +Based on the data the app sent us, +this is probably `{{ redirect_uri }}`. + +## Detailed error information +We received a request from a client with ID `{{ client_id }}`, +but this client is not registered with the server. + +Probably, +this client was registered with the server in the past, +but it is no longer recognized +because some internal server data was removed. +Your data is still safe; +we just don't recognize the app's previous authentication anymore. + +Because your browser still has the old authentication settings stored, +it tries to use them instead of setting up new ones. +By clearing those settings, +the application should automatically create a new client, +allowing you to log in again. diff --git a/test/unit/identity/configuration/IdentityProviderFactory.test.ts b/test/unit/identity/configuration/IdentityProviderFactory.test.ts index 05701709d..7adf696e7 100644 --- a/test/unit/identity/configuration/IdentityProviderFactory.test.ts +++ b/test/unit/identity/configuration/IdentityProviderFactory.test.ts @@ -59,6 +59,10 @@ describe('An IdentityProviderFactory', (): void => { res: {}, request: { href: 'http://example.com/idp/', + query: { + client_id: 'CLIENT_ID', + redirect_uri: 'REDIRECT_URI', + }, }, accepts: jest.fn().mockReturnValue('type'), } as any; @@ -235,6 +239,32 @@ describe('An IdentityProviderFactory', (): void => { expect(error.stack).toContain('Error: bad data - more info - more details'); }); + it('throws a specific error for unknown clients.', async(): Promise => { + const provider = await factory.getProvider() as any; + const { config } = provider as { config: Configuration }; + + const error = new Error('invalid_client') as errors.OIDCProviderError; + error.error_description = 'client is invalid'; + error.error_detail = 'client not found'; + + await expect((config.renderError as any)(ctx, {}, error)).resolves.toBeUndefined(); + expect(errorHandler.handleSafe).toHaveBeenCalledTimes(1); + expect(errorHandler.handleSafe) + .toHaveBeenLastCalledWith({ error: expect.objectContaining({ + statusCode: 400, + name: 'BadRequestHttpError', + message: 'Unknown client, you might need to clear the local storage on the client.', + errorCode: 'E0003', + details: { + client_id: 'CLIENT_ID', + redirect_uri: 'REDIRECT_URI', + }, + }), + request: ctx.req }); + expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); + expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ response: ctx.res, result: { statusCode: 500 }}); + }); + it('adds middleware to make the OIDC provider think the request wants HTML.', async(): Promise => { const provider = await factory.getProvider() as any; const use = provider.use as jest.Mock, Parameters>;