diff --git a/src/core/client/account/local/initLocalState.ts b/src/core/client/account/local/initLocalState.ts index 9579aceed61..aba8b459acf 100644 --- a/src/core/client/account/local/initLocalState.ts +++ b/src/core/client/account/local/initLocalState.ts @@ -1,5 +1,6 @@ import { Environment } from "relay-runtime"; +import { AuthState } from "coral-framework/lib/auth"; import { CoralContext } from "coral-framework/lib/bootstrap"; import { initLocalBaseState } from "coral-framework/lib/relay"; @@ -8,7 +9,8 @@ import { initLocalBaseState } from "coral-framework/lib/relay"; */ export default async function initLocalState( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) { - initLocalBaseState(environment, context); + initLocalBaseState(environment, context, auth); } diff --git a/src/core/client/admin/local/initLocalState.spec.ts b/src/core/client/admin/local/initLocalState.spec.ts index d678b05b99b..1d769048582 100644 --- a/src/core/client/admin/local/initLocalState.spec.ts +++ b/src/core/client/admin/local/initLocalState.spec.ts @@ -1,6 +1,5 @@ import { Environment, RecordSource } from "relay-runtime"; -import Auth from "coral-framework/lib/auth"; import { LOCAL_ID } from "coral-framework/lib/relay"; import { createAccessToken, @@ -14,7 +13,6 @@ let environment: Environment; let source: RecordSource; const context = { - auth: new Auth(), localStorage: window.localStorage, sessionStorage: window.sessionStorage, }; diff --git a/src/core/client/admin/local/initLocalState.ts b/src/core/client/admin/local/initLocalState.ts index 0dee2648856..f1c31db593c 100644 --- a/src/core/client/admin/local/initLocalState.ts +++ b/src/core/client/admin/local/initLocalState.ts @@ -2,6 +2,7 @@ import { commitLocalUpdate, Environment } from "relay-runtime"; import { REDIRECT_PATH_KEY } from "coral-admin/constants"; import { clearHash, getParamsFromHash } from "coral-framework/helpers"; +import { AuthState, updateAccessToken } from "coral-framework/lib/auth"; import { CoralContext } from "coral-framework/lib/bootstrap"; import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay"; @@ -10,7 +11,8 @@ import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay"; */ export default async function initLocalState( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) { // Initialize the redirect path in case we don't need to redirect somewhere. let redirectPath: string | null = null; @@ -29,7 +31,7 @@ export default async function initLocalState( // If there was an access token, store it. if (params.accessToken) { - context.auth.set(params.accessToken); + auth = updateAccessToken(params.accessToken); } // As we are in the middle of an auth flow (given that there was something @@ -42,7 +44,7 @@ export default async function initLocalState( await context.localStorage.setItem(REDIRECT_PATH_KEY, ""); } - initLocalBaseState(environment, context); + initLocalBaseState(environment, context, auth); commitLocalUpdate(environment, (s) => { const localRecord = s.get(LOCAL_ID)!; diff --git a/src/core/client/auth/local/initLocalState.spec.ts b/src/core/client/auth/local/initLocalState.spec.ts index 2d866e3f23c..ec0bb9c2ffe 100644 --- a/src/core/client/auth/local/initLocalState.spec.ts +++ b/src/core/client/auth/local/initLocalState.spec.ts @@ -1,6 +1,5 @@ import { Environment, RecordSource } from "relay-runtime"; -import Auth from "coral-framework/lib/auth"; import { LOCAL_ID } from "coral-framework/lib/relay"; import { createAccessToken, @@ -14,7 +13,6 @@ let environment: Environment; let source: RecordSource; const context = { - auth: new Auth(), localStorage: window.localStorage, sessionStorage: window.sessionStorage, }; diff --git a/src/core/client/auth/local/initLocalState.ts b/src/core/client/auth/local/initLocalState.ts index d4ed8be3f1b..16e047e0528 100644 --- a/src/core/client/auth/local/initLocalState.ts +++ b/src/core/client/auth/local/initLocalState.ts @@ -1,8 +1,8 @@ -/* eslint-disable prettier/prettier */ import { commitLocalUpdate, Environment } from "relay-runtime"; import { parseQuery } from "coral-common/utils"; import { getParamsFromHashAndClearIt } from "coral-framework/helpers"; +import { AuthState, updateAccessToken } from "coral-framework/lib/auth"; import { CoralContext } from "coral-framework/lib/bootstrap"; import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay"; @@ -11,15 +11,16 @@ import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay"; */ export default async function initLocalState( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) { const { error = null, accessToken = null } = getParamsFromHashAndClearIt(); if (accessToken) { - context.auth.set(accessToken); + auth = updateAccessToken(accessToken); } - initLocalBaseState(environment, context); + initLocalBaseState(environment, context, auth); commitLocalUpdate(environment, (s) => { const localRecord = s.get(LOCAL_ID)!; diff --git a/src/core/client/framework/lib/auth/Auth.ts b/src/core/client/framework/lib/auth/Auth.ts deleted file mode 100644 index 15f9f31120d..00000000000 --- a/src/core/client/framework/lib/auth/Auth.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { createInMemoryStorage } from "../storage"; -import JWTManager from "./JWTManager"; - -class Auth { - private manager: JWTManager; - - constructor(storage: Storage = createInMemoryStorage()) { - // Create the new LockBox instance based on the provided storage. - this.manager = new JWTManager(storage); - } - - /** - * getAccessToken will return the current access token or an empty string. - */ - public readonly getAccessToken = () => { - const accessToken = this.manager.getAccessToken(); - if (!accessToken) { - return ""; - } - - return accessToken; - }; - - public readonly getClaims = () => { - const claims = this.manager.getClaims(); - if (!claims) { - return {}; - } - - return claims; - }; - - /** - * set will either clear the existing token if non is provided or update it to - * the passed token. - * - * @param accessToken possibly the new token to use - */ - public readonly set = (accessToken?: string | null) => { - if (!accessToken) { - this.manager.remove(); - - return false; - } - - return this.manager.set(accessToken); - }; - - /** - * remove will remove the access token from storage and memory. - */ - public readonly remove = () => { - return this.manager.remove(); - }; - - /** - * onChange will register a function to be called when the access token is - * changed. It returns a function that will unsubscribe from the changes. - * - * @param fn the function to call when access token changes - */ - public readonly onChange = (fn: () => void) => { - // Every time that the token is "changed", fire this function. - this.manager.addListener("changed", fn); - - return () => { - this.manager.removeListener("changed", fn); - }; - }; -} - -export default Auth; diff --git a/src/core/client/framework/lib/auth/JWTManager.ts b/src/core/client/framework/lib/auth/JWTManager.ts deleted file mode 100644 index a1362bf9489..00000000000 --- a/src/core/client/framework/lib/auth/JWTManager.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { EventEmitter } from "events"; -import { clearLongTimeout, LongTimeout, setLongTimeout } from "long-settimeout"; - -import { Claims, computeExpiresIn, parseAccessTokenClaims } from "./helpers"; - -const ACCESS_TOKEN_KEY = "coral:v1:accessToken"; - -class JWTManager extends EventEmitter { - private readonly storage: Storage; - - private timeout: LongTimeout | null = null; - private accessToken: string | null = null; - private accessTokenClaims: Claims | null = null; - - constructor(storage: Storage) { - // Setup the underlying event emitter. - super(); - - // Store the storage reference. - this.storage = storage; - - // Try to get the accessToken from storage now. - const accessToken = this.storage.getItem(ACCESS_TOKEN_KEY); - if (!accessToken) { - // There is no token in persisted storage! Nothing to do. - return; - } - - // Now that we got a token, let's set it up. If `set` returns false, it - // implies that the token was invalid or expired. - const set = this.set(accessToken); - if (!set) { - this.remove(); - } - - // TODO: subscribe to storage change notifications - } - - public getAccessToken(): string | null { - return this.accessToken; - } - - public getClaims(): Claims | null { - return this.accessTokenClaims; - } - - /** - * set will update the access token in memory and persist it to storage. If - * there is an expiry on the token, a callback will be registered to - * automatically clear it when the timer expires. - * - * @param accessToken the new access token to use - */ - public set(accessToken: string) { - // Try to parse the token claims. - const accessTokenClaims = parseAccessTokenClaims(accessToken); - if (!accessTokenClaims) { - return false; - } - - // If the token has an expiry, then register a callback to remove it. - if (accessTokenClaims.exp) { - const expiresIn = computeExpiresIn(accessTokenClaims.exp); - if (!expiresIn) { - // The token has expired, don't set anything! - return false; - } - - // If there was an existing timeout, clear it. - if (this.timeout) { - clearLongTimeout(this.timeout); - } - - // Create the new timeout to clear out the token. - this.timeout = setLongTimeout(() => { - // Remove the access token when it expires. - this.remove(); - - // Emit that the stored access token has been updated. - this.emit("expired"); - }, expiresIn); - } - - // Now that we've verified that the token is valid (is a JWT), and have - // handled if the token is expired or not, we should store it in memory and - // persist it to storage. - this.accessToken = accessToken; - this.accessTokenClaims = accessTokenClaims; - this.storage.setItem(ACCESS_TOKEN_KEY, accessToken); - - // Emit that the stored access token has been updated. - this.emit("updated"); - this.emit("changed"); - - return true; - } - - /** - * remove will remove the access token from memory and remove the expiry - * callback. - */ - public remove() { - // Remove the access token from memory. - this.accessToken = null; - this.accessTokenClaims = null; - - // If there is an active timeout, remove it. - if (this.timeout) { - clearLongTimeout(this.timeout); - this.timeout = null; - } - - // Remove the token from storage. - this.storage.removeItem(ACCESS_TOKEN_KEY); - - // Emit that the stored access token has been removed. - this.emit("removed"); - this.emit("changed"); - } -} - -export default JWTManager; diff --git a/src/core/client/framework/lib/auth/auth.ts b/src/core/client/framework/lib/auth/auth.ts new file mode 100644 index 00000000000..1e91dc14ed7 --- /dev/null +++ b/src/core/client/framework/lib/auth/auth.ts @@ -0,0 +1,93 @@ +import { Claims, computeExpiresIn, parseAccessTokenClaims } from "./helpers"; + +/** + * ACCESS_TOKEN_KEY is the key in storage where the accessToken is stored. + */ +const ACCESS_TOKEN_KEY = "coral:v1:accessToken"; + +/** + * storage is the Storage used to retrieve/update/delete access tokens on. + */ +const storage = localStorage; + +export interface AuthState { + /** + * accessToken is the access token issued by the server. + */ + accessToken: string; + + /** + * claims are the parsed claims from the access token. + */ + claims: Claims; +} + +export type AccessTokenProvider = () => string | undefined; + +function parseAccessToken(accessToken: string) { + // Try to parse the access token claims. + const claims = parseAccessTokenClaims(accessToken); + if (!claims) { + // Claims couldn't be parsed. + return; + } + + if (claims.exp) { + const expiresIn = computeExpiresIn(claims.exp); + if (!expiresIn) { + // Looks like the access token has expired. + return; + } + } + + return { accessToken, claims }; +} + +export function retrieveAccessToken() { + try { + // Get the access token from storage. + const accessToken = storage.getItem(ACCESS_TOKEN_KEY); + if (!accessToken) { + // Looks like the access token wasn't in storage. + return; + } + + // Return the parsed access token. + return parseAccessToken(accessToken); + } catch (err) { + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("could not get access token from storage", err); + } + + return; + } +} + +export function updateAccessToken(accessToken: string) { + try { + // Update the access token in storage. + storage.setItem(ACCESS_TOKEN_KEY, accessToken); + } catch (err) { + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("could not set access token in storage", err); + } + } + + // Return the parsed access token. + return parseAccessToken(accessToken); +} + +export function deleteAccessToken() { + try { + storage.removeItem(ACCESS_TOKEN_KEY); + } catch (err) { + if (process.env.NODE_ENV !== "production") { + // eslint-disable-next-line no-console + console.error("could not remove access token from storage", err); + } + } + + return undefined; +} diff --git a/src/core/client/framework/lib/auth/index.ts b/src/core/client/framework/lib/auth/index.ts index 121aa884943..97ccf76494a 100644 --- a/src/core/client/framework/lib/auth/index.ts +++ b/src/core/client/framework/lib/auth/index.ts @@ -1 +1 @@ -export { default } from "./Auth"; +export * from "./auth"; diff --git a/src/core/client/framework/lib/bootstrap/CoralContext.tsx b/src/core/client/framework/lib/bootstrap/CoralContext.tsx index 4845f57e800..1f77da3aacf 100644 --- a/src/core/client/framework/lib/bootstrap/CoralContext.tsx +++ b/src/core/client/framework/lib/bootstrap/CoralContext.tsx @@ -16,8 +16,6 @@ import { TransitionControlData } from "coral-framework/testHelpers"; import { UIContext } from "coral-ui/components"; import { ClickFarAwayRegister } from "coral-ui/components/ClickOutside"; -import Auth from "../auth"; - export interface CoralContext { /** relayEnvironment for our relay framework. */ relayEnvironment: Environment; @@ -67,20 +65,11 @@ export interface CoralContext { /** Clear session data. */ clearSession: (nextAccessToken?: string | null) => Promise; - /** - * cleanupCallbacks is callbacks that are called and reset when the session - * is cleared - */ - cleanupCallbacks: Function[]; - /** Change locale and rerender */ changeLocale: (locale: LanguageCode) => Promise; /** Controls router transitions (for tests) */ transitionControl?: TransitionControlData; - - /** handles managing and persisting the authentication state */ - auth: Auth; } export const CoralReactContext = React.createContext({} as any); diff --git a/src/core/client/framework/lib/bootstrap/createManaged.tsx b/src/core/client/framework/lib/bootstrap/createManaged.tsx index 325a34bd3a0..5f860bcb4aa 100644 --- a/src/core/client/framework/lib/bootstrap/createManaged.tsx +++ b/src/core/client/framework/lib/bootstrap/createManaged.tsx @@ -19,7 +19,13 @@ import { } from "coral-framework/lib/storage"; import { ClickFarAwayRegister } from "coral-ui/components/ClickOutside"; -import Auth from "../auth"; +import { + AccessTokenProvider, + AuthState, + deleteAccessToken, + retrieveAccessToken, + updateAccessToken, +} from "../auth"; import { generateBundles, LocalesData } from "../i18n"; import { createManagedSubscriptionClient, @@ -27,13 +33,14 @@ import { ManagedSubscriptionClient, } from "../network"; import { PostMessageService } from "../postMessage"; -import { syncAuthWithLocalState } from "../relay/localState"; +import { LOCAL_ID } from "../relay"; import { CoralContext, CoralContextProvider } from "./CoralContext"; import SendPymReady from "./SendPymReady"; export type InitLocalState = ( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) => void | Promise; interface CreateContextArguments { @@ -99,17 +106,31 @@ function areWeInIframe() { function createRelayEnvironment( subscriptionClient: ManagedSubscriptionClient, - auth: Auth, - clientID: string + clientID: string, + accessToken?: string ) { - return new Environment({ - network: createNetwork(subscriptionClient, auth.getAccessToken, clientID), - store: new Store(new RecordSource()), + const source = new RecordSource(); + const accessTokenProvider: AccessTokenProvider = () => { + const local = source.get(LOCAL_ID); + if (!local) { + return; + } + + return local.accessToken as string | undefined; + }; + const environment = new Environment({ + network: createNetwork(subscriptionClient, clientID, accessTokenProvider), + store: new Store(source), }); + + return { environment, accessTokenProvider }; } -function createRestClient(auth: Auth, clientID: string) { - return new RestClient("/api", auth.getAccessToken, clientID); +function createRestClient( + clientID: string, + accessTokenProvider: AccessTokenProvider +) { + return new RestClient("/api", clientID, accessTokenProvider); } /** @@ -139,43 +160,37 @@ function createManagedCoralContextProvider( } // This is called every time a user session starts or ends. - private clearSession = async (nextAccessToken?: string | null) => { + private clearSession = async (nextAccessToken?: string) => { + // Clear session storage. + this.state.context.sessionStorage.clear(); + // Pause subscriptions. subscriptionClient.pause(); - // Call all functions to cleanup. - this.state.context.cleanupCallbacks.forEach((cb) => cb()); - - // Update the token. - this.state.context.auth.set(nextAccessToken); + // Parse the claims/token and update storage. + const auth = nextAccessToken + ? updateAccessToken(nextAccessToken) + : deleteAccessToken(); // Create the new environment. - const environment = createRelayEnvironment( + const { environment, accessTokenProvider } = createRelayEnvironment( subscriptionClient, - this.state.context.auth, - clientID + clientID, + auth?.accessToken ); - // Reset the cleanup callbacks. - this.state.context.cleanupCallbacks = []; - // Create the new context. const newContext: CoralContext = { ...this.state.context, relayEnvironment: environment, - rest: createRestClient(this.state.context.auth, clientID), + rest: createRestClient(clientID, accessTokenProvider), }; // Initialize local state. - await initLocalState(newContext.relayEnvironment, newContext); + await initLocalState(newContext.relayEnvironment, newContext, auth); - // Configure the syncing of the relay environment with the auth changes. - this.state.context.cleanupCallbacks.push( - syncAuthWithLocalState(environment, this.state.context.auth) - ); - - // Update the token. - this.state.context.auth.set(nextAccessToken); + // Update the subscription client access token. + subscriptionClient.setAccessToken(accessTokenProvider()); // Propagate new context. this.setState({ context: newContext }, () => { @@ -289,27 +304,25 @@ export default async function createManaged({ const localeBundles = await generateBundles(locales, localesData); - // Setup the auth manager backed on localStorage. - const auth = new Auth(localStorage); + // Get the access token from storage. + const auth = retrieveAccessToken(); /** clientID is sent to the server with every request */ const clientID = uuid(); const subscriptionClient = createManagedSubscriptionClient( websocketURL, - auth, clientID ); - const environment = createRelayEnvironment( + const { environment, accessTokenProvider } = createRelayEnvironment( subscriptionClient, - auth, - clientID + clientID, + auth?.accessToken ); // Assemble context. const context: CoralContext = { - auth, relayEnvironment: environment, locales, localeBundles, @@ -317,7 +330,7 @@ export default async function createManaged({ pym, eventEmitter, registerClickFarAway, - rest: createRestClient(auth, clientID), + rest: createRestClient(clientID, accessTokenProvider), postMessage: new PostMessageService(), localStorage: resolveLocalStorage(pym), sessionStorage: resolveSessionStorage(pym), @@ -326,17 +339,18 @@ export default async function createManaged({ // Noop, this is later replaced by the // managed CoralContextProvider. clearSession: (nextAccessToken?: string | null) => Promise.resolve(), - cleanupCallbacks: [], // Noop, this is later replaced by the // managed CoralContextProvider. changeLocale: (locale?: LanguageCode) => Promise.resolve(), }; // Initialize local state. - await initLocalState(context.relayEnvironment, context); + await initLocalState(context.relayEnvironment, context, auth); - // Configure the syncing of the relay environment with the auth changes. - context.cleanupCallbacks.push(syncAuthWithLocalState(environment, auth)); + // Set new token for the websocket connection. + // TODO: (cvle) dynamically reset when token changes. + // ^ only necessary when we can prolong existing session using a new token. + subscriptionClient.setAccessToken(accessTokenProvider()); // Returns a managed CoralContextProvider, that includes the above // context and handles context changes, e.g. when a user session changes. diff --git a/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts b/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts index 0a81463aa63..06d57ba784c 100644 --- a/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts +++ b/src/core/client/framework/lib/network/createManagedSubscriptionClient.ts @@ -12,8 +12,6 @@ import { import { ACCESS_TOKEN_PARAM, CLIENT_ID_PARAM } from "coral-common/constants"; import { ERROR_CODES } from "coral-common/errors"; -import Auth from "../auth"; - /** * SubscriptionRequest contains the subscription * request data that comes from Relay. @@ -28,14 +26,14 @@ export interface SubscriptionRequest { } /** - * ManagedSubscriptionClient builts on top of `SubscriptionClient` + * ManagedSubscriptionClient builds on top of `SubscriptionClient` * and manages the websocket connection economically. A connection is - * only establish when there is at least 1 active susbcription and closes + * only establish when there is at least 1 active subscription and closes * when there is no more active subscriptions. */ export interface ManagedSubscriptionClient { /** - * Susbcribe to a GraphQL subscription, this is usually called from + * Subscribe to a GraphQL subscription, this is usually called from * the SubscriptionFunction provided to Relay. */ subscribe( @@ -48,23 +46,24 @@ export interface ManagedSubscriptionClient { pause(): void; /** Resume all subscriptions eventually causing websocket to start with new connection parameters */ resume(): void; + /** Sets access token and restarts the websocket connection */ + setAccessToken(accessToken?: string): void; } /** * Creates a ManagedSubscriptionClient * * @param url url of the graphql live server - * @param auth the auth client to use and subscribe to for authentication changes * @param clientID a clientID that is provided to the graphql live server */ export default function createManagedSubscriptionClient( url: string, - auth: Auth, clientID: string ): ManagedSubscriptionClient { const requests: SubscriptionRequest[] = []; let subscriptionClient: SubscriptionClient | null = null; let paused = false; + let accessToken: string | undefined; const closeClient = () => { if (subscriptionClient) { @@ -109,7 +108,7 @@ export default function createManagedSubscriptionClient( } }, connectionParams: { - [ACCESS_TOKEN_PARAM]: auth.getAccessToken(), + [ACCESS_TOKEN_PARAM]: accessToken, [CLIENT_ID_PARAM]: clientID, }, }); @@ -144,7 +143,7 @@ export default function createManagedSubscriptionClient( // Register the request. requests.push(request as SubscriptionRequest); - // Start susbcription if we are not paused. + // Start subscription if we are not paused. if (!paused) { request.subscribe(); } @@ -180,7 +179,7 @@ export default function createManagedSubscriptionClient( r.unsubscribe = null; } } - // Close websocket conncetion. + // Close websocket connection. closeClient(); }; @@ -194,17 +193,14 @@ export default function createManagedSubscriptionClient( paused = false; }; - // Register when the access token changes. - auth.onChange(() => { - if (!paused) { - pause(); - resume(); - } - }); + const setAccessToken = (nextAccessToken?: string) => { + accessToken = nextAccessToken; + }; return Object.freeze({ subscribe, pause, resume, + setAccessToken, }); } diff --git a/src/core/client/framework/lib/network/createNetwork.ts b/src/core/client/framework/lib/network/createNetwork.ts index 0014ea262bc..f85f82fa9b7 100644 --- a/src/core/client/framework/lib/network/createNetwork.ts +++ b/src/core/client/framework/lib/network/createNetwork.ts @@ -10,13 +10,12 @@ import { GraphQLResponse, Observable, SubscribeFunction } from "relay-runtime"; import TIME from "coral-common/time"; import getLocationOrigin from "coral-framework/utils/getLocationOrigin"; +import { AccessTokenProvider } from "../auth"; import clientIDMiddleware from "./clientIDMiddleware"; import { ManagedSubscriptionClient } from "./createManagedSubscriptionClient"; import customErrorMiddleware from "./customErrorMiddleware"; import persistedQueriesGetMethodMiddleware from "./persistedQueriesGetMethodMiddleware"; -export type TokenGetter = () => string; - const graphqlURL = `${getLocationOrigin()}/api/graphql`; function createSubscriptionFunction( @@ -44,8 +43,8 @@ function createSubscriptionFunction( export default function createNetwork( subscriptionClient: ManagedSubscriptionClient, - tokenGetter: TokenGetter, - clientID: string + clientID: string, + accessTokenProvider: AccessTokenProvider ) { return new RelayNetworkLayer( [ @@ -70,7 +69,9 @@ export default function createNetwork( }, }), authMiddleware({ - token: tokenGetter, + token: () => { + return accessTokenProvider() || ""; + }, }), clientIDMiddleware(clientID), persistedQueriesGetMethodMiddleware, diff --git a/src/core/client/framework/lib/network/index.ts b/src/core/client/framework/lib/network/index.ts index aa8c3833207..ad90a4978aa 100644 --- a/src/core/client/framework/lib/network/index.ts +++ b/src/core/client/framework/lib/network/index.ts @@ -1,4 +1,4 @@ -export { default as createNetwork, TokenGetter } from "./createNetwork"; +export { default as createNetwork } from "./createNetwork"; export { default as extractGraphQLError } from "./extractGraphQLError"; export { default as extractError } from "./extractError"; export { diff --git a/src/core/client/framework/lib/relay/localState.ts b/src/core/client/framework/lib/relay/localState.ts index b3685e12213..295e058588e 100644 --- a/src/core/client/framework/lib/relay/localState.ts +++ b/src/core/client/framework/lib/relay/localState.ts @@ -1,12 +1,8 @@ -import { - commitLocalUpdate, - Environment, - RecordSourceProxy, -} from "relay-runtime"; +import { commitLocalUpdate, Environment } from "relay-runtime"; -import Auth from "coral-framework/lib/auth"; import { createAndRetain } from "coral-framework/lib/relay"; +import { AuthState } from "../auth"; import { CoralContext } from "../bootstrap"; /** @@ -19,40 +15,33 @@ export const LOCAL_TYPE = "Local"; */ export const LOCAL_ID = "client:root.local"; -export function syncAuthWithLocalState(environment: Environment, auth: Auth) { - // Attach a listener to access token changes to update the local graph. - return auth.onChange(() => { - commitLocalUpdate(environment, (source) => { - setAccessTokenRecordValues(source, auth); - }); - }); -} - -function setAccessTokenRecordValues(source: RecordSourceProxy, auth: Auth) { - // Get the local record. - const localRecord = source.get(LOCAL_ID)!; - - // Update the access token properties. - const accessToken = auth.getAccessToken(); - localRecord.setValue(accessToken, "accessToken"); - - // Update the claims. - const { jti = null, exp = null } = auth.getClaims(); - localRecord.setValue(exp, "accessTokenExp"); - localRecord.setValue(jti, "accessTokenJTI"); -} - +/** + * initLocalBaseState will initialize the local base relay state. If as a part + * of your target you need to change the auth state, you can do so by passing a + * new auth state object into this function when committing. + * + * @param environment the initialized relay environment + * @param context application context + * @param auth application auth state + */ export function initLocalBaseState( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) { - commitLocalUpdate(environment, (s) => { - const root = s.getRoot(); + commitLocalUpdate(environment, (source) => { + const root = source.getRoot(); // Create the Local Record which is the Root for the client states. - const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE); - root.setLinkedRecord(localRecord, "local"); + const local = createAndRetain(environment, source, LOCAL_ID, LOCAL_TYPE); + + root.setLinkedRecord(local, "local"); + + // Update the access token properties. + local.setValue(auth?.accessToken, "accessToken"); - setAccessTokenRecordValues(s, context.auth); + // Update the claims. + local.setValue(auth?.claims.exp, "accessTokenExp"); + local.setValue(auth?.claims.jti, "accessTokenJTI"); }); } diff --git a/src/core/client/framework/lib/rest.ts b/src/core/client/framework/lib/rest.ts index 36e6d1a10dd..96fbed9d86e 100644 --- a/src/core/client/framework/lib/rest.ts +++ b/src/core/client/framework/lib/rest.ts @@ -3,7 +3,8 @@ import { merge } from "lodash"; import { CLIENT_ID_HEADER } from "coral-common/constants"; import { Overwrite } from "coral-framework/types"; -import { extractError, TokenGetter } from "./network"; +import { AccessTokenProvider } from "./auth"; +import { extractError } from "./network"; const buildOptions = (inputOptions: RequestInit = {}) => { const defaultOptions: RequestInit = { @@ -53,13 +54,17 @@ type PartialRequestInit = Overwrite, { body?: any }> & { export class RestClient { public readonly uri: string; - private tokenGetter?: TokenGetter; private clientID?: string; + private accessTokenProvider?: AccessTokenProvider; - constructor(uri: string, tokenGetter?: TokenGetter, clientID?: string) { + constructor( + uri: string, + clientID?: string, + accessTokenProvider?: AccessTokenProvider + ) { this.uri = uri; - this.tokenGetter = tokenGetter; this.clientID = clientID; + this.accessTokenProvider = accessTokenProvider; } public async fetch( @@ -67,7 +72,8 @@ export class RestClient { options: PartialRequestInit ): Promise { let opts = options; - const token = options.token || (this.tokenGetter && this.tokenGetter()); + const token = + options.token || (this.accessTokenProvider && this.accessTokenProvider()); if (token) { opts = merge({}, options, { headers: { @@ -75,6 +81,7 @@ export class RestClient { }, }); } + if (this.clientID) { opts = merge({}, opts, { headers: { @@ -82,7 +89,9 @@ export class RestClient { }, }); } + const response = await fetch(`${this.uri}${path}`, buildOptions(opts)); + return handleResp(response); } } diff --git a/src/core/client/framework/testHelpers/createTestRenderer.tsx b/src/core/client/framework/testHelpers/createTestRenderer.tsx index b8ecb243d0e..e7a0d2b9e5a 100644 --- a/src/core/client/framework/testHelpers/createTestRenderer.tsx +++ b/src/core/client/framework/testHelpers/createTestRenderer.tsx @@ -8,13 +8,11 @@ import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime"; import sinon from "sinon"; import { RequireProperty } from "coral-common/types"; -import Auth from "coral-framework/lib/auth"; import { CoralContext, CoralContextProvider, } from "coral-framework/lib/bootstrap"; import { PostMessageService } from "coral-framework/lib/postMessage"; -import { syncAuthWithLocalState } from "coral-framework/lib/relay/localState"; import { RestClient } from "coral-framework/lib/rest"; import { createPromisifiedStorage } from "coral-framework/lib/storage"; import { createUUIDGenerator } from "coral-framework/testHelpers"; @@ -90,8 +88,6 @@ export default function createTestRenderer< }); const context: RequireProperty = { - auth: new Auth(), - cleanupCallbacks: [], relayEnvironment: environment, locales: ["en-US"], localeBundles: [ @@ -119,8 +115,6 @@ export default function createTestRenderer< }, }; - syncAuthWithLocalState(context.relayEnvironment, context.auth); - let testRenderer: ReactTestRenderer; TestRenderer.act(() => { testRenderer = TestRenderer.create( diff --git a/src/core/client/install/local/initLocalState.ts b/src/core/client/install/local/initLocalState.ts index 1073a1c9e76..ccf7e22463c 100644 --- a/src/core/client/install/local/initLocalState.ts +++ b/src/core/client/install/local/initLocalState.ts @@ -1,6 +1,7 @@ import { Environment } from "relay-runtime"; import { clearHash, getParamsFromHash } from "coral-framework/helpers"; +import { AuthState, updateAccessToken } from "coral-framework/lib/auth"; import { CoralContext } from "coral-framework/lib/bootstrap"; import { initLocalBaseState } from "coral-framework/lib/relay"; @@ -9,7 +10,8 @@ import { initLocalBaseState } from "coral-framework/lib/relay"; */ export default async function initLocalState( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) { // Get all the parameters from the hash. const params = getParamsFromHash(); @@ -18,8 +20,8 @@ export default async function initLocalState( clearHash(); // Save the token in storage. - context.auth.set(params.accessToken); + auth = updateAccessToken(params.accessToken); } - initLocalBaseState(environment, context); + initLocalBaseState(environment, context, auth); } diff --git a/src/core/client/stream/local/initLocalState.spec.ts b/src/core/client/stream/local/initLocalState.spec.ts index d6bf27b0230..47f1b69feca 100644 --- a/src/core/client/stream/local/initLocalState.spec.ts +++ b/src/core/client/stream/local/initLocalState.spec.ts @@ -1,7 +1,6 @@ import { Environment, RecordSource } from "relay-runtime"; import { timeout } from "coral-common/utils"; -import Auth from "coral-framework/lib/auth"; import { CoralContext } from "coral-framework/lib/bootstrap"; import { LOCAL_ID } from "coral-framework/lib/relay"; import { createPromisifiedStorage } from "coral-framework/lib/storage"; @@ -25,7 +24,6 @@ beforeEach(() => { it("init local state", async () => { const context: Partial = { - auth: new Auth(), localStorage: createPromisifiedStorage(), }; await initLocalState(environment, context as any); @@ -35,7 +33,6 @@ it("init local state", async () => { it("set storyID from query", async () => { const context: Partial = { - auth: new Auth(), localStorage: createPromisifiedStorage(), }; const storyID = "story-id"; @@ -49,7 +46,6 @@ it("set storyID from query", async () => { it("set commentID from query", async () => { const context: Partial = { - auth: new Auth(), localStorage: createPromisifiedStorage(), }; const commentID = "comment-id"; diff --git a/src/core/client/stream/local/initLocalState.ts b/src/core/client/stream/local/initLocalState.ts index 7322f96493e..630f877f575 100644 --- a/src/core/client/stream/local/initLocalState.ts +++ b/src/core/client/stream/local/initLocalState.ts @@ -1,6 +1,7 @@ import { commitLocalUpdate, Environment } from "relay-runtime"; import { parseQuery } from "coral-common/utils"; +import { AuthState, updateAccessToken } from "coral-framework/lib/auth"; import { CoralContext } from "coral-framework/lib/bootstrap"; import { getExternalConfig } from "coral-framework/lib/externalConfig"; import { createAndRetain, initLocalBaseState } from "coral-framework/lib/relay"; @@ -13,14 +14,15 @@ import { AUTH_POPUP_ID, AUTH_POPUP_TYPE } from "./constants"; */ export default async function initLocalState( environment: Environment, - context: CoralContext + context: CoralContext, + auth?: AuthState ) { const config = await getExternalConfig(context.pym); if (config && config.accessToken) { - context.auth.set(config.accessToken); + auth = updateAccessToken(config.accessToken); } - initLocalBaseState(environment, context); + initLocalBaseState(environment, context, auth); const commentsOrderBy = (await context.localStorage.getItem(COMMENTS_ORDER_BY)) ||