From 1b9db448dc3dd4893c484c63cf459d7d9250e693 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 5 Jan 2024 12:29:53 -0300 Subject: towards new 2fa --- .../src/pages/account/ShowAccountDetails.tsx | 4 +- .../src/pages/business/CreateCashout.tsx | 10 +- packages/taler-util/src/http-client/bank-core.ts | 66 ++++++++-- packages/taler-util/src/http-client/types.ts | 140 +++++++++++++++------ 4 files changed, 163 insertions(+), 57 deletions(-) diff --git a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx index 1f2d67c49..4b66c0d8d 100644 --- a/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx +++ b/packages/demobank-ui/src/pages/account/ShowAccountDetails.tsx @@ -89,9 +89,9 @@ export function ShowAccountDetails({ description: resp.detail.hint as TranslatedString, debug: resp.detail, }) - case "user-cant-change-contact": return notify({ + case "missing-contact-data": return notify({ type: "error", - title: i18n.str`You can't change the contact info, please contact the your account administrator.`, + title: i18n.str`You need contact data to enable 2FA.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) diff --git a/packages/demobank-ui/src/pages/business/CreateCashout.tsx b/packages/demobank-ui/src/pages/business/CreateCashout.tsx index 8987accd1..92c80ea38 100644 --- a/packages/demobank-ui/src/pages/business/CreateCashout.tsx +++ b/packages/demobank-ui/src/pages/business/CreateCashout.tsx @@ -212,12 +212,6 @@ export function CreateCashout({ description: resp.detail.hint as TranslatedString, debug: resp.detail, }); - case "no-contact-info": return notify({ - type: "error", - title: i18n.str`Missing contact info before to create the cashout`, - description: resp.detail.hint as TranslatedString, - debug: resp.detail, - }); case "no-enough-balance": return notify({ type: "error", title: i18n.str`The account does not have sufficient funds`, @@ -230,9 +224,9 @@ export function CreateCashout({ description: resp.detail.hint as TranslatedString, debug: resp.detail, }); - case "tan-failed": return notify({ + case "no-cashout-uri": return notify({ type: "error", - title: i18n.str`Sending the confirmation code failed.`, + title: i18n.str`Missing cashout URI in the profile`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }); diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts index 51d6d7c96..b7e0292bd 100644 --- a/packages/taler-util/src/http-client/bank-core.ts +++ b/packages/taler-util/src/http-client/bank-core.ts @@ -18,7 +18,9 @@ import { HttpStatusCode, LibtoolVersion, TalerErrorCode, - codecForTalerErrorDetail + codecForChallenge, + codecForTalerErrorDetail, + codecForTanTransmission } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -123,6 +125,7 @@ export class TalerCoreBankHttpClient { }, }); switch (resp.status) { + case HttpStatusCode.Accepted: return opSuccess(resp, codecForChallenge()) case HttpStatusCode.NoContent: return opEmptySuccess() case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); @@ -153,6 +156,7 @@ export class TalerCoreBankHttpClient { }, }); switch (resp.status) { + case HttpStatusCode.Accepted: return opSuccess(resp, codecForChallenge()) case HttpStatusCode.NoContent: return opEmptySuccess() case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); @@ -163,7 +167,7 @@ export class TalerCoreBankHttpClient { case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME: return opKnownFailure("user-cant-change-name", resp); case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT: return opKnownFailure("user-cant-change-debt", resp); case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT: return opKnownFailure("user-cant-change-cashout", resp); - case TalerErrorCode.BANK_NON_ADMIN_PATCH_CONTACT: return opKnownFailure("user-cant-change-contact", resp); + case TalerErrorCode.BANK_MISSING_TAN_INFO: return opKnownFailure("missing-contact-data", resp); default: return opUnknownFailure(resp, body) } } @@ -185,6 +189,7 @@ export class TalerCoreBankHttpClient { }, }); switch (resp.status) { + case HttpStatusCode.Accepted: return opSuccess(resp, codecForChallenge()) case HttpStatusCode.NoContent: return opEmptySuccess() case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); @@ -326,6 +331,7 @@ export class TalerCoreBankHttpClient { body, }); switch (resp.status) { + case HttpStatusCode.Accepted: return opSuccess(resp, codecForChallenge()) case HttpStatusCode.Ok: return opSuccess(resp, codecForCreateTransactionResponse()) case HttpStatusCode.BadRequest: return opKnownFailure("invalid-input", resp); case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); @@ -405,6 +411,7 @@ export class TalerCoreBankHttpClient { }, }); switch (resp.status) { + case HttpStatusCode.Accepted: return opSuccess(resp, codecForChallenge()) case HttpStatusCode.NoContent: return opEmptySuccess() //FIXME: missing in docs case HttpStatusCode.BadRequest: return opKnownFailure("invalid-id", resp) @@ -466,6 +473,7 @@ export class TalerCoreBankHttpClient { body, }); switch (resp.status) { + case HttpStatusCode.Accepted: return opSuccess(resp, codecForChallenge()) case HttpStatusCode.Ok: return opSuccess(resp, codecForCashoutPending()) case HttpStatusCode.NotFound: return opKnownFailure("account-not-found", resp) case HttpStatusCode.Conflict: { @@ -474,20 +482,19 @@ export class TalerCoreBankHttpClient { switch (details.code) { case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED: return opKnownFailure("request-already-used", resp); case TalerErrorCode.BANK_BAD_CONVERSION: return opKnownFailure("incorrect-exchange-rate", resp); - case TalerErrorCode.BANK_MISSING_TAN_INFO: return opKnownFailure("no-contact-info", resp); case TalerErrorCode.BANK_UNALLOWED_DEBIT: return opKnownFailure("no-enough-balance", resp); + case TalerErrorCode.BANK_CONFIRM_INCOMPLETE: return opKnownFailure("no-cashout-uri", resp); default: return opUnknownFailure(resp, body) } } case HttpStatusCode.NotImplemented: return opKnownFailure("cashout-not-supported", resp); - case HttpStatusCode.BadGateway: return opKnownFailure("tan-failed", resp); default: return opUnknownFailure(resp, await resp.text()) } } /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-abort - * + * @deprecated since 4 */ async abortCashoutById(auth: UserAndToken, cid: number) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}/abort`, this.baseUrl); @@ -508,7 +515,7 @@ export class TalerCoreBankHttpClient { /** * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts-$CASHOUT_ID-confirm - * + * @deprecated since 4 */ async confirmCashoutById(auth: UserAndToken, cid: number, body: TalerCorebankApi.CashoutConfirmRequest) { const url = new URL(`accounts/${auth.username}/cashouts/${cid}/confirm`, this.baseUrl); @@ -522,7 +529,6 @@ export class TalerCoreBankHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: return opEmptySuccess() case HttpStatusCode.NotFound: return opKnownFailure("not-found", resp); - // case HttpStatusCode.Forbidden: return opKnownFailure("wrong-tan-or-credential", resp); case HttpStatusCode.Conflict: { const body = await resp.json() const details = codecForTalerErrorDetail().decode(body) @@ -637,6 +643,52 @@ export class TalerCoreBankHttpClient { } } + // + // 2FA + // + async sendChallenge(auth: UserAndToken, cid: string) { + const url = new URL(`accounts/${auth.username}/challenge/${cid}`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: return opSuccess(resp, codecForTanTransmission()) + case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.NotFound: return opKnownFailure("invalid-challenge", resp); + case HttpStatusCode.BadGateway: { + const body = await resp.json() + const details = codecForTalerErrorDetail().decode(body) + switch (details.code) { + case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return opKnownFailure("tan-failed", resp); + default: return opUnknownFailure(resp, body) + } + } + default: return opUnknownFailure(resp, await resp.text()) + } + } + + async confirmChallenge(auth: UserAndToken, cid: string) { + const url = new URL(`accounts/${auth.username}/challenge/${cid}/confirm`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(auth.token) + }, + }); + switch (resp.status) { + case HttpStatusCode.Ok: return opEmptySuccess() + case HttpStatusCode.Unauthorized: return opKnownFailure("unauthorized", resp); + case HttpStatusCode.NotFound: return opKnownFailure("invalid-challenge", resp); + case HttpStatusCode.Conflict: return opKnownFailure("wrong-code", resp); + case HttpStatusCode.TooManyRequests: return opKnownFailure("too-many-errors", resp); + default: return opUnknownFailure(resp, await resp.text()) + } + } + + // // Others API // diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index b0d4deca1..f43a0a3a1 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -1,3 +1,4 @@ +import { deprecate } from "util"; import { codecForAmountString } from "../amounts.js"; import { Codec, @@ -280,8 +281,8 @@ export const codecForCoreBankConfig = (): Codec => .property("currency_specification", codecForCurrencySpecificiation()) .property("currency", codecForString()) .property("supported_tan_channels", codecForList(codecForEither( - codecForConstString(TanChannel.SMS), - codecForConstString(TanChannel.EMAIL), + codecForConstString(TalerCorebankApi.TanChannel.SMS), + codecForConstString(TalerCorebankApi.TanChannel.EMAIL), ))) .build("TalerCorebankApi.Config"); @@ -434,8 +435,8 @@ export const codecForBankAccountCreateWithdrawalResponse = .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse"); export const codecForCashoutPending = - (): Codec => - buildCodecForObject() + (): Codec => + buildCodecForObject() .property("cashout_id", codecForNumber()) .build("TalerCorebankApi.CashoutPending"); @@ -463,11 +464,12 @@ export const codecForCashoutInfo = (): Codec => .property("cashout_id", codecForNumber()) .property( "status", - codecForEither( - codecForConstString("pending"), - codecForConstString("aborted"), - codecForConstString("confirmed"), - ), + codecOptional( + codecForEither( + codecForConstString("pending"), + codecForConstString("aborted"), + codecForConstString("confirmed"), + )), ) .build("TalerCorebankApi.CashoutInfo"); @@ -484,11 +486,12 @@ export const codecForGlobalCashoutInfo = .property("username", codecForString()) .property( "status", - codecForEither( - codecForConstString("pending"), - codecForConstString("aborted"), - codecForConstString("confirmed"), - ), + codecOptional( + codecForEither( + codecForConstString("pending"), + codecForConstString("aborted"), + codecForConstString("confirmed"), + )), ) .build("TalerCorebankApi.GlobalCashoutInfo"); @@ -497,26 +500,25 @@ export const codecForCashoutStatusResponse = buildCodecForObject() .property("amount_credit", codecForAmountString()) .property("amount_debit", codecForAmountString()) - .property("confirmation_time", codecOptional(codecForTimestamp)) .property("creation_time", codecForTimestamp) - // .property("credit_payto_uri", codecForPaytoString()) + .property( + "tan_channel", + codecOptional(codecForEither( + codecForConstString(TalerCorebankApi.TanChannel.SMS), + codecForConstString(TalerCorebankApi.TanChannel.EMAIL), + )), + ) + .property("subject", codecForString()) + .property("confirmation_time", codecOptional(codecForTimestamp)) .property( "status", - codecForEither( + codecOptional(codecForEither( codecForConstString("pending"), codecForConstString("aborted"), codecForConstString("confirmed"), - ), + )), ) - .property( - "tan_channel", - codecForEither( - codecForConstString(TanChannel.SMS), - codecForConstString(TanChannel.EMAIL), - ), - ) - .property("subject", codecForString()) - .property("tan_info", codecForString()) + .property("tan_info", codecOptional(codecForString())) .build("TalerCorebankApi.CashoutStatusResponse"); export const codecForConversionRatesResponse = @@ -735,6 +737,22 @@ export const codecForAmlDecisionDetail = .property("decider_pub", codecForString()) .build("TalerExchangeApi.AmlDecisionDetail"); +export const codecForChallenge = + (): Codec => + buildCodecForObject() + .property("challenge_id", codecForString()) + .build("TalerCorebankApi.Challenge"); + +export const codecForTanTransmission = + (): Codec => + buildCodecForObject() + .property("tan_channel", codecForEither( + codecForConstString(TalerCorebankApi.TanChannel.SMS), + codecForConstString(TalerCorebankApi.TanChannel.EMAIL), + )) + .property("tan_info", codecForString()) + .build("TalerCorebankApi.TanTransmission"); + interface KycDetail { provider_section: string; attributes?: Object; @@ -894,10 +912,6 @@ const codecForLibtoolVersion = codecForString; const codecForCurrencyName = codecForString; const codecForDecimalNumber = codecForString; -enum TanChannel { - SMS = "sms", - EMAIL = "email", -} export type WithdrawalOperationStatus = | "pending" | "selected" @@ -1442,10 +1456,6 @@ export namespace TalerCorebankApi { is_taler_exchange?: boolean; // Addresses where to send the TAN for transactions. - // Currently only used for cashouts. - // If missing, cashouts will fail. - // In the future, might be used for other transactions - // as well. contact_data?: ChallengeContactData; // 'payto' address of a fiat bank account. @@ -1497,6 +1507,10 @@ export namespace TalerCorebankApi { // If present, change the max debit allowed for this user // Only admin can change this property. debit_threshold?: AmountString; + + // If present, enables 2FA and set the TAN channel used for challenges + tan_channel?: TanChannel; + } export interface AccountPasswordChange { @@ -1579,6 +1593,9 @@ export namespace TalerCorebankApi { // Is this a taler exchange account? is_taler_exchange: boolean; + + // Is 2FA enabled and what channel is used for challenges? + tan_channel?: TanChannel; } export interface CashoutRequest { @@ -1613,15 +1630,20 @@ export namespace TalerCorebankApi { // this field is missing, it defaults to SMS. // The default choice prefers to change the communication // channel respect to the one used to issue this request. + /** + * @deprecated since 4, use 2fa + */ tan_channel?: TanChannel; } - export interface CashoutPending { + export interface CashoutResponse { // ID identifying the operation being created - // and now waiting for the TAN confirmation. cashout_id: number; } + /** + * @deprecated since 4, use 2fa + */ export interface CashoutConfirmRequest { // the TAN that confirms $CASHOUT_ID. tan: string; @@ -1634,7 +1656,10 @@ export namespace TalerCorebankApi { export interface CashoutInfo { cashout_id: number; - status: "pending" | "aborted" | "confirmed"; + /** + * @deprecated since 4, use new 2fa + */ + status?: "pending" | "aborted" | "confirmed"; } export interface GlobalCashouts { // Every string represents a cash-out operation ID. @@ -1643,11 +1668,13 @@ export namespace TalerCorebankApi { export interface GlobalCashoutInfo { cashout_id: number; username: string; - status: "pending" | "aborted" | "confirmed"; + /** + * @deprecated since 4, use new 2fa + */ + status?: "pending" | "aborted" | "confirmed"; } export interface CashoutStatusResponse { - status: "pending" | "aborted" | "confirmed"; // Amount debited to the internal // regional currency bank account. @@ -1666,16 +1693,30 @@ export namespace TalerCorebankApi { // Time when the cashout was created. creation_time: Timestamp; + /** + * @deprecated since 4, use new 2fa + */ + status?: "pending" | "aborted" | "confirmed"; + // Time when the cashout was confirmed via its TAN. // Missing when the operation wasn't confirmed yet. + /** + * @deprecated since 4, use new 2fa + */ confirmation_time?: Timestamp; // Channel of the last successful transmission of the TAN challenge. // Missing when all transmissions failed. + /** + * @deprecated since 4, use new 2fa + */ tan_channel?: TanChannel; // Info of the last successful transmission of the TAN challenge. // Missing when all transmissions failed. + /** + * @deprecated since 4, use new 2fa + */ tan_info?: string; } @@ -1767,6 +1808,25 @@ export namespace TalerCorebankApi { // exchange to another bank account. talerOutVolume: AmountString; } + export interface TanTransmission { + // Channel of the last successful transmission of the TAN challenge. + tan_channel: TanChannel; + + // Info of the last successful transmission of the TAN challenge. + tan_info: string; + } + + export interface Challenge { + // Unique identifier of the challenge to solve to run this protected + // operation. + challenge_id: string; + } + + export enum TanChannel { + SMS = "sms", + EMAIL = "email" + } + } export namespace TalerExchangeApi { -- cgit v1.2.3