fix(auth): keep Pireon's OAuth error hint in token errors and logs

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 15:52:16 +02:00
parent cbaad17fc7
commit b1002e6599
2 changed files with 30 additions and 1 deletions

View File

@@ -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 () => { it('rejects the refresh when no client accepts it', async () => {
const { provider, respond } = setup(); const { provider, respond } = setup();
respond(['password-client', 'code-client']); respond(['password-client', 'code-client']);

View File

@@ -184,12 +184,23 @@ export class PireonIdentityProvider extends IdentityProvider {
throw new ServiceUnavailableException('Identity server unavailable'); throw new ServiceUnavailableException('Identity server unavailable');
} }
if (response.status === 400 || response.status === 401) { 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 { const error = (await response.json().catch(() => ({}))) as {
error?: string;
error_description?: string; error_description?: string;
hint?: string;
message?: 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( throw new InvalidCredentialsError(
error.error_description ?? error.message ?? 'Invalid credentials', error.hint ? `${description} (${error.hint})` : description,
); );
} }
this.assertOk(response, 'POST /oauth/token'); this.assertOk(response, 'POST /oauth/token');