[next] Cookie Support (#2339)

* feat: added cookie support to coral

* feat: adapt client to use cookies

* fix: safari input styles

* fix: lint

* fix: linting

* fix: support clearing cookies properly, oauth

* feat: support cookies for websocket upgrade requests

* fix: lint

* fix: tests
This commit is contained in:
Wyatt Johnson
2019-06-25 21:48:58 +02:00
committed by Vinh
parent 3576b6a943
commit e72b15c505
79 changed files with 375 additions and 278 deletions
@@ -62,7 +62,7 @@ export interface CoralContext {
eventEmitter: EventEmitter2;
/** Clear session data. */
clearSession: () => Promise<void>;
clearSession: (nextAccessToken?: string | null) => Promise<void>;
/** Controls router transitions (for tests) */
transitionControl?: TransitionControlData;
@@ -8,7 +8,11 @@ import { Environment, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v1";
import { getBrowserInfo } from "coral-framework/lib/browserInfo";
import { LOCAL_ID } from "coral-framework/lib/relay";
import {
commitLocalUpdatePromisified,
LOCAL_ID,
setAccessTokenInLocalState,
} from "coral-framework/lib/relay";
import {
createLocalStorage,
createPromisifiedStorage,
@@ -105,7 +109,7 @@ function createRelayEnvironment(
const tokenGetter: TokenGetter = () => {
const localState = source.get(LOCAL_ID);
if (localState) {
return (localState.loggedIn && localState.accessToken) || "";
return localState.accessToken || "";
}
return "";
};
@@ -145,7 +149,7 @@ function createMangedCoralContextProvider(
}
// This is called every time a user session starts or ends.
private clearSession = async () => {
private clearSession = async (nextAccessToken?: string | null) => {
// Clear session storage.
this.state.context.sessionStorage.clear();
@@ -173,6 +177,13 @@ function createMangedCoralContextProvider(
// a new token.
subscriptionClient.setAccessToken(newTokenGetter());
// Set next access token.
if (nextAccessToken) {
await commitLocalUpdatePromisified(newEnvironment, async store => {
setAccessTokenInLocalState(nextAccessToken, store);
});
}
// Propagate new context.
this.setState(
{
@@ -292,7 +303,7 @@ export default async function createManaged({
uuidGenerator: uuid,
// Noop, this is later replaced by the
// managed CoralContextProvider.
clearSession: () => Promise.resolve(),
clearSession: (nextAccessToken?: string | null) => Promise.resolve(),
};
// Initialize local state.
@@ -25,14 +25,13 @@ export function setAccessTokenInLocalState(
const localRecord = source.get(LOCAL_ID)!;
localRecord.setValue(accessToken || "", "accessToken");
if (accessToken) {
const { payload, expired } = parseJWT(accessToken);
const { payload } = parseJWT(accessToken);
localRecord.setValue(payload.exp, "accessTokenExp");
localRecord.setValue(payload.jti, "accessTokenJTI");
localRecord.setValue(!expired, "loggedIn");
// TODO: (cvle) maybe a timer to detect when accessToken has expired?
} else {
localRecord.setValue(null, "accessTokenExp");
localRecord.setValue(null, "accessTokenJTI");
localRecord.setValue(false, "loggedIn");
}
}
@@ -41,10 +40,6 @@ export async function initLocalBaseState(
{ localStorage }: CoralContext,
accessToken?: string | null
) {
if (accessToken === undefined) {
accessToken = await localStorage!.getItem("accessToken");
}
commitLocalUpdate(environment, s => {
const root = s.getRoot();
@@ -52,7 +47,7 @@ export async function initLocalBaseState(
const localRecord = createAndRetain(environment, s, LOCAL_ID, LOCAL_TYPE);
root.setLinkedRecord(localRecord, "local");
// Set auth token
// Set access token
setAccessTokenInLocalState(accessToken || null, s);
});
}
@@ -1,59 +0,0 @@
import { Environment, RecordSource } from "relay-runtime";
import sinon from "sinon";
import { CoralContext } from "coral-framework/lib/bootstrap";
import { LOCAL_ID } from "coral-framework/lib/relay";
import { createPromisifiedStorage } from "coral-framework/lib/storage";
import {
createAccessToken,
createRelayEnvironment,
} from "coral-framework/testHelpers";
import SetAccessTokenMutation from "./SetAccessTokenMutation";
let environment: Environment;
const source: RecordSource = new RecordSource();
beforeAll(() => {
environment = createRelayEnvironment({
source,
});
});
const accessToken = createAccessToken();
it("Sets auth token to localStorage", async () => {
const clearSessionStub = sinon.stub();
const context: Partial<CoralContext> = {
localStorage: createPromisifiedStorage(),
clearSession: clearSessionStub,
};
await SetAccessTokenMutation.commit(
environment,
{ accessToken },
context as any
);
expect(source.get(LOCAL_ID)!.accessToken).toEqual(accessToken);
await expect(context.localStorage!.getItem("accessToken")).resolves.toEqual(
accessToken
);
expect(clearSessionStub.calledOnce).toBe(true);
});
it("Removes auth token from localStorage", async () => {
const clearSessionStub = sinon.stub();
const context: Partial<CoralContext> = {
localStorage: createPromisifiedStorage(),
clearSession: clearSessionStub,
};
localStorage.setItem("accessToken", accessToken);
await SetAccessTokenMutation.commit(
environment,
{ accessToken: null },
context as any
);
await expect(
context.localStorage!.getItem("accessToken")
).resolves.toBeNull();
expect(clearSessionStub.calledOnce).toBe(true);
});
@@ -1,11 +1,7 @@
import { Environment } from "relay-runtime";
import { CoralContext } from "coral-framework/lib/bootstrap";
import {
commitLocalUpdatePromisified,
createMutation,
setAccessTokenInLocalState,
} from "coral-framework/lib/relay";
import { createMutation } from "coral-framework/lib/relay";
interface SetAccessTokenInput {
accessToken: string | null;
@@ -16,18 +12,10 @@ const SetAccessTokenMutation = createMutation(
async (
environment: Environment,
input: SetAccessTokenInput,
{ localStorage, clearSession }: CoralContext
{ clearSession }: CoralContext
) => {
await commitLocalUpdatePromisified(environment, async store => {
setAccessTokenInLocalState(input.accessToken, store);
if (input.accessToken) {
await localStorage.setItem("accessToken", input.accessToken);
} else {
await localStorage.removeItem("accessToken");
}
// Clear current session, as we are starting a new one.
await clearSession();
});
// Clear current session, as we are starting a new one.
await clearSession(input.accessToken);
}
);
@@ -16,6 +16,7 @@ import { RestClient } from "coral-framework/lib/rest";
import { createPromisifiedStorage } from "coral-framework/lib/storage";
import { createUUIDGenerator } from "coral-framework/testHelpers";
import sinon from "sinon";
import createFluentBundle from "./createFluentBundle";
import createRelayEnvironment from "./createRelayEnvironment";
import createSubscriptionHandler, {
@@ -102,13 +103,12 @@ export default function createTestRenderer<
browserInfo: params.browserInfo || { ios: false },
uuidGenerator: createUUIDGenerator(),
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
clearSession: () => Promise.resolve(),
clearSession: sinon.stub(),
transitionControl: {
allowTransition: true,
history: [],
},
};
let testRenderer: ReactTestRenderer;
TestRenderer.act(() => {
testRenderer = TestRenderer.create(