From b1002e659918eaa3f0b95abc6760295b358594dd Mon Sep 17 00:00:00 2001 From: Josep Piguillem Date: Sat, 3 Oct 2026 15:52:16 +0200 Subject: [PATCH] fix(auth): keep Pireon's OAuth error hint in token errors and logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passport answers token failures with a generic `invalid_request` description and puts the actual cause in `hint` (invalid redirect URI, expired or revoked code, code issued to another client…). The proxy dropped it, so sign-in showed only "The request is missing a required parameter…". It is now appended to the error message and logged with the grant type and client id. Co-Authored-By: Claude Opus 5.5 --- .../pireon/pireon-identity.provider.spec.ts | 18 ++++++++++++++++++ .../src/lib/pireon/pireon-identity.provider.ts | 13 ++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.spec.ts b/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.spec.ts index 546cd29..6c39915 100644 --- a/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.spec.ts +++ b/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.spec.ts @@ -106,6 +106,24 @@ describe('PireonIdentityProvider tokens', () => { ]); }); + it("keeps Pireon's hint, which says what was wrong", async () => { + const { provider } = setup(); + jest.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response( + JSON.stringify({ + error: 'invalid_request', + error_description: + 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.', + hint: 'Invalid redirect URI', + }), + { status: 400 }, + ), + ); + await expect(provider.issueToken(code)).rejects.toThrow( + /otherwise malformed\. \(Invalid redirect URI\)/, + ); + }); + it('rejects the refresh when no client accepts it', async () => { const { provider, respond } = setup(); respond(['password-client', 'code-client']); diff --git a/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.ts b/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.ts index 8219acd..13df47f 100644 --- a/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.ts +++ b/libs/api/identity/infrastructure/src/lib/pireon/pireon-identity.provider.ts @@ -184,12 +184,23 @@ export class PireonIdentityProvider extends IdentityProvider { throw new ServiceUnavailableException('Identity server unavailable'); } if (response.status === 400 || response.status === 401) { + // OAuth2 error (RFC 6749 §5.2). Passport puts the useful part in `hint` + // (which parameter was wrong), so keep it in the message and the log. const error = (await response.json().catch(() => ({}))) as { + error?: string; error_description?: string; + hint?: string; message?: string; }; + const description = + error.error_description ?? error.message ?? 'Invalid credentials'; + this.logger.warn( + `Pireon rejected ${grant.type} for client ${client.id}: ` + + `${error.error ?? response.status} — ${description}` + + (error.hint ? ` (hint: ${error.hint})` : ''), + ); throw new InvalidCredentialsError( - error.error_description ?? error.message ?? 'Invalid credentials', + error.hint ? `${description} (${error.hint})` : description, ); } this.assertOk(response, 'POST /oauth/token');