Skip to content

Commit

Permalink
Refactor token storage & renewal with TokenState class #63
Browse files Browse the repository at this point in the history
  • Loading branch information
hupf committed Nov 9, 2023
1 parent 4600fb0 commit 37066d2
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 138 deletions.
5 changes: 2 additions & 3 deletions src/components/Content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import { when } from "lit/directives/when.js";
import { localized } from "@lit/localize";
import { StateController } from "@lit-app/state";
import { portalState } from "../state/portal-state";
import { getCurrentAccessToken } from "../utils/storage";
import { tokenState } from "../state/token-state";
import { theme } from "../utils/theme";
import { tokenMatchesScope } from "../utils/token";

@customElement("bkd-content")
@localized()
Expand Down Expand Up @@ -95,7 +94,7 @@ export class Content extends LitElement {
}

render() {
if (!tokenMatchesScope(getCurrentAccessToken(), portalState.app.scope)) {
if (tokenState.scope !== portalState.app.scope) {
// Token scope does not match current app, wait for correct
// token to be activated in <Portal> component to avoid requests
// resulting in 403 due to unsufficient rights.
Expand Down
11 changes: 3 additions & 8 deletions src/components/Header/SubstitutionsToggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import caretIcon from "../../assets/icons/caret.svg?raw";
import closeSmallIcon from "../../assets/icons/close-small.svg?raw";
import substitutionIcon from "../../assets/icons/substitution.svg?raw";
import { DropdownController } from "../../controllers/dropdown";
import { tokenState } from "../../state/token-state.ts";
import { Substitution, fetchCurrentSubstitutions } from "../../utils/fetch";
import { buildUrl } from "../../utils/routing.ts";
import { getCurrentAccessToken } from "../../utils/storage";
import { submit } from "../../utils/submit";
import { theme } from "../../utils/theme";
import { getTokenPayload } from "../../utils/token";
import { SubstitutionsDropdown } from "./SubstitutionsDropdown";

@customElement("bkd-substitutions-toggle")
Expand Down Expand Up @@ -133,11 +132,7 @@ export class SubstitutionsToggle extends LitElement {
}

private getActiveSubstitutionId(): number | null {
const token = getCurrentAccessToken();
if (!token) return null;

const { substitutionId } = getTokenPayload(token);
return substitutionId ?? null;
return tokenState.accessTokenPayload?.substitutionId ?? null;
}

private toggle(event: Event) {
Expand Down Expand Up @@ -181,7 +176,7 @@ export class SubstitutionsToggle extends LitElement {

private redirect(url: string): void {
submit("POST", url, {
access_token: getCurrentAccessToken() ?? "",
access_token: tokenState.accessToken ?? "",
redirect_uri: buildUrl("home"),
});
}
Expand Down
14 changes: 5 additions & 9 deletions src/components/Portal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { localized } from "@lit/localize";
import { StateController, Unsubscribe } from "@lit-app/state";
import { settings } from "../settings";
import { portalState } from "../state/portal-state";
import { tokenState } from "../state/token-state.ts";
import {
activateTokenForScope,
createOAuthClient,
Expand All @@ -14,16 +15,16 @@ import {
import { getInitialLocale } from "../utils/locale";
import { getNavigationItemByAppPath } from "../utils/navigation";
import { getHash, getScopeFromUrl, updateHash } from "../utils/routing";
import { getCurrentAccessToken } from "../utils/storage";
import {
customProperties,
fontFaces,
registerLightDomStyles,
theme,
} from "../utils/theme";
import { tokenMatchesScope } from "../utils/token.ts";
import { initializeTokenRenewal } from "../utils/token-renewal.ts";

const oAuthClient = createOAuthClient();
initializeTokenRenewal(oAuthClient);

const authReady = (async function () {
// Start Authorization Code Flow with PKCE
Expand Down Expand Up @@ -86,7 +87,7 @@ export class Portal extends LitElement {
// has no access to the navigation item from the redirect URL,
// hence is redirected to home (see
// https://github.com/bkd-mba-fbi/evento-portal/issues/106).
if (!tokenMatchesScope(getCurrentAccessToken(), portalState.app.scope)) {
if (tokenState.scope !== portalState.app.scope) {
activateTokenForScope(
oAuthClient,
portalState.app.scope,
Expand Down Expand Up @@ -126,11 +127,6 @@ export class Portal extends LitElement {
window.removeEventListener("hashchange", this.handleHashChange);
}

private isAuthenticated(): boolean {
const token = getCurrentAccessToken();
return Boolean(token);
}

/**
* Update the document title based on the current state
*/
Expand Down Expand Up @@ -201,7 +197,7 @@ export class Portal extends LitElement {
render() {
return html`
${when(
this.authReady && this.isAuthenticated(),
this.authReady && tokenState.authenticated,
() => html`
<bkd-header @bkdlogout=${this.handleLogout.bind(this)}></bkd-header>
<bkd-content></bkd-content>
Expand Down
15 changes: 6 additions & 9 deletions src/state/portal-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { fetchInstanceName, fetchUserAccessInfo } from "../utils/fetch";
import { getInitialLocale, getLocale, updateLocale } from "../utils/locale";
import { filterAllowed, getApp, getNavigationItem } from "../utils/navigation";
import { cleanupQueryParams, updateQueryParam } from "../utils/routing";
import { getCurrentAccessToken, storeLocale } from "../utils/storage";
import { getTokenPayload } from "../utils/token";
import { storeLocale } from "../utils/storage";
import { tokenState } from "./token-state";

export const LOCALE_QUERY_PARAM = "locale";
export const NAV_ITEM_QUERY_PARAM = "module";
Expand Down Expand Up @@ -182,10 +182,9 @@ export class PortalState extends State {
}

private updateNavigation(): void {
const token = getCurrentAccessToken();
if (!token) return;
const { instanceId } = tokenState;
if (!instanceId) return;

const { instanceId } = getTokenPayload(token);
this.navigation = filterAllowed(
settings.navigation,
instanceId,
Expand Down Expand Up @@ -237,16 +236,14 @@ export class PortalState extends State {
}

private async loadRolesAndPermissions(): Promise<void> {
const token = getCurrentAccessToken();
if (!token) return;
if (!tokenState.authenticated) return;

const { roles, permissions } = await fetchUserAccessInfo();
this.rolesAndPermissions = [...roles, ...permissions];
}

private async loadInstanceName(): Promise<void> {
const token = getCurrentAccessToken();
if (!token) return;
if (!tokenState.authenticated) return;

const instanceName = await fetchInstanceName();
this.instanceName = [msg("Evento"), instanceName]
Expand Down
152 changes: 152 additions & 0 deletions src/state/token-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import {
getCurrentAccessToken,
getRefreshToken,
resetAllTokens,
storeAccessToken,
storeCurrentAccessToken,
storeRefreshToken,
} from "../utils/storage";
import { TokenPayload, getTokenPayload, isTokenExpired } from "../utils/token";

type Subscriber = (token: TokenPayload | null) => void;
type Unsubscribe = () => void;

export class TokenState {
private _refreshToken = getRefreshToken();
private _refreshTokenPayload: TokenPayload | null = null;
private _accessToken = getCurrentAccessToken();
private _accessTokenPayload: TokenPayload | null = null;
private refreshTokenSubscribers: Subscriber[] = [];
private accessTokenSubscribers: Subscriber[] = [];

constructor() {
this.afterRefreshTokenUpdate(this.refreshToken, false);
this.afterAccessTokenUpdate(this.accessToken, false);
}

get refreshToken() {
return this._refreshToken;
}
set refreshToken(refreshToken: string | null) {
this._refreshToken = refreshToken;
this.afterRefreshTokenUpdate(refreshToken);
}

get refreshTokenPayload() {
return this._refreshTokenPayload;
}

get accessToken() {
return this._accessToken;
}
set accessToken(accessToken: string | null) {
this._accessToken = accessToken;
this.afterAccessTokenUpdate(accessToken);
}

get accessTokenPayload() {
return this._accessTokenPayload;
}

get authenticated(): boolean {
return Boolean(this.accessToken);
}

get scope(): string | null {
return this.accessTokenPayload?.scope ?? null;
}

get locale(): string | null {
return this.accessTokenPayload?.locale ?? null;
}

get instanceId(): string | null {
return this.accessTokenPayload?.instanceId ?? null;
}

isRefreshTokenExpired(): boolean {
return isTokenExpired(this._refreshTokenPayload);
}

resetAllTokens(): void {
this._refreshToken = null;
this._refreshTokenPayload = null;

this._accessToken = null;
this._accessTokenPayload = null;

resetAllTokens();
}

onRefreshTokenUpdate(callback: Subscriber): Unsubscribe {
this.refreshTokenSubscribers.push(callback);

// Initially call with current value
callback(this.refreshTokenPayload);

return () => {
const index = this.refreshTokenSubscribers.findIndex(
(s) => s === callback,
);
this.refreshTokenSubscribers.splice(index, 1);
};
}

onAccessTokenUpdate(callback: Subscriber): Unsubscribe {
this.accessTokenSubscribers.push(callback);

// Initially call with current value
callback(this.accessTokenPayload);

return () => {
const index = this.accessTokenSubscribers.findIndex(
(s) => s === callback,
);
this.accessTokenSubscribers.splice(index, 1);
};
}

private afterRefreshTokenUpdate(
refreshToken: string | null,
store = true,
): void {
this._refreshTokenPayload = refreshToken
? getTokenPayload(refreshToken)
: null;

if (refreshToken && store) {
storeRefreshToken(refreshToken);
}

this.notifyRefreshTokenSubscribers();
}

private afterAccessTokenUpdate(
accessToken: string | null,
store = true,
): void {
const payload = accessToken ? getTokenPayload(accessToken) : null;
this._accessTokenPayload = payload;

if (accessToken && payload && store) {
storeAccessToken(payload.scope, accessToken);
storeCurrentAccessToken(accessToken);
}

this.notifyAccessTokenSubscribers();
}

private notifyRefreshTokenSubscribers(): void {
this.refreshTokenSubscribers.forEach((callback) =>
callback(this.refreshTokenPayload),
);
}

private notifyAccessTokenSubscribers(): void {
this.accessTokenSubscribers.forEach((callback) =>
callback(this.accessTokenPayload),
);
}
}

export const tokenState = new TokenState();
Loading

0 comments on commit 37066d2

Please sign in to comment.