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

[CORL-3008] when auth token is invalid, clear it and reload the stream #4472

Merged
merged 3 commits into from
Jan 11, 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
2 changes: 1 addition & 1 deletion client/src/core/client/framework/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Claims, computeExpiresIn, parseAccessTokenClaims } from "./helpers";
/**
* ACCESS_TOKEN_KEY is the key in storage where the accessToken is stored.
*/
const ACCESS_TOKEN_KEY = "v2:accessToken";
export const ACCESS_TOKEN_KEY = "v2:accessToken";

export interface AuthState {
/**
Expand Down
20 changes: 18 additions & 2 deletions client/src/core/client/framework/lib/bootstrap/createManaged.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ interface CreateContextArguments {
* of the render window
*/
customScrollContainer?: HTMLElement;

/**
* called when a token is invalid and we are unable to login so we can
* clear the local auth token allowing a user to login again instead of
* throwing a network error that is impassable.
*/
onAuthError?: () => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a comment here explaining onAuthError, consistent with other args?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup!

}

/**
Expand Down Expand Up @@ -142,6 +149,7 @@ function createRelayEnvironment(
clientID: string,
localeBundles: FluentBundle[],
tokenRefreshProvider: TokenRefreshProvider,
onAuthError?: () => void,
clearCacheBefore?: Date
) {
const source = new RecordSource();
Expand All @@ -160,6 +168,7 @@ function createRelayEnvironment(
clientID,
accessTokenProvider,
localeBundles,
onAuthError,
tokenRefreshProvider.refreshToken,
clearCacheBefore
),
Expand Down Expand Up @@ -203,6 +212,8 @@ function createManagedCoralContextProvider(
clientID: string,
initLocalState: InitLocalState,
localesData: LocalesData,
localStorage: PromisifiedStorage<string>,
onAuthError?: () => void,
ErrorBoundary?: React.ComponentType<{ children?: React.ReactNode }>,
refreshAccessTokenPromise?: RefreshAccessTokenPromise,
staticConfig?: StaticConfig | null
Expand Down Expand Up @@ -270,6 +281,7 @@ function createManagedCoralContextProvider(
clientID,
this.state.context.localeBundles,
this.state.context.tokenRefreshProvider,
onAuthError,
// Disable the cache on requests for the next 30 seconds.
new Date(Date.now() + 30 * 1000)
);
Expand Down Expand Up @@ -339,7 +351,7 @@ function createManagedCoralContextProvider(
/*
* resolveStorage decides which storage to use in the context
*/
function resolveStorage(
export function resolveStorage(
type: "localStorage" | "sessionStorage" | "indexedDB"
): PromisifiedStorage {
switch (type) {
Expand Down Expand Up @@ -397,6 +409,7 @@ export default async function createManaged({
refreshAccessTokenPromise,
staticConfig = getStaticConfig(window),
customScrollContainer,
onAuthError,
}: CreateContextArguments): Promise<
ComponentType<{ children?: React.ReactNode }>
> {
Expand Down Expand Up @@ -468,7 +481,8 @@ export default async function createManaged({
subscriptionClient,
clientID,
localeBundles,
tokenRefreshProvider
tokenRefreshProvider,
onAuthError
);

// Assemble context.
Expand Down Expand Up @@ -522,6 +536,8 @@ export default async function createManaged({
clientID,
initLocalState,
localesData,
localStorage,
onAuthError,
reporter?.ErrorBoundary,
refreshAccessTokenPromise,
staticConfig
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function createNetwork(
clientID: string,
accessTokenProvider: AccessTokenProvider,
localeBundles: FluentBundle[],
onAuthError?: () => void,
tokenRefresh?: TokenRefresh,
clearCacheBefore?: Date
) {
Expand All @@ -78,7 +79,11 @@ export default function createNetwork(
retryDelays: (attempt: number) => Math.pow(2, attempt + 4) * 100,
// or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
statusCodes: [500, 503, 504],
beforeRetry: ({ abort, attempt, lastError }) => {
beforeRetry: async ({ abort, attempt, lastError }) => {
if (lastError?.name === "Missing Auth Error" && onAuthError) {
onAuthError();
}

if (attempt > 2) {
let message = lastError?.message;
if (message && lastError?.name !== "RRNLRetryMiddlewareError") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ShowAuthPopupMutation,
waitTillAuthPopupIsClosed,
} from "../../common/AuthPopup";
import { MissingAuthError } from "./missingAuthError";

const authControlQuery = graphql`
query RefreshTokenHandlerAuthControlQuery {
Expand Down Expand Up @@ -59,7 +60,7 @@ const RefreshTokenHandler: FunctionComponent = () => {
);

if (!data?.settings?.auth) {
throw new Error(
throw new MissingAuthError(
"Missing auth data. Make sure <UserBoxContainer /> has been rendered."
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class MissingAuthError extends Error {
public readonly name: string;

constructor(message: string | undefined) {
super(message);

this.name = "Missing Auth Error";
}
}
15 changes: 14 additions & 1 deletion client/src/core/client/stream/stream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { LanguageCode } from "coral-common/common/lib/helpers/i18n/locales";
import { parseQuery } from "coral-common/common/lib/utils";
import { RefreshAccessTokenCallback } from "coral-embed/Coral";
import { createManaged } from "coral-framework/lib/bootstrap";
import { RefreshAccessTokenPromise } from "coral-framework/lib/bootstrap/createManaged";
import {
RefreshAccessTokenPromise,
resolveStorage,
} from "coral-framework/lib/bootstrap/createManaged";
import {
CSSAsset,
EncapsulationContext,
Expand All @@ -28,6 +31,7 @@ import localesData from "./locales";
import { EmotionShadowRoot } from "./shadow";

// Import css variables.
import { ACCESS_TOKEN_KEY } from "coral-framework/lib/auth";
import "coral-ui/theme/streamEmbed.css";
import "coral-ui/theme/typography.css";

Expand Down Expand Up @@ -99,6 +103,13 @@ export async function attach(options: AttachOptions) {
await new Promise((resolve) => options.refreshAccessToken!(resolve));
}

const onContextAuthError = async () => {
const localStorage = resolveStorage("localStorage");
await localStorage.removeItem(ACCESS_TOKEN_KEY);
await remove(options.element);
await attach(options);
};

const ManagedCoralContextProvider = await createManaged({
rootURL: options.rootURL,
lang: options.locale,
Expand All @@ -111,6 +122,7 @@ export async function attach(options: AttachOptions) {
refreshAccessTokenPromise,
staticConfig: options.staticConfig,
customScrollContainer: options.customScrollContainer,
onAuthError: onContextAuthError,
});

// Amount of initial css files to be loaded.
Expand All @@ -134,6 +146,7 @@ export async function attach(options: AttachOptions) {
// flash of unstyled content.
const [isCSSLoaded, setIsCSSLoaded] = useState(false);
const [loadError, setLoadError] = useState(false);

const handleLoadError = useCallback((href: string) => {
globalErrorReporter.report(
// encode href, otherwise sentry will not send it.
Expand Down
Loading