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-1048] Cookie Deprecation #2944

Merged
merged 10 commits into from
May 13, 2020
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
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"jwks-rsa": "^1.7.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.15",
"long-settimeout": "^1.0.1",
"lru-cache": "^5.1.1",
"luxon": "^1.22.2",
"metascraper-author": "^5.11.6",
Expand Down
6 changes: 4 additions & 2 deletions src/core/client/account/local/initLocalState.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -8,7 +9,8 @@ import { initLocalBaseState } from "coral-framework/lib/relay";
*/
export default async function initLocalState(
environment: Environment,
context: CoralContext
context: CoralContext,
auth?: AuthState
) {
await initLocalBaseState(environment, context);
initLocalBaseState(environment, context, auth);
}
1 change: 0 additions & 1 deletion src/core/client/admin/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const REDIRECT_PATH_KEY = "coral:adminRedirectPath";
export const ACCESS_TOKEN_KEY = "coral:accessToken";
export const HOTKEYS = {
NEXT: "j",
PREV: "k",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ exports[`get access token from url 1`] = `
"{
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"accessToken\\": \\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==\\",
\\"accessTokenExp\\": null,
\\"accessToken\\": \\"eyJraWQiOiI5NmM4MDY2YS1kOTg3LTQyODItODNmOS1kYTUxNjc5N2Y5ZmMiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==.\\",
\\"accessTokenJTI\\": \\"31b26591-4e9a-4388-a7ff-e1bdc5d97cce\\",
\\"redirectPath\\": null,
\\"authView\\": \\"SIGN_IN\\",
Expand All @@ -25,9 +24,6 @@ exports[`init local state 1`] = `
\\"client:root.local\\": {
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"accessToken\\": \\"\\",
\\"accessTokenExp\\": null,
\\"accessTokenJTI\\": null,
\\"redirectPath\\": null,
\\"authView\\": \\"SIGN_IN\\",
\\"authError\\": null
Expand Down
30 changes: 7 additions & 23 deletions src/core/client/admin/local/initLocalState.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { commitLocalUpdate, Environment } from "relay-runtime";

import { ACCESS_TOKEN_KEY, REDIRECT_PATH_KEY } from "coral-admin/constants";
import { REDIRECT_PATH_KEY } from "coral-admin/constants";
import { clearHash, getParamsFromHash } from "coral-framework/helpers";
import { AuthState, storeAccessToken } from "coral-framework/lib/auth";
import { CoralContext } from "coral-framework/lib/bootstrap";
import { parseJWT } from "coral-framework/lib/jwt";
import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay";

/**
* Initializes the local state, before we start the App.
*/
export default async function initLocalState(
environment: Environment,
context: CoralContext
context: CoralContext,
auth?: AuthState
) {
// Get the access token from the session storage.
let accessToken = await context.sessionStorage.getItem(ACCESS_TOKEN_KEY);

// Initialize the redirect path in case we don't need to redirect somewhere.
let redirectPath: string | null = null;
let error: string | null = null;
Expand All @@ -31,11 +29,9 @@ export default async function initLocalState(
error = params.error;
}

// If there was an access token, store it and replace the one that was in
// the session storage before.
// If there was an access token, store it.
if (params.accessToken) {
accessToken = params.accessToken;
await context.sessionStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
auth = storeAccessToken(params.accessToken);
}

// As we are in the middle of an auth flow (given that there was something
Expand All @@ -48,19 +44,7 @@ export default async function initLocalState(
await context.localStorage.setItem(REDIRECT_PATH_KEY, "");
}

if (accessToken) {
// As there's a token on the request, decode it, and check to see if it's
// expired already. If it is, this will send them back to the error page.
const { payload } = parseJWT(accessToken);
if (payload && payload.exp) {
if (payload.exp - Date.now() / 1000 <= 0) {
accessToken = null;
await context.sessionStorage.removeItem(ACCESS_TOKEN_KEY);
}
}
}

await initLocalBaseState(environment, context, accessToken);
initLocalBaseState(environment, context, auth);

commitLocalUpdate(environment, (s) => {
const localRecord = s.get(LOCAL_ID)!;
Expand Down
11 changes: 9 additions & 2 deletions src/core/client/admin/routes/Invite/InviteCompleteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { FORM_ERROR } from "final-form";
import React, { useCallback, useMemo } from "react";
import { Form } from "react-final-form";

import { parseAccessTokenClaims } from "coral-framework/lib/auth/helpers";
import { InvalidRequestError } from "coral-framework/lib/errors";
import { parseJWT } from "coral-framework/lib/jwt";
import { useMutation } from "coral-framework/lib/relay";
import {
Button,
Expand Down Expand Up @@ -52,7 +52,14 @@ const InviteCompleteForm: React.FunctionComponent<Props> = ({
},
[token]
);
const email = useMemo(() => parseJWT(token).payload.email, [token]);
const email = useMemo(() => {
const claims = parseAccessTokenClaims<{ email?: string }>(token);
if (!claims) {
return null;
}

return claims.email;
}, [token]);

return (
<div data-testid="invite-complete-form">
Expand Down
11 changes: 9 additions & 2 deletions src/core/client/admin/routes/Invite/Success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Localized } from "@fluent/react/compat";
import { Link } from "found";
import React, { useMemo } from "react";

import { parseAccessTokenClaims } from "coral-framework/lib/auth/helpers";
import { ExternalLink } from "coral-framework/lib/i18n/components";
import { parseJWT } from "coral-framework/lib/jwt";
import { HorizontalGutter, Typography } from "coral-ui/components";

import styles from "./Success.css";
Expand All @@ -19,7 +19,14 @@ const Success: React.FunctionComponent<Props> = ({
organizationName,
organizationURL,
}) => {
const email = useMemo(() => parseJWT(token).payload.email, [token]);
const email = useMemo(() => {
const claims = parseAccessTokenClaims<{ email?: string }>(token);
if (!claims) {
return null;
}

return claims.email;
}, [token]);

return (
<HorizontalGutter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ exports[`get access token from url 1`] = `
"{
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"accessToken\\": \\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==\\",
\\"accessTokenExp\\": null,
\\"accessToken\\": \\"eyJraWQiOiI5NmM4MDY2YS1kOTg3LTQyODItODNmOS1kYTUxNjc5N2Y5ZmMiLCJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==.\\",
\\"accessTokenJTI\\": \\"31b26591-4e9a-4388-a7ff-e1bdc5d97cce\\",
\\"view\\": \\"SIGN_IN\\",
\\"error\\": null
Expand All @@ -24,9 +23,6 @@ exports[`init local state 1`] = `
\\"client:root.local\\": {
\\"__id\\": \\"client:root.local\\",
\\"__typename\\": \\"Local\\",
\\"accessToken\\": \\"\\",
\\"accessTokenExp\\": null,
\\"accessTokenJTI\\": null,
\\"view\\": \\"SIGN_IN\\",
\\"error\\": null
}
Expand Down
18 changes: 10 additions & 8 deletions src/core/client/auth/local/initLocalState.ts
Original file line number Diff line number Diff line change
@@ -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, storeAccessToken } from "coral-framework/lib/auth";
import { CoralContext } from "coral-framework/lib/bootstrap";
import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay";

Expand All @@ -11,16 +11,18 @@ 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();
const { error = null, accessToken = null } = getParamsFromHashAndClearIt();

await initLocalBaseState(environment, context, accessToken);
if (accessToken) {
auth = storeAccessToken(accessToken);
}

commitLocalUpdate(environment, s => {
initLocalBaseState(environment, context, auth);

commitLocalUpdate(environment, (s) => {
const localRecord = s.get(LOCAL_ID)!;

// Parse query params
Expand Down
90 changes: 90 additions & 0 deletions src/core/client/framework/lib/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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) {
// TODO: (wyattjoh) add error reporting around this error
// eslint-disable-next-line no-console
console.error("could not get access token from storage", err);

return;
}
}

export function storeAccessToken(accessToken: string) {
try {
// Update the access token in storage.
storage.setItem(ACCESS_TOKEN_KEY, accessToken);
} catch (err) {
// TODO: (wyattjoh) add error reporting around this error
// 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) {
// TODO: (wyattjoh) add error reporting around this error
// eslint-disable-next-line no-console
console.error("could not remove access token from storage", err);
}

return undefined;
}
56 changes: 56 additions & 0 deletions src/core/client/framework/lib/auth/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const SKEW_TOLERANCE = 300;

export interface Claims {
jti?: string;
exp?: number;
}

export function parseAccessTokenClaims<T = {}>(
accessToken: string
): (Claims & T) | null {
const parts = accessToken.split(".");
if (parts.length !== 3) {
// TODO: (wyattjoh) add error reporting around this error
// eslint-disable-next-line no-console
console.warn("access token does not have the right number of parts");

return null;
}

try {
const claims = JSON.parse(atob(parts[1]));

// Validate `jti` claim.
if (!claims.jti || typeof claims.jti !== "string") {
delete claims.jti;
}

// Validate `exp` claim.
if (!claims.exp || typeof claims.exp !== "number") {
delete claims.exp;
}

return claims;
} catch (err) {
// TODO: (wyattjoh) add error reporting around this error
// eslint-disable-next-line no-console
console.error("access token can not be parsed:", err);

return null;
}
}

/**
* computeExpiresIn will return null if we are already expired, or the time in
* milliseconds from now that we are expired.
*
* @param expiredAt the epoch timestamp that we're considered expired
*/
export function computeExpiresIn(expiredAt: number) {
const expiresIn = expiredAt * 1000 - Date.now();
if (expiresIn + SKEW_TOLERANCE <= 0) {
return null;
}

return expiresIn;
}
1 change: 1 addition & 0 deletions src/core/client/framework/lib/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./auth";
Loading