Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix] websocket auth token handling #509

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ location accessible via HTTP(S), e.g., into an Apache HTTP server-managed locati
TermIt can operate in one of two authentication modes - using its internal user database and authentication means (default)
or via an OIDC authentication service such as [Keycloak](https://www.keycloak.org/). Corresponding parameters (service
URL, clientId) need to be set up (see the table above for the relevant parameters and explanation).

**example:**

```
REACT_APP_AUTHENTICATION=oidc
REACT_APP_AUTH_SERVER_URL=http://keycloak.lan/realms/termit
REACT_APP_AUTH_CLIENT_ID=termit-ui
```
24 changes: 12 additions & 12 deletions src/WebSocketApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,18 @@ export const WebSocketWrapper: React.FC<{
}> = ({ children, Provider = StompSessionProvider }) => {
const [securityToken, setSecurityToken] = useState<string>("");

useEffect(
() =>
BrowserStorage.onChange((e) => {
const token = SecurityUtils.loadToken();
// using length prevents from aborting websocket due to token refresh
// but will abort it when token is cleared or new one is set
if (token.length !== securityToken.length) {
setSecurityToken(token);
}
}),
[securityToken]
);
useEffect(() => {
const callback = () => {
const token = SecurityUtils.loadToken();
// using length prevents from aborting websocket due to token refresh
// but will abort it when token is cleared or new one is set
if (token.length !== securityToken.length) {
setSecurityToken(token);
}
};
callback();
return BrowserStorage.onTokenChange(callback);
}, [securityToken]);

return (
<Provider
Expand Down
3 changes: 3 additions & 0 deletions src/component/misc/oidc/OidcAuthWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, {
} from "react";
import { User, UserManager } from "oidc-client";
import { generateRedirectUri, getOidcConfig } from "../../../util/OidcUtils";
import BrowserStorage from "../../../util/BrowserStorage";

// Taken from https://github.com/datagov-cz/assembly-line-shared but using a different config processing mechanism

Expand Down Expand Up @@ -74,6 +75,7 @@ const OidcAuthWrapper: React.FC<AuthProps> = ({
} catch (error) {
throwError(error as Error);
}
BrowserStorage.dispatchTokenChangeEvent();
};
getUser();
}, [location, history, throwError, setUser, userManager]);
Expand All @@ -87,6 +89,7 @@ const OidcAuthWrapper: React.FC<AuthProps> = ({
} catch (error) {
throwError(error as Error);
}
BrowserStorage.dispatchTokenChangeEvent();
};

userManager.events.addUserLoaded(updateUserData);
Expand Down
18 changes: 10 additions & 8 deletions src/util/BrowserStorage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export const TOKEN_CHANGE_EVENT = "token-change";

/**
* Represent an interface to the browser-based storage
*/
const BrowserStorage = {
set(key: string, value: string): void {
localStorage.setItem(key, value);
this.dispatchStorageEvent();
},

get(key: string, defaultValue: string | null = null): string | null {
Expand All @@ -14,21 +15,22 @@ const BrowserStorage = {

remove(key: string): void {
localStorage.removeItem(key);
this.dispatchStorageEvent();
},

dispatchStorageEvent() {
window.dispatchEvent(new Event("storage"));
dispatchTokenChangeEvent() {
window.dispatchEvent(
new Event(TOKEN_CHANGE_EVENT, { bubbles: true, cancelable: false })
);
},

/**
* Adds {@link #callback} as event listener for the {@code storage} event.
* Adds {@link #callback} as event listener for the {@link TOKEN_CHANGE_EVENT}.
* @param callback the event listener
* @returns unsubscribe callback
*/
onChange(callback: (event: StorageEvent) => void) {
window.addEventListener("storage", callback);
return () => window.removeEventListener("storage", callback);
onTokenChange(callback: (event: Event) => void) {
window.addEventListener(TOKEN_CHANGE_EVENT, callback);
return () => window.removeEventListener(TOKEN_CHANGE_EVENT, callback);
},
};

Expand Down
9 changes: 7 additions & 2 deletions src/util/SecurityUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getOidcIdentityStorageKey, isUsingOidcAuth } from "./OidcUtils";
export default class SecurityUtils {
public static saveToken(jwt: string): void {
BrowserStorage.set(Constants.STORAGE_JWT_KEY, jwt);
BrowserStorage.dispatchTokenChangeEvent();
}

public static loadToken(): string {
Expand All @@ -20,13 +21,17 @@ export default class SecurityUtils {
/**
* Return access token of the currently logged-in user.
* To be used as an Authorization header content for API fetch calls.
* @return Authorization header contents if the token is available, empty string otherwise
*/
private static getOidcToken(): string {
const identityData = sessionStorage.getItem(getOidcIdentityStorageKey());
const identity = identityData
? JSON.parse(identityData)
: (null as User | null);
return `${identity?.token_type} ${identity?.access_token}`;
if (identity) {
return `${identity.token_type} ${identity.access_token}`;
}
return "";
}

public static clearToken(): void {
Expand All @@ -35,7 +40,7 @@ export default class SecurityUtils {
} else {
BrowserStorage.remove(Constants.STORAGE_JWT_KEY);
}
BrowserStorage.dispatchStorageEvent();
BrowserStorage.dispatchTokenChangeEvent();
}

public static isLoggedIn(currentUser?: User | null): boolean {
Expand Down