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:
@@ -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']);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user