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:
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),
},
});