taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5c394478b2806aeaf109c0b0903e9fd3ba7c68fa
parent df872794aaee66742b5c5a526de16078a316b7f6
Author: Florian Dold <dold@taler.net>
Date:   Wed, 23 Sep 2026 14:49:38 +0200

util: handle challenger's 410 for a permanently failed validation

Challenger v10 reports a validation the user can no longer pass as 410
with the error redirect to the client, which used to be an unknown
failure.

Issue: https://bugs.taler.net/n/11740

Diffstat:
Mpackages/taler-util/src/http-client/challenger.test.ts | 26++++++++++++++++++++++++++
Mpackages/taler-util/src/http-client/challenger.ts | 19+++++++++++++++++++
Mpackages/taler-util/src/types-taler-challenger.ts | 23+++++++++++++++++++++++
3 files changed, 68 insertions(+), 0 deletions(-)

diff --git a/packages/taler-util/src/http-client/challenger.test.ts b/packages/taler-util/src/http-client/challenger.test.ts @@ -152,3 +152,29 @@ test("solve decodes recoverable 409 and 429 responses", async () => { assert.deepStrictEqual(result.body, { ...pending, ec: undefined }); } }); + +test("a permanently failed validation is a known 410 failure everywhere", async () => { + const failed = { + code: 9772, + hint: "The validation failed permanently.", + redirect_url: + "https://client.example.com/cb?error=access_denied&error_description=x&state=s", + }; + const calls: Array<(c: ChallengerHttpClient) => Promise<any>> = [ + (c) => c.login("nonce", "1", "https://client.example.com/cb", "s"), + (c) => c.challenge("nonce", { CONTACT_EMAIL: "alice@example.com" }), + (c) => c.solve("nonce", { pin: "1234" }), + ]; + for (const call of calls) { + const { lib } = fixedLib(HttpStatusCode.Gone, {}, failed); + const client = new ChallengerHttpClient( + "https://challenger.example.com/", + lib, + ); + const res = await call(client); + assert.ok(isOperationFail(res), "410 must be a known failure"); + assert.strictEqual(res.case, HttpStatusCode.Gone); + assert.strictEqual((res as any).body.code, 9772); + assert.strictEqual((res as any).body.redirect_url, failed.redirect_url); + } +}); diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts @@ -39,6 +39,7 @@ import { codecForChallengeSetupResponse, codecForChallengeSolveResponse, codecForChallengeStatus, + codecForChallengeValidationFailedResponse, codecForChallengerAuthResponse, codecForChallengerInfoResponse, codecForChallengerTermsOfServiceResponse, @@ -156,6 +157,12 @@ export class ChallengerHttpClient { return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownAlternativeHttpFailure( + resp, + HttpStatusCode.Gone, + codecForChallengeValidationFailedResponse(), + ); case HttpStatusCode.TooManyRequests: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.InternalServerError: @@ -193,6 +200,12 @@ export class ChallengerHttpClient { return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownAlternativeHttpFailure( + resp, + HttpStatusCode.Gone, + codecForChallengeValidationFailedResponse(), + ); case HttpStatusCode.TooManyRequests: return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.PayloadTooLarge: @@ -262,6 +275,12 @@ export class ChallengerHttpClient { return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotAcceptable: return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownAlternativeHttpFailure( + resp, + HttpStatusCode.Gone, + codecForChallengeValidationFailedResponse(), + ); case HttpStatusCode.TooManyRequests: return opKnownAlternativeHttpFailure( resp, diff --git a/packages/taler-util/src/types-taler-challenger.ts b/packages/taler-util/src/types-taler-challenger.ts @@ -186,6 +186,21 @@ export interface InvalidPinResponse { no_challenge: boolean; } +// Returned with 410 Gone once the validation failed permanently: the user +// exhausted every address change, TAN transmission and TAN attempt. +export interface ValidationFailedResponse { + // TALER_EC_CHALLENGER_VALIDATION_FAILED + code: number; + + // human-readable description of the error + hint?: string; + + // RFC 6749 "access_denied" error response to the client; the user agent + // must send the user there so that the client learns about the failure. + // Missing if the client never told us where to redirect to. + redirect_url?: string; +} + export interface ChallengerAuthResponse { // Token used to authenticate access in /info. access_token: string; @@ -299,6 +314,14 @@ export const codecForChallengeSolveResponse = .alternative("pending", codecForChallengeInvalidPinResponse()) .build("ChallengerApi.ChallengeSolveResponse"); +export const codecForChallengeValidationFailedResponse = + (): Codec<ValidationFailedResponse> => + buildCodecForObject<ValidationFailedResponse>() + .property("code", codecForNumber()) + .property("hint", codecOptional(codecForString())) + .property("redirect_url", codecOptional(codecForString())) + .build("ChallengerApi.ValidationFailedResponse"); + export const codecForChallengerAuthResponse = (): Codec<ChallengerAuthResponse> => buildCodecForObject<ChallengerAuthResponse>()