mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 16:32:15 +08:00
[CORL-1048] Cookie Deprecation (#2944)
* feat: dropped cookie support due to ITP issues * feat: added improved accessToken handling * fix: linting * fix: removed variadic part of JWT * fix: bump long-settimeout version * review: removed management classes * fix: updated snaps * review: renamed based on review * review: removed guard clauses around errors surrounding auth
This commit is contained in:
Generated
+5
@@ -31743,6 +31743,11 @@
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"long-settimeout": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/long-settimeout/-/long-settimeout-1.0.1.tgz",
|
||||
"integrity": "sha512-+Riw1FKJ5Aotk2WatDq2U3HCZEUxKRAeC0TOEbj4mnR+m3zhleQ5BuWsKu+Vb6kHoDQqq+jLKIcCqOODC7CIyg=="
|
||||
},
|
||||
"longest-streak": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-1.0.0.tgz",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
await initLocalBaseState(environment, context);
|
||||
initLocalBaseState(environment, context, auth);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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\\",
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
@@ -11,11 +11,9 @@ import { initLocalBaseState, LOCAL_ID } from "coral-framework/lib/relay";
|
||||
*/
|
||||
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;
|
||||
@@ -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
|
||||
@@ -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)!;
|
||||
|
||||
@@ -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,
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./auth";
|
||||
@@ -9,11 +9,6 @@ import { v1 as uuid } from "uuid";
|
||||
|
||||
import { LanguageCode } from "coral-common/helpers/i18n";
|
||||
import { getBrowserInfo } from "coral-framework/lib/browserInfo";
|
||||
import {
|
||||
commitLocalUpdatePromisified,
|
||||
LOCAL_ID,
|
||||
setAccessTokenInLocalState,
|
||||
} from "coral-framework/lib/relay";
|
||||
import { RestClient } from "coral-framework/lib/rest";
|
||||
import {
|
||||
createLocalStorage,
|
||||
@@ -24,20 +19,28 @@ import {
|
||||
} from "coral-framework/lib/storage";
|
||||
import { ClickFarAwayRegister } from "coral-ui/components/ClickOutside";
|
||||
|
||||
import {
|
||||
AccessTokenProvider,
|
||||
AuthState,
|
||||
deleteAccessToken,
|
||||
retrieveAccessToken,
|
||||
storeAccessToken,
|
||||
} from "../auth";
|
||||
import { generateBundles, LocalesData } from "../i18n";
|
||||
import {
|
||||
createManagedSubscriptionClient,
|
||||
createNetwork,
|
||||
ManagedSubscriptionClient,
|
||||
TokenGetter,
|
||||
} from "../network";
|
||||
import { PostMessageService } from "../postMessage";
|
||||
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<void>;
|
||||
|
||||
interface CreateContextArguments {
|
||||
@@ -47,6 +50,9 @@ interface CreateContextArguments {
|
||||
/** Init will be called after the context has been created. */
|
||||
initLocalState?: InitLocalState;
|
||||
|
||||
/** Access token that should be used instead of what's currently in storage */
|
||||
accessToken?: string;
|
||||
|
||||
/** A pym child that interacts with the pym parent. */
|
||||
pym?: PymChild;
|
||||
|
||||
@@ -100,25 +106,31 @@ function areWeInIframe() {
|
||||
|
||||
function createRelayEnvironment(
|
||||
subscriptionClient: ManagedSubscriptionClient,
|
||||
clientID: string
|
||||
clientID: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
const source = new RecordSource();
|
||||
const tokenGetter: TokenGetter = () => {
|
||||
const localState = source.get(LOCAL_ID);
|
||||
if (localState) {
|
||||
return (localState.accessToken as string) || "";
|
||||
const accessTokenProvider: AccessTokenProvider = () => {
|
||||
const local = source.get(LOCAL_ID);
|
||||
if (!local) {
|
||||
return;
|
||||
}
|
||||
return "";
|
||||
|
||||
return local.accessToken as string | undefined;
|
||||
};
|
||||
const environment = new Environment({
|
||||
network: createNetwork(subscriptionClient, tokenGetter, clientID),
|
||||
network: createNetwork(subscriptionClient, clientID, accessTokenProvider),
|
||||
store: new Store(source),
|
||||
});
|
||||
return { environment, tokenGetter, subscriptionClient };
|
||||
|
||||
return { environment, accessTokenProvider };
|
||||
}
|
||||
|
||||
function createRestClient(tokenGetter: () => string, clientID: string) {
|
||||
return new RestClient("/api", tokenGetter, clientID);
|
||||
function createRestClient(
|
||||
clientID: string,
|
||||
accessTokenProvider: AccessTokenProvider
|
||||
) {
|
||||
return new RestClient("/api", clientID, accessTokenProvider);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,51 +160,43 @@ 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();
|
||||
|
||||
// Create a new context with a new Relay Environment.
|
||||
const {
|
||||
environment: newEnvironment,
|
||||
tokenGetter: newTokenGetter,
|
||||
} = createRelayEnvironment(subscriptionClient, clientID);
|
||||
// Parse the claims/token and update storage.
|
||||
const auth = nextAccessToken
|
||||
? storeAccessToken(nextAccessToken)
|
||||
: deleteAccessToken();
|
||||
|
||||
const newContext = {
|
||||
// Create the new environment.
|
||||
const { environment, accessTokenProvider } = createRelayEnvironment(
|
||||
subscriptionClient,
|
||||
clientID,
|
||||
auth?.accessToken
|
||||
);
|
||||
|
||||
// Create the new context.
|
||||
const newContext: CoralContext = {
|
||||
...this.state.context,
|
||||
relayEnvironment: newEnvironment,
|
||||
rest: createRestClient(newTokenGetter, clientID),
|
||||
relayEnvironment: environment,
|
||||
rest: createRestClient(clientID, accessTokenProvider),
|
||||
};
|
||||
|
||||
// Initialize local state.
|
||||
await initLocalState(newContext.relayEnvironment, newContext);
|
||||
await initLocalState(newContext.relayEnvironment, newContext, 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(newTokenGetter());
|
||||
|
||||
// Set next access token.
|
||||
if (nextAccessToken) {
|
||||
await commitLocalUpdatePromisified(newEnvironment, async (store) => {
|
||||
setAccessTokenInLocalState(nextAccessToken, store);
|
||||
});
|
||||
}
|
||||
// Update the subscription client access token.
|
||||
subscriptionClient.setAccessToken(accessTokenProvider());
|
||||
|
||||
// Propagate new context.
|
||||
this.setState(
|
||||
{
|
||||
context: newContext,
|
||||
},
|
||||
() => {
|
||||
// Resume subscriptions after context has changed.
|
||||
subscriptionClient.resume();
|
||||
}
|
||||
);
|
||||
this.setState({ context: newContext }, () => {
|
||||
// Resume subscriptions after context has changed.
|
||||
subscriptionClient.resume();
|
||||
});
|
||||
};
|
||||
|
||||
// This is called when the locale should change.
|
||||
@@ -300,8 +304,8 @@ export default async function createManaged({
|
||||
|
||||
const localeBundles = await generateBundles(locales, localesData);
|
||||
|
||||
const localStorage = resolveLocalStorage(pym);
|
||||
const sessionStorage = resolveSessionStorage(pym);
|
||||
// Get the access token from storage.
|
||||
const auth = retrieveAccessToken();
|
||||
|
||||
/** clientID is sent to the server with every request */
|
||||
const clientID = uuid();
|
||||
@@ -311,9 +315,10 @@ export default async function createManaged({
|
||||
clientID
|
||||
);
|
||||
|
||||
const { environment, tokenGetter } = createRelayEnvironment(
|
||||
const { environment, accessTokenProvider } = createRelayEnvironment(
|
||||
subscriptionClient,
|
||||
clientID
|
||||
clientID,
|
||||
auth?.accessToken
|
||||
);
|
||||
|
||||
// Assemble context.
|
||||
@@ -325,10 +330,10 @@ export default async function createManaged({
|
||||
pym,
|
||||
eventEmitter,
|
||||
registerClickFarAway,
|
||||
rest: createRestClient(tokenGetter, clientID),
|
||||
rest: createRestClient(clientID, accessTokenProvider),
|
||||
postMessage: new PostMessageService(),
|
||||
localStorage,
|
||||
sessionStorage,
|
||||
localStorage: resolveLocalStorage(pym),
|
||||
sessionStorage: resolveSessionStorage(pym),
|
||||
browserInfo: getBrowserInfo(),
|
||||
uuidGenerator: uuid,
|
||||
// Noop, this is later replaced by the
|
||||
@@ -340,13 +345,12 @@ export default async function createManaged({
|
||||
};
|
||||
|
||||
// Initialize local state.
|
||||
await initLocalState(context.relayEnvironment, context);
|
||||
await initLocalState(context.relayEnvironment, context, auth);
|
||||
|
||||
// Set current token for the websocket connection.
|
||||
// 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(tokenGetter());
|
||||
// ^ 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.
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
export interface JWT {
|
||||
header: {
|
||||
alg: string;
|
||||
typ: string;
|
||||
};
|
||||
payload: {
|
||||
[_: string]: any;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
iss?: string;
|
||||
sub?: string;
|
||||
jti?: string;
|
||||
};
|
||||
expired: boolean;
|
||||
}
|
||||
|
||||
export function parseJWT(token: string, skewTolerance = 300): JWT {
|
||||
const [headerBase64, payloadBase64] = token.split(".");
|
||||
if (!headerBase64 && !payloadBase64) {
|
||||
throw new Error("invalid jwt token");
|
||||
}
|
||||
const header = JSON.parse(atob(headerBase64));
|
||||
const payload = JSON.parse(atob(payloadBase64));
|
||||
return {
|
||||
header,
|
||||
payload,
|
||||
get expired() {
|
||||
return Date.now() / 1000 + skewTolerance >= payload.exp;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -26,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(
|
||||
@@ -47,7 +47,7 @@ export interface ManagedSubscriptionClient {
|
||||
/** 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;
|
||||
setAccessToken(accessToken?: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +63,7 @@ export default function createManagedSubscriptionClient(
|
||||
const requests: SubscriptionRequest[] = [];
|
||||
let subscriptionClient: SubscriptionClient | null = null;
|
||||
let paused = false;
|
||||
let accessToken = "";
|
||||
let accessToken: string | undefined;
|
||||
|
||||
const closeClient = () => {
|
||||
if (subscriptionClient) {
|
||||
@@ -143,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();
|
||||
}
|
||||
@@ -179,7 +179,7 @@ export default function createManagedSubscriptionClient(
|
||||
r.unsubscribe = null;
|
||||
}
|
||||
}
|
||||
// Close websocket conncetion.
|
||||
// Close websocket connection.
|
||||
closeClient();
|
||||
};
|
||||
|
||||
@@ -193,12 +193,8 @@ export default function createManagedSubscriptionClient(
|
||||
paused = false;
|
||||
};
|
||||
|
||||
const setAccessToken = (t: string) => {
|
||||
accessToken = t;
|
||||
if (!paused) {
|
||||
pause();
|
||||
resume();
|
||||
}
|
||||
const setAccessToken = (nextAccessToken?: string) => {
|
||||
accessToken = nextAccessToken;
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,12 +22,7 @@ export {
|
||||
commitMutationPromiseNormalized,
|
||||
} from "./commitMutationPromise";
|
||||
export { default as commitLocalUpdatePromisified } from "./commitLocalUpdatePromisified";
|
||||
export {
|
||||
initLocalBaseState,
|
||||
setAccessTokenInLocalState,
|
||||
LOCAL_ID,
|
||||
LOCAL_TYPE,
|
||||
} from "./localState";
|
||||
export { initLocalBaseState, LOCAL_ID, LOCAL_TYPE } from "./localState";
|
||||
export {
|
||||
fetchQuery,
|
||||
createFetch,
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
commitLocalUpdate,
|
||||
Environment,
|
||||
RecordSourceProxy,
|
||||
} from "relay-runtime";
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { parseJWT } from "coral-framework/lib/jwt";
|
||||
import { createAndRetain } from "coral-framework/lib/relay";
|
||||
|
||||
import { AuthState } from "../auth";
|
||||
import { CoralContext } from "../bootstrap";
|
||||
|
||||
/**
|
||||
* The Root Record of Client-Side Schema Extension must be of this type.
|
||||
*/
|
||||
@@ -18,48 +15,33 @@ export const LOCAL_TYPE = "Local";
|
||||
*/
|
||||
export const LOCAL_ID = "client:root.local";
|
||||
|
||||
export function setAccessTokenInLocalState(
|
||||
accessToken: string | null,
|
||||
source: RecordSourceProxy
|
||||
) {
|
||||
const localRecord = source.get(LOCAL_ID)!;
|
||||
localRecord.setValue(accessToken || "", "accessToken");
|
||||
if (accessToken) {
|
||||
const { payload } = parseJWT(accessToken);
|
||||
// TODO: (cvle) maybe a timer to detect when accessToken has expired?
|
||||
|
||||
// Set the exp if it's valid.
|
||||
if (typeof payload.exp === "number") {
|
||||
localRecord.setValue(payload.exp, "accessTokenExp");
|
||||
} else {
|
||||
localRecord.setValue(null, "accessTokenExp");
|
||||
}
|
||||
|
||||
// Set the jti if it's valid.
|
||||
if (typeof payload.jti === "string" && payload.jti.length > 0) {
|
||||
localRecord.setValue(payload.jti, "accessTokenJTI");
|
||||
} else {
|
||||
localRecord.setValue(null, "accessTokenJTI");
|
||||
}
|
||||
} else {
|
||||
localRecord.setValue(null, "accessTokenExp");
|
||||
localRecord.setValue(null, "accessTokenJTI");
|
||||
}
|
||||
}
|
||||
|
||||
export async function initLocalBaseState(
|
||||
/**
|
||||
* 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,
|
||||
{ localStorage }: CoralContext,
|
||||
accessToken?: string | null
|
||||
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);
|
||||
|
||||
// Set access token
|
||||
setAccessTokenInLocalState(accessToken || null, s);
|
||||
root.setLinkedRecord(local, "local");
|
||||
|
||||
// Update the access token properties.
|
||||
local.setValue(auth?.accessToken, "accessToken");
|
||||
|
||||
// Update the claims.
|
||||
local.setValue(auth?.claims.exp, "accessTokenExp");
|
||||
local.setValue(auth?.claims.jti, "accessTokenJTI");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { merge } from "lodash";
|
||||
import { CLIENT_ID_HEADER } from "coral-common/constants";
|
||||
import { Overwrite } from "coral-framework/types";
|
||||
|
||||
import { AccessTokenProvider } from "./auth";
|
||||
import { extractError } from "./network";
|
||||
|
||||
const buildOptions = (inputOptions: RequestInit = {}) => {
|
||||
@@ -53,13 +54,17 @@ type PartialRequestInit = Overwrite<Partial<RequestInit>, { body?: any }> & {
|
||||
|
||||
export class RestClient {
|
||||
public readonly uri: string;
|
||||
private tokenGetter?: () => string;
|
||||
private clientID?: string;
|
||||
private accessTokenProvider?: AccessTokenProvider;
|
||||
|
||||
constructor(uri: string, tokenGetter?: () => string, clientID?: string) {
|
||||
constructor(
|
||||
uri: string,
|
||||
clientID?: string,
|
||||
accessTokenProvider?: AccessTokenProvider
|
||||
) {
|
||||
this.uri = uri;
|
||||
this.tokenGetter = tokenGetter;
|
||||
this.clientID = clientID;
|
||||
this.accessTokenProvider = accessTokenProvider;
|
||||
}
|
||||
|
||||
public async fetch<T = {}>(
|
||||
@@ -67,7 +72,8 @@ export class RestClient {
|
||||
options: PartialRequestInit
|
||||
): Promise<T> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
export default function createAccessToken(payload = {}) {
|
||||
return `${btoa(
|
||||
JSON.stringify({
|
||||
alg: "HS256",
|
||||
typ: "JWT",
|
||||
})
|
||||
)}.${btoa(
|
||||
JSON.stringify({
|
||||
jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce",
|
||||
...payload,
|
||||
})
|
||||
)}`;
|
||||
const TOKEN_JTI = "31b26591-4e9a-4388-a7ff-e1bdc5d97cce";
|
||||
|
||||
function encodePart(obj: object): string {
|
||||
return btoa(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export default function createAccessToken(payload = {}) {
|
||||
return [
|
||||
{ kid: "96c8066a-d987-4282-83f9-da516797f9fc", alg: "HS256" },
|
||||
{ jti: TOKEN_JTI, ...payload },
|
||||
null,
|
||||
]
|
||||
.map((obj) => (obj ? encodePart(obj) : obj))
|
||||
.join(".");
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ export default function createTestRenderer<
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
TestRenderer.act(() => {
|
||||
testRenderer = TestRenderer.create(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const INSTALL_ACCESS_TOKEN_KEY = "coral:install:accessToken";
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import { clearHash, getParamsFromHash } from "coral-framework/helpers";
|
||||
import { AuthState, storeAccessToken } from "coral-framework/lib/auth";
|
||||
import { CoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { initLocalBaseState } from "coral-framework/lib/relay";
|
||||
|
||||
import { INSTALL_ACCESS_TOKEN_KEY } from "../constants";
|
||||
|
||||
/**
|
||||
* 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(
|
||||
INSTALL_ACCESS_TOKEN_KEY
|
||||
);
|
||||
|
||||
// Get all the parameters from the hash.
|
||||
const params = getParamsFromHash();
|
||||
if (params && params.accessToken) {
|
||||
// As there's an access token in the hash, let's clear it.
|
||||
clearHash();
|
||||
|
||||
// Save the token in session storage to override what we found.
|
||||
accessToken = params.accessToken;
|
||||
await context.sessionStorage.setItem(INSTALL_ACCESS_TOKEN_KEY, accessToken);
|
||||
// Save the token in storage.
|
||||
auth = storeAccessToken(params.accessToken);
|
||||
}
|
||||
|
||||
await initLocalBaseState(environment, context, accessToken);
|
||||
initLocalBaseState(environment, context, auth);
|
||||
}
|
||||
|
||||
@@ -12,9 +12,6 @@ exports[`init local state 1`] = `
|
||||
\\"client:root.local\\": {
|
||||
\\"__id\\": \\"client:root.local\\",
|
||||
\\"__typename\\": \\"Local\\",
|
||||
\\"accessToken\\": \\"\\",
|
||||
\\"accessTokenExp\\": null,
|
||||
\\"accessTokenJTI\\": null,
|
||||
\\"commentsOrderBy\\": \\"CREATED_AT_DESC\\",
|
||||
\\"authPopup\\": {
|
||||
\\"__ref\\": \\"client:root.local.authPopup\\"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { commitLocalUpdate, Environment } from "relay-runtime";
|
||||
|
||||
import { parseQuery } from "coral-common/utils";
|
||||
import { AuthState, storeAccessToken } 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);
|
||||
await initLocalBaseState(
|
||||
environment,
|
||||
context,
|
||||
config ? config.accessToken : undefined
|
||||
);
|
||||
if (config && config.accessToken) {
|
||||
auth = storeAccessToken(config.accessToken);
|
||||
}
|
||||
|
||||
initLocalBaseState(environment, context, auth);
|
||||
|
||||
const commentsOrderBy =
|
||||
(await context.localStorage.getItem(COMMENTS_ORDER_BY)) ||
|
||||
@@ -44,6 +46,7 @@ export default async function initLocalState(
|
||||
if (query.commentID) {
|
||||
localRecord.setValue(query.commentID, "commentID");
|
||||
}
|
||||
|
||||
// Set sort
|
||||
localRecord.setValue(commentsOrderBy, "commentsOrderBy");
|
||||
|
||||
@@ -63,7 +66,7 @@ export default async function initLocalState(
|
||||
localRecord.setValue("COMMENTS", "activeTab");
|
||||
localRecord.setValue("MY_COMMENTS", "profileTab");
|
||||
|
||||
// Initilzie the comments tab to NONE for now, it will be initialized to an
|
||||
// Initialize the comments tab to NONE for now, it will be initialized to an
|
||||
// actual tab when we find out how many feature comments there are.
|
||||
localRecord.setValue("NONE", "commentsTab");
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { setLongTimeout } from "long-settimeout";
|
||||
|
||||
/** A promisified timeout. */
|
||||
export default function timeout(ms = 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
return new Promise((resolve) => setLongTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Joi from "@hapi/joi";
|
||||
import { CookieOptions, NextFunction, RequestHandler, Response } from "express";
|
||||
import { NextFunction, RequestHandler, Response } from "express";
|
||||
import { Redis } from "ioredis";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { DateTime } from "luxon";
|
||||
import passport, { Authenticator } from "passport";
|
||||
|
||||
import { stringifyQuery } from "coral-common/utils";
|
||||
@@ -16,7 +15,6 @@ import { validate } from "coral-server/app/request/body";
|
||||
import { AuthenticationError } from "coral-server/errors";
|
||||
import { User } from "coral-server/models/user";
|
||||
import {
|
||||
COOKIE_NAME,
|
||||
extractTokenFromRequest,
|
||||
JWTSigningConfig,
|
||||
revokeJWT,
|
||||
@@ -95,8 +93,9 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) {
|
||||
await revokeJWT(redis, jti, exp, now);
|
||||
}
|
||||
|
||||
// Clear the cookie.
|
||||
res.clearCookie(COOKIE_NAME, generateCookieOptions(req, new Date(0)));
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// // Clear the cookie.
|
||||
// res.clearCookie(COOKIE_NAME, generateCookieOptions(req, new Date(0)));
|
||||
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
@@ -115,11 +114,6 @@ export async function handleSuccessfulLogin(
|
||||
// Tenant is guaranteed at this point.
|
||||
const tenant = coral.tenant!;
|
||||
|
||||
// Compute the expiry date.
|
||||
const expiresIn = DateTime.fromJSDate(coral.now).plus({
|
||||
seconds: tenant.auth.sessionDuration,
|
||||
});
|
||||
|
||||
// Grab the token.
|
||||
const token = await signTokenString(
|
||||
signingConfig,
|
||||
@@ -133,11 +127,18 @@ export async function handleSuccessfulLogin(
|
||||
res.header("Cache-Control", "private, no-cache, no-store, must-revalidate");
|
||||
res.header("Expires", "-1");
|
||||
res.header("Pragma", "no-cache");
|
||||
res.cookie(
|
||||
COOKIE_NAME,
|
||||
token,
|
||||
generateCookieOptions(req, expiresIn.toJSDate())
|
||||
);
|
||||
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// // Compute the expiry date.
|
||||
// const expiresIn = DateTime.fromJSDate(coral.now).plus({
|
||||
// seconds: tenant.auth.sessionDuration,
|
||||
// });
|
||||
//
|
||||
// res.cookie(
|
||||
// COOKIE_NAME,
|
||||
// token,
|
||||
// generateCookieOptions(req, expiresIn.toJSDate())
|
||||
// );
|
||||
|
||||
// Send back the details!
|
||||
res.json({ token });
|
||||
@@ -146,20 +147,21 @@ export async function handleSuccessfulLogin(
|
||||
}
|
||||
}
|
||||
|
||||
const generateCookieOptions = (
|
||||
req: Request,
|
||||
expiresIn: Date
|
||||
): CookieOptions => ({
|
||||
path: "/api",
|
||||
httpOnly: true,
|
||||
secure: req.secure,
|
||||
// Chrome will ignore `SameSite: None` when not used in a secure context
|
||||
// anyways, so don't bother setting `None` when we're not secure. The only
|
||||
// time we aren't behind HTTPS is when we're testing/in development where the
|
||||
// the setting for `SameSite: Lax` would be OK.
|
||||
sameSite: req.secure ? "none" : "lax",
|
||||
expires: expiresIn,
|
||||
});
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// const generateCookieOptions = (
|
||||
// req: Request,
|
||||
// expiresIn: Date
|
||||
// ): CookieOptions => ({
|
||||
// path: "/api",
|
||||
// httpOnly: true,
|
||||
// secure: req.secure,
|
||||
// // Chrome will ignore `SameSite: None` when not used in a secure context
|
||||
// // anyways, so don't bother setting `None` when we're not secure. The only
|
||||
// // time we aren't behind HTTPS is when we're testing/in development where the
|
||||
// // the setting for `SameSite: Lax` would be OK.
|
||||
// sameSite: req.secure ? "none" : "lax",
|
||||
// expires: expiresIn,
|
||||
// });
|
||||
|
||||
function redirectWithHash(
|
||||
res: Response,
|
||||
@@ -191,11 +193,6 @@ export async function handleOAuth2Callback(
|
||||
const coral = req.coral!;
|
||||
const tenant = coral.tenant!;
|
||||
|
||||
// Compute the expiry date.
|
||||
const expiresIn = DateTime.fromJSDate(coral.now).plus({
|
||||
seconds: tenant.auth.sessionDuration,
|
||||
});
|
||||
|
||||
// Grab the token.
|
||||
const accessToken = await signTokenString(
|
||||
signingConfig,
|
||||
@@ -204,11 +201,18 @@ export async function handleOAuth2Callback(
|
||||
{},
|
||||
coral.now
|
||||
);
|
||||
res.cookie(
|
||||
COOKIE_NAME,
|
||||
accessToken,
|
||||
generateCookieOptions(req, expiresIn.toJSDate())
|
||||
);
|
||||
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// // Compute the expiry date.
|
||||
// const expiresIn = DateTime.fromJSDate(coral.now).plus({
|
||||
// seconds: tenant.auth.sessionDuration,
|
||||
// });
|
||||
//
|
||||
// res.cookie(
|
||||
// COOKIE_NAME,
|
||||
// accessToken,
|
||||
// generateCookieOptions(req, expiresIn.toJSDate())
|
||||
// );
|
||||
|
||||
// Send back the details!
|
||||
return redirectWithHash(res, path, { accessToken });
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import cookies from "cookie-parser";
|
||||
import express, { Router } from "express";
|
||||
|
||||
import { LanguageCode } from "coral-common/helpers/i18n/locales";
|
||||
@@ -16,7 +15,10 @@ export function createRouter(app: AppOptions, options: RouterOptions) {
|
||||
const router = express.Router();
|
||||
|
||||
// Attach the API router.
|
||||
router.use("/api", cookies(), createAPIRouter(app, options));
|
||||
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// router.use("/api", cookies(), createAPIRouter(app, options));
|
||||
router.use("/api", createAPIRouter(app, options));
|
||||
|
||||
// Attach the GraphiQL if enabled.
|
||||
if (app.config.get("enable_graphiql")) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Joi from "@hapi/joi";
|
||||
import cookie from "cookie";
|
||||
import { IncomingMessage } from "http";
|
||||
import { Redis } from "ioredis";
|
||||
import jwt, { KeyFunction, SignOptions, VerifyOptions } from "jsonwebtoken";
|
||||
@@ -293,10 +292,11 @@ export async function signString<T extends {}>(
|
||||
return jwt.sign(payload, secret, { ...options, algorithm });
|
||||
}
|
||||
|
||||
/**
|
||||
* COOKIE_NAME is the name of the authorization cookie used by Coral.
|
||||
*/
|
||||
export const COOKIE_NAME = "authorization";
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// /**
|
||||
// * COOKIE_NAME is the name of the authorization cookie used by Coral.
|
||||
// */
|
||||
// export const COOKIE_NAME = "authorization";
|
||||
|
||||
/**
|
||||
* isExpressRequest will check to see if this is a Request or an
|
||||
@@ -315,31 +315,32 @@ export function isExpressRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* extractJWTFromRequestCookie will parse the cookies off of the request if it
|
||||
* can.
|
||||
*
|
||||
* @param req the incoming request possibly containing a cookie
|
||||
*/
|
||||
function extractJWTFromRequestCookie(
|
||||
req: Request | IncomingMessage
|
||||
): string | null {
|
||||
if (!isExpressRequest(req)) {
|
||||
// Grab the cookie header.
|
||||
const header = req.headers.cookie;
|
||||
if (typeof header !== "string" || header.length === 0) {
|
||||
return null;
|
||||
}
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// /**
|
||||
// * extractJWTFromRequestCookie will parse the cookies off of the request if it
|
||||
// * can.
|
||||
// *
|
||||
// * @param req the incoming request possibly containing a cookie
|
||||
// */
|
||||
// function extractJWTFromRequestCookie(
|
||||
// req: Request | IncomingMessage
|
||||
// ): string | null {
|
||||
// if (!isExpressRequest(req)) {
|
||||
// // Grab the cookie header.
|
||||
// const header = req.headers.cookie;
|
||||
// if (typeof header !== "string" || header.length === 0) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// Parse the cookies from that header.
|
||||
const cookies = cookie.parse(header);
|
||||
return cookies[COOKIE_NAME] || null;
|
||||
}
|
||||
// // Parse the cookies from that header.
|
||||
// const cookies = cookie.parse(header);
|
||||
// return cookies[COOKIE_NAME] || null;
|
||||
// }
|
||||
|
||||
return req.cookies && req.cookies[COOKIE_NAME]
|
||||
? req.cookies[COOKIE_NAME]
|
||||
: null;
|
||||
}
|
||||
// return req.cookies && req.cookies[COOKIE_NAME]
|
||||
// ? req.cookies[COOKIE_NAME]
|
||||
// : null;
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -365,7 +366,7 @@ function extractJWTFromRequestHeaders(
|
||||
|
||||
/**
|
||||
* extractJWTFromRequest will extract the token from the request if it can find
|
||||
* it. It first tries to get the token from the headers, then from the cookie.
|
||||
* it. It will try to extract the token from the headers.
|
||||
*
|
||||
* @param req the request to extract the JWT from
|
||||
* @param excludeQuery when true, does not pull from the query params
|
||||
@@ -374,10 +375,9 @@ export function extractTokenFromRequest(
|
||||
req: Request | IncomingMessage,
|
||||
excludeQuery = false
|
||||
): string | null {
|
||||
return (
|
||||
extractJWTFromRequestHeaders(req, excludeQuery) ||
|
||||
extractJWTFromRequestCookie(req)
|
||||
);
|
||||
// NOTE: disabled cookie support due to ITP/First Party Cookie bugs
|
||||
// return extractJWTFromRequestHeaders(req, excludeQuery)|| extractJWTFromRequestCookie(req)
|
||||
return extractJWTFromRequestHeaders(req, excludeQuery);
|
||||
}
|
||||
|
||||
function generateJTIRevokedKey(jti: string) {
|
||||
|
||||
Reference in New Issue
Block a user