taler-typescript-core

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

commit 9a7dee809ec56bdc2aa4b33c425e3f5970692bc8
parent 72beeec6c05358b262b550e428f31bf3feed5545
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed,  9 Oct 2024 10:47:04 -0300

delete session token after logout, session duration 30 min, refresh session on refresh window

Diffstat:
Mpackages/bank-ui/src/Routing.tsx | 59+++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mpackages/bank-ui/src/hooks/session.ts | 12++++++++++--
Mpackages/bank-ui/src/pages/BankFrame.tsx | 9++++++---
Mpackages/bank-ui/src/pages/LoginForm.tsx | 46+++++++++++++++++++++++++++-------------------
Mpackages/taler-util/src/http-client/authentication.ts | 33++++++++++++++++++++++++++++++++-
Mpackages/taler-util/src/http-client/exchange.ts | 4++--
6 files changed, 128 insertions(+), 35 deletions(-)

diff --git a/packages/bank-ui/src/Routing.tsx b/packages/bank-ui/src/Routing.tsx @@ -28,6 +28,7 @@ import { Fragment, VNode, h } from "preact"; import { AbsoluteTime, AccessToken, + Duration, HttpStatusCode, TranslatedString, assertUnreachable, @@ -37,7 +38,7 @@ import { useEffect } from "preact/hooks"; import { useSessionState } from "./hooks/session.js"; import { AccountPage } from "./pages/AccountPage/index.js"; import { BankFrame } from "./pages/BankFrame.js"; -import { LoginForm } from "./pages/LoginForm.js"; +import { SESSION_DURATION, LoginForm } from "./pages/LoginForm.js"; import { PublicHistoriesPage } from "./pages/PublicHistoriesPage.js"; import { RegistrationPage } from "./pages/RegistrationPage.js"; import { ShowNotifications } from "./pages/ShowNotifications.js"; @@ -58,8 +59,49 @@ import { ShowCashoutDetails } from "./pages/regional/ShowCashoutDetails.js"; export function Routing(): VNode { const session = useSessionState(); + const refreshSession = session.state.status !== "loggedIn" || session.state.expiration.t_ms === "never" ? + undefined : + { user: session.state.username, time: session.state.expiration, auth: session.state.token }; + + const { + lib: { auth: authenticator }, + } = useBankCoreApiContext(); + + useEffect(() => { + if (!refreshSession) return; + /** + * we need to wait before refreshing the session. Waiting too much and the token will + * be expired. So 20% before expiration should be close enough. + */ + const timeLeftBeforeExpiration = Duration.getRemaining(refreshSession.time) + const refreshWindow = Duration.multiply(Duration.fromTalerProtocolDuration(SESSION_DURATION), 0.2) + if (timeLeftBeforeExpiration.d_ms === "forever" || refreshWindow.d_ms === "forever") return; + const remain = Math.max(timeLeftBeforeExpiration.d_ms - refreshWindow.d_ms, 0) + console.log({remain, left:timeLeftBeforeExpiration.d_ms, safe: refreshWindow.d_ms}); + const timeoutId = setTimeout(async () => { + const result = await authenticator(refreshSession.user).createAccessTokenBearer_BANK(refreshSession.auth, { + scope: "readwrite", + duration: SESSION_DURATION, + refreshable: true, + }) + if (result.type === "fail") { + console.log(`could not refresh session ${result.case}`) + return; + } + session.logIn({ + username: refreshSession.user, + token: createRFC8959AccessTokenEncoded(result.body.access_token), + expiration: AbsoluteTime.fromProtocolTimestamp(result.body.expiration), + }); + + }, remain) + return () => { + clearTimeout(timeoutId) + } + }, [refreshSession]) + if (session.state.status === "loggedIn") { - const { isUserAdministrator, username } = session.state; + const { isUserAdministrator, username, expiration } = session.state; return ( <BankFrame account={username} @@ -72,8 +114,8 @@ export function Routing(): VNode { return ( <BankFrame> <PublicRounting - onLoggedUser={(username, token) => { - session.logIn({ username, token: token }); + onLoggedUser={(username, token, expiration) => { + session.logIn({ username, token, expiration }); }} /> </BankFrame> @@ -94,7 +136,7 @@ const publicPages = { function PublicRounting({ onLoggedUser, }: { - onLoggedUser: (username: string, token: AccessToken) => void; + onLoggedUser: (username: string, token: AccessToken, expiration: AbsoluteTime) => void; }): VNode { const { i18n } = useTranslationContext(); const location = useCurrentLocation(publicPages); @@ -114,13 +156,14 @@ function PublicRounting({ .auth(username) .createAccessTokenBasic(username, password, { scope: "readwrite", - duration: { d_us: "forever" }, + duration: SESSION_DURATION, refreshable: true, }); if (resp.type === "ok") { onLoggedUser( username, createRFC8959AccessTokenEncoded(resp.body.access_token), + AbsoluteTime.fromProtocolTimestamp(resp.body.expiration), ); } else { switch (resp.case) { @@ -128,7 +171,7 @@ function PublicRounting({ return notify({ type: "error", title: i18n.str`Wrong credentials for "${username}"`, - description: resp.detail?.hint as TranslatedString , + description: resp.detail?.hint as TranslatedString, debug: resp.detail, when: AbsoluteTime.now(), }); @@ -136,7 +179,7 @@ function PublicRounting({ return notify({ type: "error", title: i18n.str`Account not found`, - description: resp.detail?.hint as TranslatedString , + description: resp.detail?.hint as TranslatedString, debug: resp.detail, when: AbsoluteTime.now(), }); diff --git a/packages/bank-ui/src/hooks/session.ts b/packages/bank-ui/src/hooks/session.ts @@ -15,13 +15,16 @@ */ import { + AbsoluteTime, AccessToken, Codec, buildCodecForObject, buildCodecForUnion, + codecForAbsoluteTime, codecForBoolean, codecForConstString, codecForString, + codecOptionalDefault, } from "@gnu-taler/taler-util"; import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; import { mutate } from "swr"; @@ -32,16 +35,18 @@ import { mutate } from "swr"; */ export type SessionState = LoggedIn | LoggedOut | Expired; -interface LoggedIn { +export interface LoggedIn { status: "loggedIn"; isUserAdministrator: boolean; username: string; token: AccessToken; + expiration: AbsoluteTime; } interface Expired { status: "expired"; isUserAdministrator: boolean; username: string; + expiration: AbsoluteTime; } interface LoggedOut { status: "loggedOut"; @@ -51,6 +56,7 @@ export const codecForSessionStateLoggedIn = (): Codec<LoggedIn> => buildCodecForObject<LoggedIn>() .property("status", codecForConstString("loggedIn")) .property("username", codecForString()) + .property("expiration", codecOptionalDefault(codecForAbsoluteTime, AbsoluteTime.now())) .property("token", codecForString() as Codec<AccessToken>) .property("isUserAdministrator", codecForBoolean()) .build("SessionState.LoggedIn"); @@ -59,6 +65,7 @@ export const codecForSessionStateExpired = (): Codec<Expired> => buildCodecForObject<Expired>() .property("status", codecForConstString("expired")) .property("username", codecForString()) + .property("expiration", codecOptionalDefault(codecForAbsoluteTime, AbsoluteTime.now())) .property("isUserAdministrator", codecForBoolean()) .build("SessionState.Expired"); @@ -83,7 +90,7 @@ export interface SessionStateHandler { state: SessionState; logOut(): void; expired(): void; - logIn(info: { username: string; token: AccessToken }): void; + logIn(info: { username: string; token: AccessToken, expiration: AbsoluteTime }): void; } const SESSION_STATE_KEY = buildStorageKey( @@ -112,6 +119,7 @@ export function useSessionState(): SessionStateHandler { const nextState: SessionState = { status: "expired", username: state.username, + expiration: state.expiration, isUserAdministrator: state.username === "admin", }; update(nextState); diff --git a/packages/bank-ui/src/pages/BankFrame.tsx b/packages/bank-ui/src/pages/BankFrame.tsx @@ -66,7 +66,7 @@ export function BankFrame({ const [, , resetBankState] = useBankState(); const d = useBankCoreApiContext(); const config = d === undefined ? undefined : d.config; - + const authenticator = d === undefined ? undefined : d.lib.auth const [error, resetError] = useErrorBoundary(); useEffect(() => { @@ -104,9 +104,12 @@ export function BankFrame({ session.state.status !== "loggedIn" ? undefined : () => { - session.logOut(); - resetBankState(); + if (session.state.status === "loggedIn" && authenticator) { + authenticator(session.state.username).deleteAccessToken(session.state.token) } + session.logOut(); + resetBankState(); + } } sites={ !settings.topNavSites ? [] : Object.entries(settings.topNavSites) diff --git a/packages/bank-ui/src/pages/LoginForm.tsx b/packages/bank-ui/src/pages/LoginForm.tsx @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { HttpStatusCode, createRFC8959AccessTokenEncoded, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util"; +import { AbsoluteTime, Duration, HttpStatusCode, createRFC8959AccessTokenEncoded, createRFC8959AccessTokenPlain } from "@gnu-taler/taler-util"; import { Button, LocalNotificationBanner, @@ -31,6 +31,10 @@ import { undefinedIfEmpty } from "../utils.js"; import { doAutoFocus } from "./PaytoWireTransferForm.js"; import { USERNAME_REGEX } from "./RegistrationPage.js"; +export const SESSION_DURATION = Duration.toTalerProtocolDuration(Duration.fromSpec({ + minutes: 30, +})) + /** * Collect and submit login data. */ @@ -45,6 +49,7 @@ export function LoginForm({ }): VNode { const session = useSessionState(); + const sessionState = session.state.status === "loggedIn" ? session.state : undefined; const sessionUser = session.state.status !== "loggedOut" ? session.state.username : undefined; const [username, setUsername] = useState<string | undefined>( @@ -73,6 +78,9 @@ export function LoginForm({ }); async function doLogout() { + if (sessionState) { + authenticator(sessionState.username).deleteAccessToken(sessionState.token) + } session.logOut(); } @@ -80,24 +88,24 @@ export function LoginForm({ !username || !password ? undefined : withErrorHandler( - async () => - authenticator(username).createAccessTokenBasic(username, password, { - scope: "readwrite", - duration: { d_us: "forever" }, - refreshable: true, - }), - (result) => { - session.logIn({ username, token: createRFC8959AccessTokenEncoded(result.body.access_token) }); - }, - (fail) => { - switch (fail.case) { - case HttpStatusCode.Unauthorized: - return i18n.str`Wrong credentials for "${username}"`; - case HttpStatusCode.NotFound: - return i18n.str`Account not found`; - } - }, - ); + async () => + authenticator(username).createAccessTokenBasic(username, password, { + scope: "readwrite", + duration: SESSION_DURATION, + refreshable: true, + }), + (result) => { + session.logIn({ username, token: createRFC8959AccessTokenEncoded(result.body.access_token), expiration: AbsoluteTime.fromProtocolTimestamp(result.body.expiration) }); + }, + (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Wrong credentials for "${username}"`; + case HttpStatusCode.NotFound: + return i18n.str`Account not found`; + } + }, + ); return ( <div class="flex min-h-full flex-col justify-center "> diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts @@ -88,9 +88,37 @@ export class TalerAuthenticationHttpClient { } /** - * + * FIXME: merge this with createAccessTokenBearer the protocol of both + * services need to reply the same + * * @returns */ + async createAccessTokenBearer_BANK(token: AccessToken, body: TokenRequest) { + const url = new URL(`token`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + headers: { + Authorization: makeBearerTokenAuthHeader(token), + }, + body, + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTokenSuccessResponse()); + //FIXME: missing in docs + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + /** + * + * @returns + */ async createAccessTokenBearer(token: AccessToken, body: TokenRequest) { const url = new URL(`token`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -125,6 +153,9 @@ export class TalerAuthenticationHttpClient { case HttpStatusCode.Ok: return opEmptySuccess(resp); //FIXME: missing in docs + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + //FIXME: missing in docs case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -635,7 +635,7 @@ export class TalerExchangeHttpClient { * */ async checkKycStatus( - account: ReserveAccount, + signingKey: SigningKey, paytoHash: string, params: { timeout?: number; @@ -654,7 +654,7 @@ export class TalerExchangeHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", headers: { - "Account-Owner-Signature": buildKYCQuerySignature(account.signingKey), + "Account-Owner-Signature": buildKYCQuerySignature(signingKey), }, });