diff --git a/package-lock.json b/package-lock.json index 40720e0a9..972240ac9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3574,6 +3574,21 @@ "integrity": "sha512-p+gNRe4RPjpl1lTBUomFJ42P8ymArH/P93DFJ0iY873BJ4ZmogcKc6TbHgZQmtQMsy3jxcAo0HcTjidXwo8uKg==", "dev": true }, + "@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==", + "dev": true + }, + "@types/cookie-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.1.tgz", + "integrity": "sha512-iJY6B3ZGufLiDf2OCAgiAAQuj1sMKC/wz/7XCEjZ+/MDuultfFJuSwrBKcLSmJ5iYApLzCCYBYJZs0Ws8GPmwA==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/cors": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.4.tgz", @@ -8174,9 +8189,25 @@ } }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } }, "cookie-signature": { "version": "1.0.6", @@ -10720,6 +10751,12 @@ "vary": "~1.1.2" }, "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "dev": true + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -12866,6 +12903,13 @@ "type-is": "~1.6.16", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } } }, "express-static-gzip": { diff --git a/package.json b/package.json index 1a8abcff6..212602f06 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,8 @@ "consolidate": "0.14.0", "content-security-policy-builder": "^2.0.0", "convict": "^4.3.1", + "cookie": "^0.4.0", + "cookie-parser": "^1.4.4", "cors": "^2.8.4", "dataloader": "^1.4.0", "dompurify": "^1.0.8", @@ -153,6 +155,8 @@ "@types/compression-webpack-plugin": "^2.0.0", "@types/consolidate": "0.0.34", "@types/convict": "^4.2.0", + "@types/cookie": "^0.3.3", + "@types/cookie-parser": "^1.4.1", "@types/cors": "^2.8.4", "@types/cross-spawn": "^6.0.0", "@types/dotenv": "^4.0.3", diff --git a/src/core/client/account/local/local.graphql b/src/core/client/account/local/local.graphql index 14fca266f..ef8602ee7 100644 --- a/src/core/client/account/local/local.graphql +++ b/src/core/client/account/local/local.graphql @@ -2,7 +2,6 @@ type Local { accessToken: String accessTokenExp: Int accessTokenJTI: String - loggedIn: Boolean! } extend type Query { diff --git a/src/core/client/admin/local/local.graphql b/src/core/client/admin/local/local.graphql index cfba71292..eff5f0146 100644 --- a/src/core/client/admin/local/local.graphql +++ b/src/core/client/admin/local/local.graphql @@ -28,7 +28,6 @@ type Local { accessToken: String accessTokenExp: Int accessTokenJTI: String - loggedIn: Boolean! redirectPath: String authView: View authError: String diff --git a/src/core/client/admin/routes/Login/views/SignIn/SignInMutation.ts b/src/core/client/admin/routes/Login/views/SignIn/SignInMutation.ts index e13505868..80a55c28c 100644 --- a/src/core/client/admin/routes/Login/views/SignIn/SignInMutation.ts +++ b/src/core/client/admin/routes/Login/views/SignIn/SignInMutation.ts @@ -12,13 +12,7 @@ const SignInMutation = createMutation( context: CoralContext ) => { const result = await signIn(context.rest, input); - - // Put the token on the hash and clean the session. - // It'll be picked up by initLocalState. - location.hash = `accessToken=${result.token}`; - await context.clearSession(); - // TODO: (cvle) A better way would be if `context.clearSession` would return the new session and - // we set the accessToken directly in there. + await context.clearSession(result.token); } ); diff --git a/src/core/client/admin/test/auth/__snapshots__/signInWithEmail.spec.tsx.snap b/src/core/client/admin/test/auth/__snapshots__/signInWithEmail.spec.tsx.snap index f9dc60369..936de9c28 100644 --- a/src/core/client/admin/test/auth/__snapshots__/signInWithEmail.spec.tsx.snap +++ b/src/core/client/admin/test/auth/__snapshots__/signInWithEmail.spec.tsx.snap @@ -764,4 +764,4 @@ exports[`shows server error 1`] = ` `; -exports[`submits form successfully 1`] = `"http://localhost/admin/login#accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ=="`; +exports[`submits form successfully 1`] = `"http://localhost/admin/login"`; diff --git a/src/core/client/admin/test/auth/accountCompletion.spec.tsx b/src/core/client/admin/test/auth/accountCompletion.spec.tsx index 3a7e96137..e90c346c6 100644 --- a/src/core/client/admin/test/auth/accountCompletion.spec.tsx +++ b/src/core/client/admin/test/auth/accountCompletion.spec.tsx @@ -2,7 +2,6 @@ import { pureMerge } from "coral-common/utils"; import { GQLResolver } from "coral-framework/schema"; import { act, - createAccessToken, createResolversStub, CreateTestRendererParams, replaceHistoryLocation, @@ -40,8 +39,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue("SIGN_IN", "authView"); - localRecord.setValue(true, "loggedIn"); - localRecord.setValue(createAccessToken(), "accessToken"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/auth/addEmailAddress.spec.tsx b/src/core/client/admin/test/auth/addEmailAddress.spec.tsx index 7dd0ab3ff..017dda358 100644 --- a/src/core/client/admin/test/auth/addEmailAddress.spec.tsx +++ b/src/core/client/admin/test/auth/addEmailAddress.spec.tsx @@ -1,7 +1,6 @@ import { pureMerge } from "coral-common/utils"; import { GQLResolver } from "coral-framework/schema"; import { - createAccessToken, createResolversStub, CreateTestRendererParams, replaceHistoryLocation, @@ -37,8 +36,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue("ADD_EMAIL_ADDRESS", "authView"); - localRecord.setValue(true, "loggedIn"); - localRecord.setValue(createAccessToken(), "accessToken"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/auth/createPassword.spec.tsx b/src/core/client/admin/test/auth/createPassword.spec.tsx index 5299c7bbc..cf55e2b93 100644 --- a/src/core/client/admin/test/auth/createPassword.spec.tsx +++ b/src/core/client/admin/test/auth/createPassword.spec.tsx @@ -1,7 +1,6 @@ import { pureMerge } from "coral-common/utils"; import { GQLResolver } from "coral-framework/schema"; import { - createAccessToken, createResolversStub, CreateTestRendererParams, replaceHistoryLocation, @@ -37,8 +36,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue("CREATE_PASSWORD", "authView"); - localRecord.setValue(true, "loggedIn"); - localRecord.setValue(createAccessToken(), "accessToken"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/auth/createUsername.spec.tsx b/src/core/client/admin/test/auth/createUsername.spec.tsx index f700edebc..078adbcc8 100644 --- a/src/core/client/admin/test/auth/createUsername.spec.tsx +++ b/src/core/client/admin/test/auth/createUsername.spec.tsx @@ -1,7 +1,6 @@ import { pureMerge } from "coral-common/utils"; import { GQLResolver } from "coral-framework/schema"; import { - createAccessToken, createResolversStub, CreateTestRendererParams, replaceHistoryLocation, @@ -31,8 +30,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue("CREATE_USERNAME", "authView"); - localRecord.setValue(true, "loggedIn"); - localRecord.setValue(createAccessToken(), "accessToken"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/auth/redirectLoggedIn.spec.tsx b/src/core/client/admin/test/auth/redirectLoggedIn.spec.tsx index baeacdfab..069e4a44e 100644 --- a/src/core/client/admin/test/auth/redirectLoggedIn.spec.tsx +++ b/src/core/client/admin/test/auth/redirectLoggedIn.spec.tsx @@ -4,7 +4,6 @@ import { } from "coral-framework/schema"; import { act, - createAccessToken, createQueryResolverStub, createResolversStub, CreateTestRendererParams, @@ -43,8 +42,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); - localRecord.setValue(createAccessToken(), "accessToken"); localRecord.setValue("SIGN_IN", "authView"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); diff --git a/src/core/client/admin/test/auth/redirectLoggedOut.spec.tsx b/src/core/client/admin/test/auth/redirectLoggedOut.spec.tsx index fd5c1f081..f33a9eef7 100644 --- a/src/core/client/admin/test/auth/redirectLoggedOut.spec.tsx +++ b/src/core/client/admin/test/auth/redirectLoggedOut.spec.tsx @@ -39,7 +39,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(false, "loggedIn"); localRecord.setValue("SIGN_IN", "authView"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); diff --git a/src/core/client/admin/test/auth/restricted.spec.tsx b/src/core/client/admin/test/auth/restricted.spec.tsx index c53e1789b..ed4dd4445 100644 --- a/src/core/client/admin/test/auth/restricted.spec.tsx +++ b/src/core/client/admin/test/auth/restricted.spec.tsx @@ -4,7 +4,6 @@ import { pureMerge } from "coral-common/utils"; import { LOCAL_ID, lookup } from "coral-framework/lib/relay"; import { GQLResolver, GQLUSER_ROLE } from "coral-framework/schema"; import { - createAccessToken, createResolversStub, CreateTestRendererParams, replaceHistoryLocation, @@ -41,8 +40,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); - localRecord.setValue(createAccessToken(), "accessToken"); localRecord.setValue("SIGN_IN", "authView"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); @@ -105,6 +102,5 @@ it("sign out when clicking on sign in as", async () => { "/admin/moderate/reported" ); }); - - expect(lookup(context.relayEnvironment, LOCAL_ID)!.loggedIn).toBeFalsy(); + restMock.verify(); }); diff --git a/src/core/client/admin/test/auth/signInWithEmail.spec.tsx b/src/core/client/admin/test/auth/signInWithEmail.spec.tsx index d063246c8..1ea04cb4e 100644 --- a/src/core/client/admin/test/auth/signInWithEmail.spec.tsx +++ b/src/core/client/admin/test/auth/signInWithEmail.spec.tsx @@ -41,7 +41,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(false, "loggedIn"); localRecord.setValue("SIGN_IN", "authView"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); diff --git a/src/core/client/admin/test/auth/signOut.spec.tsx b/src/core/client/admin/test/auth/signOut.spec.tsx index addd6dd41..afd1dd278 100644 --- a/src/core/client/admin/test/auth/signOut.spec.tsx +++ b/src/core/client/admin/test/auth/signOut.spec.tsx @@ -1,7 +1,6 @@ -import sinon from "sinon"; +import sinon, { SinonStub } from "sinon"; import { pureMerge } from "coral-common/utils"; -import { LOCAL_ID, lookup } from "coral-framework/lib/relay"; import { GQLResolver } from "coral-framework/schema"; import { createResolversStub, @@ -40,7 +39,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } @@ -77,6 +75,8 @@ it("logs out", async () => { signOutButton.props.onClick(); await wait(() => { - expect(lookup(context.relayEnvironment, LOCAL_ID)!.loggedIn).toBeFalsy(); + restMock.verify(); + // Wait for new session to start. + expect((context.clearSession as SinonStub).called).toBe(true); }); }); diff --git a/src/core/client/admin/test/community/community.spec.tsx b/src/core/client/admin/test/community/community.spec.tsx index 3e9773f51..50f0a8dbd 100644 --- a/src/core/client/admin/test/community/community.spec.tsx +++ b/src/core/client/admin/test/community/community.spec.tsx @@ -52,7 +52,6 @@ const createTestRenderer = async ( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/advanced.spec.tsx b/src/core/client/admin/test/configure/advanced.spec.tsx index fe18779b6..579d91db4 100644 --- a/src/core/client/admin/test/configure/advanced.spec.tsx +++ b/src/core/client/admin/test/configure/advanced.spec.tsx @@ -33,7 +33,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/auth.spec.tsx b/src/core/client/admin/test/configure/auth.spec.tsx index d6b4b4390..ccd8e5015 100644 --- a/src/core/client/admin/test/configure/auth.spec.tsx +++ b/src/core/client/admin/test/configure/auth.spec.tsx @@ -51,7 +51,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/general.spec.tsx b/src/core/client/admin/test/configure/general.spec.tsx index b6a08f2d6..c2ed85321 100644 --- a/src/core/client/admin/test/configure/general.spec.tsx +++ b/src/core/client/admin/test/configure/general.spec.tsx @@ -35,7 +35,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/moderation.spec.tsx b/src/core/client/admin/test/configure/moderation.spec.tsx index cf459dcaf..fe7e1ed92 100644 --- a/src/core/client/admin/test/configure/moderation.spec.tsx +++ b/src/core/client/admin/test/configure/moderation.spec.tsx @@ -33,7 +33,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/organization.spec.tsx b/src/core/client/admin/test/configure/organization.spec.tsx index a3c9e23e3..d132f7f5e 100644 --- a/src/core/client/admin/test/configure/organization.spec.tsx +++ b/src/core/client/admin/test/configure/organization.spec.tsx @@ -33,7 +33,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/permission.spec.tsx b/src/core/client/admin/test/configure/permission.spec.tsx index 3e2f6635e..adcab9c26 100644 --- a/src/core/client/admin/test/configure/permission.spec.tsx +++ b/src/core/client/admin/test/configure/permission.spec.tsx @@ -32,7 +32,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/configure/wordList.spec.tsx b/src/core/client/admin/test/configure/wordList.spec.tsx index ba7de1620..6a0a2f512 100644 --- a/src/core/client/admin/test/configure/wordList.spec.tsx +++ b/src/core/client/admin/test/configure/wordList.spec.tsx @@ -33,7 +33,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx b/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx index 202ec6cff..af6b7788e 100644 --- a/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx +++ b/src/core/client/admin/test/decisionHistory/decisionHistory.spec.tsx @@ -78,7 +78,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/admin/test/stories/stories.spec.tsx b/src/core/client/admin/test/stories/stories.spec.tsx index 824993ba4..5943c951b 100644 --- a/src/core/client/admin/test/stories/stories.spec.tsx +++ b/src/core/client/admin/test/stories/stories.spec.tsx @@ -53,7 +53,6 @@ async function createTestRenderer( params.resolvers ), initLocalState: (localRecord, source, environment) => { - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap b/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap index 84136b740..9561d9ec5 100644 --- a/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap +++ b/src/core/client/auth/local/__snapshots__/initLocalState.spec.ts.snap @@ -1,12 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`get auth token from url 1`] = ` +exports[`get access token from url 1`] = ` "{ \\"__id\\": \\"client:root.local\\", \\"__typename\\": \\"Local\\", \\"accessToken\\": \\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIzMWIyNjU5MS00ZTlhLTQzODgtYTdmZi1lMWJkYzVkOTdjY2UifQ==\\", \\"accessTokenJTI\\": \\"31b26591-4e9a-4388-a7ff-e1bdc5d97cce\\", - \\"loggedIn\\": true, \\"view\\": \\"SIGN_IN\\", \\"error\\": null }" @@ -27,7 +26,6 @@ exports[`init local state 1`] = ` \\"accessToken\\": \\"\\", \\"accessTokenExp\\": null, \\"accessTokenJTI\\": null, - \\"loggedIn\\": false, \\"view\\": \\"SIGN_IN\\", \\"error\\": null } diff --git a/src/core/client/auth/local/initLocalState.spec.ts b/src/core/client/auth/local/initLocalState.spec.ts index c7c35d62d..6f5c1cc68 100644 --- a/src/core/client/auth/local/initLocalState.spec.ts +++ b/src/core/client/auth/local/initLocalState.spec.ts @@ -39,7 +39,7 @@ it("set view from query", async () => { restoreHistoryLocation(); }); -it("get auth token from url", async () => { +it("get access token from url", async () => { const restoreHistoryLocation = replaceHistoryLocation( `http://localhost/#accessToken=${createAccessToken()}` ); diff --git a/src/core/client/auth/local/local.graphql b/src/core/client/auth/local/local.graphql index 2532160f7..e5e28807b 100644 --- a/src/core/client/auth/local/local.graphql +++ b/src/core/client/auth/local/local.graphql @@ -11,7 +11,6 @@ type Local { accessToken: String accessTokenExp: Int accessTokenJTI: String - loggedIn: Boolean! view: View! error: String } diff --git a/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap b/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap index 8b46af7b6..01d22ae39 100644 --- a/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap +++ b/src/core/client/auth/test/__snapshots__/signIn.spec.tsx.snap @@ -340,7 +340,7 @@ exports[`auth configuration renders all auth enabled 1`] = ` > Forgot your password? diff --git a/src/core/client/auth/test/accountCompletion.spec.tsx b/src/core/client/auth/test/accountCompletion.spec.tsx index 58db9a492..41cbb7f5f 100644 --- a/src/core/client/auth/test/accountCompletion.spec.tsx +++ b/src/core/client/auth/test/accountCompletion.spec.tsx @@ -122,7 +122,7 @@ it("do not render createPassword view when local auth is disabled", async () => await wait(() => expect(windowMock.closeStub.called).toBe(true)); }); -it("send back auth token", async () => { +it("send back access token", async () => { const { context } = await createTestRenderer({ Query: { viewer: { diff --git a/src/core/client/auth/test/signIn.spec.tsx b/src/core/client/auth/test/signIn.spec.tsx index 2e06086be..ed6cb605c 100644 --- a/src/core/client/auth/test/signIn.spec.tsx +++ b/src/core/client/auth/test/signIn.spec.tsx @@ -1,8 +1,9 @@ import { get } from "lodash"; -import sinon from "sinon"; +import sinon, { SinonStub } from "sinon"; import { pureMerge } from "coral-common/utils"; import { + createAccessToken, toJSON, wait, waitForElement, @@ -165,7 +166,7 @@ it("shows server error", async () => { it("submits form successfully", async () => { const { form, context } = await createTestRenderer(); const { getByLabelText } = within(form!); - const accessToken = "access-token"; + const accessToken = createAccessToken(); const emailAddressField = getByLabelText("Email Address"); const passwordField = getByLabelText("Password"); const submitButton = form!.find( @@ -198,8 +199,13 @@ it("submits form successfully", async () => { expect(toJSON(form!)).toMatchSnapshot(); - // Wait for window hash to contain a token. - await wait(() => expect(location.hash).toBe(`#accessToken=${accessToken}`)); + // Wait for new session to start. + await wait(() => + expect((context.clearSession as SinonStub).calledWith(accessToken)).toBe( + true + ) + ); + restMock.verify(); }); diff --git a/src/core/client/auth/test/signUp.spec.tsx b/src/core/client/auth/test/signUp.spec.tsx index c2446185d..0eab6eb51 100644 --- a/src/core/client/auth/test/signUp.spec.tsx +++ b/src/core/client/auth/test/signUp.spec.tsx @@ -1,7 +1,8 @@ import { get, merge } from "lodash"; -import sinon from "sinon"; +import sinon, { SinonStub } from "sinon"; import { + createAccessToken, toJSON, wait, waitForElement, @@ -186,7 +187,7 @@ it("shows server error", async () => { it("submits form successfully", async () => { const { context, main, form } = await createTestRenderer(); const { getByLabelText } = within(form!); - const accessToken = "access-token"; + const accessToken = createAccessToken(); const emailAddressField = getByLabelText("Email Address"); const usernameField = getByLabelText("Username"); const passwordField = getByLabelText("Password"); @@ -223,8 +224,12 @@ it("submits form successfully", async () => { expect(toJSON(main)).toMatchSnapshot(); - // Wait for window hash to contain a token. - await wait(() => expect(location.hash).toBe(`#accessToken=${accessToken}`)); + // Wait for new session to start. + await wait(() => + expect((context.clearSession as SinonStub).calledWith(accessToken)).toBe( + true + ) + ); restMock.verify(); }); diff --git a/src/core/client/auth/views/SignIn/SignInMutation.ts b/src/core/client/auth/views/SignIn/SignInMutation.ts index 56113a867..1e436c79d 100644 --- a/src/core/client/auth/views/SignIn/SignInMutation.ts +++ b/src/core/client/auth/views/SignIn/SignInMutation.ts @@ -13,12 +13,7 @@ export async function commit( { rest, clearSession }: CoralContext ) { const result = await signIn(rest, pick(input, ["email", "password"])); - // Put the token on the hash and clean the session. - // It'll be picked up by initLocalState. - location.hash = `accessToken=${result.token}`; - await clearSession(); - // TODO: (cvle) A better way would be if `context.clearSession` would return the new session and - // we set the accessToken directly in there. + await clearSession(result.token); } export const withSignInMutation = createMutationContainer("signIn", commit); diff --git a/src/core/client/auth/views/SignUp/SignUpMutation.ts b/src/core/client/auth/views/SignUp/SignUpMutation.ts index 9bf3fbe66..7e7904d9b 100644 --- a/src/core/client/auth/views/SignUp/SignUpMutation.ts +++ b/src/core/client/auth/views/SignUp/SignUpMutation.ts @@ -16,10 +16,7 @@ export async function commit( rest, pick(input, ["email", "password", "username"]) ); - // Put the token on the hash and clean the session. - // It'll be picked up by initLocalState. - location.hash = `accessToken=${result.token}`; - await clearSession(); + await clearSession(result.token); } export const withSignUpMutation = createMutationContainer("signUp", commit); diff --git a/src/core/client/framework/lib/bootstrap/CoralContext.tsx b/src/core/client/framework/lib/bootstrap/CoralContext.tsx index b88962704..7b1ceb978 100644 --- a/src/core/client/framework/lib/bootstrap/CoralContext.tsx +++ b/src/core/client/framework/lib/bootstrap/CoralContext.tsx @@ -62,7 +62,7 @@ export interface CoralContext { eventEmitter: EventEmitter2; /** Clear session data. */ - clearSession: () => Promise; + clearSession: (nextAccessToken?: string | null) => Promise; /** Controls router transitions (for tests) */ transitionControl?: TransitionControlData; diff --git a/src/core/client/framework/lib/bootstrap/createManaged.tsx b/src/core/client/framework/lib/bootstrap/createManaged.tsx index 67199e5d6..bfc9910ac 100644 --- a/src/core/client/framework/lib/bootstrap/createManaged.tsx +++ b/src/core/client/framework/lib/bootstrap/createManaged.tsx @@ -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. diff --git a/src/core/client/framework/lib/relay/localState.ts b/src/core/client/framework/lib/relay/localState.ts index 1b2ef671d..f5ba7335e 100644 --- a/src/core/client/framework/lib/relay/localState.ts +++ b/src/core/client/framework/lib/relay/localState.ts @@ -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); }); } diff --git a/src/core/client/framework/mutations/SetAccessTokenMutation.spec.ts b/src/core/client/framework/mutations/SetAccessTokenMutation.spec.ts deleted file mode 100644 index 239e59865..000000000 --- a/src/core/client/framework/mutations/SetAccessTokenMutation.spec.ts +++ /dev/null @@ -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 = { - 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 = { - 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); -}); diff --git a/src/core/client/framework/mutations/SetAccessTokenMutation.ts b/src/core/client/framework/mutations/SetAccessTokenMutation.ts index 6a73d12ac..73af5958f 100644 --- a/src/core/client/framework/mutations/SetAccessTokenMutation.ts +++ b/src/core/client/framework/mutations/SetAccessTokenMutation.ts @@ -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); } ); diff --git a/src/core/client/framework/testHelpers/createTestRenderer.tsx b/src/core/client/framework/testHelpers/createTestRenderer.tsx index 9a29ddd2d..62c0dfe40 100644 --- a/src/core/client/framework/testHelpers/createTestRenderer.tsx +++ b/src/core/client/framework/testHelpers/createTestRenderer.tsx @@ -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( diff --git a/src/core/client/stream/App/TabBarContainer.tsx b/src/core/client/stream/App/TabBarContainer.tsx index 642e0e035..23d6b42e5 100644 --- a/src/core/client/stream/App/TabBarContainer.tsx +++ b/src/core/client/stream/App/TabBarContainer.tsx @@ -31,16 +31,16 @@ export class TabBarContainer extends Component { public render() { const { - local: { loggedIn, activeTab }, + local: { activeTab }, + viewer, } = this.props; return ( @@ -52,7 +52,6 @@ const enhanced = withSetActiveTabMutation( withLocalStateContainer( graphql` fragment TabBarContainerLocal on Local { - loggedIn activeTab } ` diff --git a/src/core/client/stream/App/listeners/OnPostMessageSetAccessToken.spec.tsx b/src/core/client/stream/App/listeners/OnPostMessageSetAccessToken.spec.tsx index a59962b61..f97042eb5 100644 --- a/src/core/client/stream/App/listeners/OnPostMessageSetAccessToken.spec.tsx +++ b/src/core/client/stream/App/listeners/OnPostMessageSetAccessToken.spec.tsx @@ -5,7 +5,7 @@ import { createSinonStub } from "coral-framework/testHelpers"; import { OnPostMessageSetAccessToken } from "./OnPostMessageSetAccessToken"; -it("Listens to event and sets auth token", () => { +it("Listens to event and sets access token", () => { const token = "access-token"; const postMessage: any = { on: (name: string, cb: (token: string) => void) => { diff --git a/src/core/client/stream/common/UserBox/UserBoxContainer.spec.tsx b/src/core/client/stream/common/UserBox/UserBoxContainer.spec.tsx index 0bffcf8a7..2a8babe98 100644 --- a/src/core/client/stream/common/UserBox/UserBoxContainer.spec.tsx +++ b/src/core/client/stream/common/UserBox/UserBoxContainer.spec.tsx @@ -17,6 +17,7 @@ it("renders fully", () => { focus: false, view: "SIGN_IN", }, + accessToken: "access-token", accessTokenJTI: "JTI", }, viewer: null, @@ -74,6 +75,7 @@ it("renders without logout button", () => { focus: false, view: "SIGN_IN", }, + accessToken: "access-token", accessTokenJTI: null, }, viewer: null, @@ -131,6 +133,7 @@ it("renders sso only", () => { focus: false, view: "SIGN_IN", }, + accessToken: "access-token", accessTokenJTI: "JTI", }, viewer: null, @@ -188,6 +191,7 @@ it("renders sso only without logout button", () => { focus: false, view: "SIGN_IN", }, + accessToken: "access-token", accessTokenJTI: "JTI", }, viewer: null, @@ -245,6 +249,7 @@ it("renders without register button", () => { focus: false, view: "SIGN_IN", }, + accessToken: "access-token", accessTokenJTI: "JTI", }, viewer: null, diff --git a/src/core/client/stream/common/UserBox/UserBoxContainer.tsx b/src/core/client/stream/common/UserBox/UserBoxContainer.tsx index eeca7717c..5f6467cd9 100644 --- a/src/core/client/stream/common/UserBox/UserBoxContainer.tsx +++ b/src/core/client/stream/common/UserBox/UserBoxContainer.tsx @@ -45,7 +45,9 @@ export class UserBoxContainer extends Component { private handleSignOut = () => this.props.signOut(); private get supportsLogout() { - return !!this.props.local.accessTokenJTI; + return Boolean( + !this.props.local.accessToken || this.props.local.accessTokenJTI + ); } private get supportsRegister() { @@ -125,6 +127,7 @@ const enhanced = withSignOutMutation( focus view } + accessToken accessTokenJTI } ` diff --git a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap index 583073520..a6b74f98a 100644 --- a/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap +++ b/src/core/client/stream/local/__snapshots__/initLocalState.spec.ts.snap @@ -15,7 +15,6 @@ exports[`init local state 1`] = ` \\"accessToken\\": \\"\\", \\"accessTokenExp\\": null, \\"accessTokenJTI\\": null, - \\"loggedIn\\": false, \\"commentsOrderBy\\": \\"CREATED_AT_DESC\\", \\"authPopup\\": { \\"__ref\\": \\"client:root.local.authPopup\\" diff --git a/src/core/client/stream/local/initLocalState.spec.ts b/src/core/client/stream/local/initLocalState.spec.ts index 3840834a3..47f1b69fe 100644 --- a/src/core/client/stream/local/initLocalState.spec.ts +++ b/src/core/client/stream/local/initLocalState.spec.ts @@ -5,7 +5,6 @@ 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, replaceHistoryLocation, } from "coral-framework/testHelpers"; @@ -57,13 +56,3 @@ it("set commentID from query", async () => { expect(source.get(LOCAL_ID)!.commentID).toBe(commentID); restoreHistoryLocation(); }); - -it("set accessToken from localStorage", async () => { - const context: Partial = { - localStorage: createPromisifiedStorage(), - }; - const accessToken = createAccessToken(); - context.localStorage!.setItem("accessToken", accessToken); - await initLocalState(environment, context as any); - expect(source.get(LOCAL_ID)!.accessToken).toBe(accessToken); -}); diff --git a/src/core/client/stream/local/local.graphql b/src/core/client/stream/local/local.graphql index 17f41aec9..9046b60e2 100644 --- a/src/core/client/stream/local/local.graphql +++ b/src/core/client/stream/local/local.graphql @@ -46,7 +46,6 @@ type Local { accessToken: String accessTokenExp: Int accessTokenJTI: String - loggedIn: Boolean! activeTab: TAB! authPopup: AuthPopup! storyID: String diff --git a/src/core/client/stream/tabs/Comments/Comment/ReportButton/ReportButtonContainer.tsx b/src/core/client/stream/tabs/Comments/Comment/ReportButton/ReportButtonContainer.tsx index 64a63d15d..21e3bb335 100644 --- a/src/core/client/stream/tabs/Comments/Comment/ReportButton/ReportButtonContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Comment/ReportButton/ReportButtonContainer.tsx @@ -23,7 +23,7 @@ class ReportButtonContainer extends React.Component< ReportButtonContainerProps > { private get loggedIn() { - return this.props.viewer; + return Boolean(this.props.viewer); } private handleSignIn = () => this.props.showAuthPopup({ view: "SIGN_IN" }); diff --git a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.spec.tsx b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.spec.tsx index 0a53e7cc1..d7f123bcf 100644 --- a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.spec.tsx +++ b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.spec.tsx @@ -19,9 +19,6 @@ type Props = PropTypesOf; function createDefaultProps(add: DeepPartial = {}): Props { return pureMerge( { - local: { - loggedIn: true, - }, showAuthPopup: noop as any, createComment: noop as any, refreshSettings: noop as any, @@ -49,6 +46,9 @@ function createDefaultProps(add: DeepPartial = {}): Props { message: "", }, }, + viewer: { + id: "viewer-id", + }, }, add ); diff --git a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx index fa7a863bd..30e706096 100644 --- a/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Stream/PostCommentForm/PostCommentFormContainer.tsx @@ -11,13 +11,12 @@ import { graphql, withFetch, withFragmentContainer, - withLocalStateContainer, } from "coral-framework/lib/relay"; import { PromisifiedStorage } from "coral-framework/lib/storage"; import { PropTypesOf } from "coral-framework/types"; -import { PostCommentFormContainer_settings as SettingsData } from "coral-stream/__generated__/PostCommentFormContainer_settings.graphql"; -import { PostCommentFormContainer_story as StoryData } from "coral-stream/__generated__/PostCommentFormContainer_story.graphql"; -import { PostCommentFormContainerLocal as Local } from "coral-stream/__generated__/PostCommentFormContainerLocal.graphql"; +import { PostCommentFormContainer_settings } from "coral-stream/__generated__/PostCommentFormContainer_settings.graphql"; +import { PostCommentFormContainer_story } from "coral-stream/__generated__/PostCommentFormContainer_story.graphql"; +import { PostCommentFormContainer_viewer } from "coral-stream/__generated__/PostCommentFormContainer_viewer.graphql"; import { ShowAuthPopupMutation, withShowAuthPopupMutation, @@ -42,9 +41,9 @@ interface Props { createComment: CreateCommentMutation; refreshSettings: FetchProp; sessionStorage: PromisifiedStorage; - settings: SettingsData; - local: Local; - story: StoryData; + settings: PostCommentFormContainer_settings; + viewer: PostCommentFormContainer_viewer | null; + story: PostCommentFormContainer_story; showAuthPopup: ShowAuthPopupMutation; } @@ -71,7 +70,7 @@ export class PostCommentFormContainer extends Component { initialized: false, nudge: true, keepFormWhenClosed: - this.props.local.loggedIn && + !!this.props.viewer && !this.props.story.isClosed && !this.props.settings.disableCommenting.enabled, submitStatus: null, @@ -194,7 +193,7 @@ export class PostCommentFormContainer extends Component { ); } } - if (!this.props.local.loggedIn) { + if (!this.props.viewer) { return ( ({ withShowAuthPopupMutation( withCreateCommentMutation( withFetch(RefreshSettingsFetch)( - withLocalStateContainer( - graphql` - fragment PostCommentFormContainerLocal on Local { - loggedIn + withFragmentContainer({ + settings: graphql` + fragment PostCommentFormContainer_settings on Settings { + charCount { + enabled + min + max + } + disableCommenting { + enabled + message + } + closeCommenting { + message + } } - ` - )( - withFragmentContainer({ - settings: graphql` - fragment PostCommentFormContainer_settings on Settings { - charCount { + `, + story: graphql` + fragment PostCommentFormContainer_story on Story { + id + isClosed + ...MessageBoxContainer_story + settings { + messageBox { enabled - min - max - } - disableCommenting { - enabled - message - } - closeCommenting { - message } } - `, - story: graphql` - fragment PostCommentFormContainer_story on Story { - id - isClosed - ...MessageBoxContainer_story - settings { - messageBox { - enabled - } - } - } - `, - })(PostCommentFormContainer) - ) + } + `, + viewer: graphql` + fragment PostCommentFormContainer_viewer on User { + id + } + `, + })(PostCommentFormContainer) ) ) ) diff --git a/src/core/client/stream/tabs/Comments/Stream/StreamContainer.tsx b/src/core/client/stream/tabs/Comments/Stream/StreamContainer.tsx index 312450ca3..eec61e312 100644 --- a/src/core/client/stream/tabs/Comments/Stream/StreamContainer.tsx +++ b/src/core/client/stream/tabs/Comments/Stream/StreamContainer.tsx @@ -85,6 +85,7 @@ export const StreamContainer: FunctionComponent = props => { )} {banned && } @@ -170,6 +171,7 @@ const enhanced = withFragmentContainer({ ...UserBoxContainer_viewer ...CreateCommentReplyMutation_viewer ...CreateCommentMutation_viewer + ...PostCommentFormContainer_viewer status { current } diff --git a/src/core/client/stream/test/comments/create.ts b/src/core/client/stream/test/comments/create.ts index c52615ae0..f2bcf4357 100644 --- a/src/core/client/stream/test/comments/create.ts +++ b/src/core/client/stream/test/comments/create.ts @@ -8,7 +8,6 @@ export default function create(params: CreateTestRendererParams) { initLocalState: (localRecord, source, environment) => { localRecord.setValue("COMMENTS", "activeTab"); localRecord.setValue("ALL_COMMENTS", "commentsTab"); - localRecord.setValue(false, "loggedIn"); localRecord.setValue("jti", "accessTokenJTI"); localRecord.setValue("CREATED_AT_DESC", "commentsOrderBy"); if (params.initLocalState) { diff --git a/src/core/client/stream/test/comments/permalink/permalinkViewPostReply.spec.tsx b/src/core/client/stream/test/comments/permalink/permalinkViewPostReply.spec.tsx index f83d63851..1deda89cc 100644 --- a/src/core/client/stream/test/comments/permalink/permalinkViewPostReply.spec.tsx +++ b/src/core/client/stream/test/comments/permalink/permalinkViewPostReply.spec.tsx @@ -60,7 +60,6 @@ const createTestRenderer = async ( initLocalState: (localRecord, source, environment) => { localRecord.setValue(storyFixture.id, "storyID"); localRecord.setValue(commentFixture.id, "commentID"); - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/stream/test/comments/stream/banned.spec.tsx b/src/core/client/stream/test/comments/stream/banned.spec.tsx index dd854f8fc..6746c5d07 100644 --- a/src/core/client/stream/test/comments/stream/banned.spec.tsx +++ b/src/core/client/stream/test/comments/stream/banned.spec.tsx @@ -51,7 +51,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue(story.id, "storyID"); - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/stream/test/comments/stream/charCountEditComment.spec.tsx b/src/core/client/stream/test/comments/stream/charCountEditComment.spec.tsx index d01afa610..b615c7e98 100644 --- a/src/core/client/stream/test/comments/stream/charCountEditComment.spec.tsx +++ b/src/core/client/stream/test/comments/stream/charCountEditComment.spec.tsx @@ -52,7 +52,6 @@ async function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); diff --git a/src/core/client/stream/test/comments/stream/charCountPostComment.spec.tsx b/src/core/client/stream/test/comments/stream/charCountPostComment.spec.tsx index 99fc2f7c3..0bd809844 100644 --- a/src/core/client/stream/test/comments/stream/charCountPostComment.spec.tsx +++ b/src/core/client/stream/test/comments/stream/charCountPostComment.spec.tsx @@ -44,7 +44,6 @@ async function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); diff --git a/src/core/client/stream/test/comments/stream/charCountReplyComment.spec.tsx b/src/core/client/stream/test/comments/stream/charCountReplyComment.spec.tsx index eae32b1c4..4e67e33bb 100644 --- a/src/core/client/stream/test/comments/stream/charCountReplyComment.spec.tsx +++ b/src/core/client/stream/test/comments/stream/charCountReplyComment.spec.tsx @@ -44,7 +44,6 @@ async function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); diff --git a/src/core/client/stream/test/comments/stream/editComment.spec.tsx b/src/core/client/stream/test/comments/stream/editComment.spec.tsx index 92aab6bd0..3df911cd4 100644 --- a/src/core/client/stream/test/comments/stream/editComment.spec.tsx +++ b/src/core/client/stream/test/comments/stream/editComment.spec.tsx @@ -69,7 +69,6 @@ function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(storyWithReplies.id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); return testRenderer; diff --git a/src/core/client/stream/test/comments/stream/ignoreUser.spec.tsx b/src/core/client/stream/test/comments/stream/ignoreUser.spec.tsx index 58fbe61c3..03ca6fd7b 100644 --- a/src/core/client/stream/test/comments/stream/ignoreUser.spec.tsx +++ b/src/core/client/stream/test/comments/stream/ignoreUser.spec.tsx @@ -38,7 +38,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue(story.id, "storyID"); - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/stream/test/comments/stream/memberSince.spec.tsx b/src/core/client/stream/test/comments/stream/memberSince.spec.tsx index d71284e54..6333430c3 100644 --- a/src/core/client/stream/test/comments/stream/memberSince.spec.tsx +++ b/src/core/client/stream/test/comments/stream/memberSince.spec.tsx @@ -31,7 +31,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue(story.id, "storyID"); - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/stream/test/comments/stream/moderation.spec.tsx b/src/core/client/stream/test/comments/stream/moderation.spec.tsx index 14c570a52..af3382690 100644 --- a/src/core/client/stream/test/comments/stream/moderation.spec.tsx +++ b/src/core/client/stream/test/comments/stream/moderation.spec.tsx @@ -33,7 +33,6 @@ async function createTestRenderer( ), initLocalState: (localRecord, source, environment) => { localRecord.setValue(story.id, "storyID"); - localRecord.setValue(true, "loggedIn"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); } diff --git a/src/core/client/stream/test/comments/stream/postComment.spec.tsx b/src/core/client/stream/test/comments/stream/postComment.spec.tsx index 31cb8159c..44e1f6862 100644 --- a/src/core/client/stream/test/comments/stream/postComment.spec.tsx +++ b/src/core/client/stream/test/comments/stream/postComment.spec.tsx @@ -45,7 +45,6 @@ async function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); diff --git a/src/core/client/stream/test/comments/stream/postLocalReply.spec.tsx b/src/core/client/stream/test/comments/stream/postLocalReply.spec.tsx index c859839d9..f98de8ce8 100644 --- a/src/core/client/stream/test/comments/stream/postLocalReply.spec.tsx +++ b/src/core/client/stream/test/comments/stream/postLocalReply.spec.tsx @@ -60,7 +60,6 @@ beforeEach(() => { resolvers, initLocalState: localRecord => { localRecord.setValue(storyWithDeepestReplies.id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, })); }); diff --git a/src/core/client/stream/test/comments/stream/postReply.spec.tsx b/src/core/client/stream/test/comments/stream/postReply.spec.tsx index 307b0ceb5..d3773ef17 100644 --- a/src/core/client/stream/test/comments/stream/postReply.spec.tsx +++ b/src/core/client/stream/test/comments/stream/postReply.spec.tsx @@ -41,7 +41,6 @@ async function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); diff --git a/src/core/client/stream/test/comments/stream/reaction.spec.tsx b/src/core/client/stream/test/comments/stream/reaction.spec.tsx index ea9b37e0b..e40d3a1b0 100644 --- a/src/core/client/stream/test/comments/stream/reaction.spec.tsx +++ b/src/core/client/stream/test/comments/stream/reaction.spec.tsx @@ -57,7 +57,6 @@ function createTestRenderer() { resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); return { testRenderer, resolvers }; diff --git a/src/core/client/stream/test/comments/stream/renderMessageBox.spec.tsx b/src/core/client/stream/test/comments/stream/renderMessageBox.spec.tsx index 75171f8b6..f0d80f2be 100644 --- a/src/core/client/stream/test/comments/stream/renderMessageBox.spec.tsx +++ b/src/core/client/stream/test/comments/stream/renderMessageBox.spec.tsx @@ -32,7 +32,6 @@ async function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(storyWithNoComments.id, "storyID"); - localRecord.setValue(Boolean(data.loggedIn), "loggedIn"); }, }); diff --git a/src/core/client/stream/test/comments/stream/reportComment.spec.tsx b/src/core/client/stream/test/comments/stream/reportComment.spec.tsx index bca80bfd8..59f8b0a60 100644 --- a/src/core/client/stream/test/comments/stream/reportComment.spec.tsx +++ b/src/core/client/stream/test/comments/stream/reportComment.spec.tsx @@ -66,7 +66,6 @@ function createTestRenderer( resolvers, initLocalState: localRecord => { localRecord.setValue(stories[0].id, "storyID"); - localRecord.setValue(true, "loggedIn"); }, }); return { testRenderer, resolvers }; diff --git a/src/core/client/stream/test/profile/create.ts b/src/core/client/stream/test/profile/create.ts index 6b367ca53..f9abfc978 100644 --- a/src/core/client/stream/test/profile/create.ts +++ b/src/core/client/stream/test/profile/create.ts @@ -11,7 +11,6 @@ export default function create(params: CreateTestRendererParams) { initLocalState: (localRecord, source, environment) => { localRecord.setValue("PROFILE", "activeTab"); localRecord.setValue("jti", "accessTokenJTI"); - localRecord.setValue(true, "loggedIn"); localRecord.setValue(story.id, "storyID"); if (params.initLocalState) { params.initLocalState(localRecord, source, environment); diff --git a/src/core/client/ui/components/TextField/TextField.css b/src/core/client/ui/components/TextField/TextField.css index dc7fd48fb..96e81d0b2 100644 --- a/src/core/client/ui/components/TextField/TextField.css +++ b/src/core/client/ui/components/TextField/TextField.css @@ -13,7 +13,6 @@ box-sizing: border-box; border-radius: var(--round-corners); width: 100%; - line-height: 36px; align-self: stretch; &:read-only { diff --git a/src/core/client/ui/shared/typography.css b/src/core/client/ui/shared/typography.css index 6dc5ea2d5..abd184eb5 100644 --- a/src/core/client/ui/shared/typography.css +++ b/src/core/client/ui/shared/typography.css @@ -183,7 +183,7 @@ font-weight: var(--font-weight-regular); font-family: var(--font-family-sans-serif); font-size: calc(16rem / var(--rem-base)); - line-height: calc(18em / 16); + line-height: 1; letter-spacing: calc(0.2em / 16); color: var(--palette-text-primary); } diff --git a/src/core/common/errors.ts b/src/core/common/errors.ts index 795916152..7be60adc1 100644 --- a/src/core/common/errors.ts +++ b/src/core/common/errors.ts @@ -261,4 +261,9 @@ export enum ERROR_CODES { * times by the same user. */ RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED", + + /** + * JWT_REVOKED is returned when the token referenced has been revoked. + */ + JWT_REVOKED = "JWT_REVOKED", } diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts index ff97ac044..3f52e96af 100644 --- a/src/core/server/app/middleware/passport/index.ts +++ b/src/core/server/app/middleware/passport/index.ts @@ -1,4 +1,4 @@ -import { NextFunction, RequestHandler, Response } from "express"; +import { CookieOptions, NextFunction, RequestHandler, Response } from "express"; import { Redis } from "ioredis"; import Joi from "joi"; import jwt from "jsonwebtoken"; @@ -14,12 +14,14 @@ 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, signTokenString, } from "coral-server/services/jwt"; import { Request } from "coral-server/types/express"; +import { DateTime } from "luxon"; export type VerifyCallback = ( err?: Error | null, @@ -70,20 +72,18 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) { // Extract the token from the request. const token = extractTokenFromRequest(req); if (!token) { - // TODO: (wyattjoh) return a better error. - throw new Error("logout requires a token on the request, none was found"); + // No token on the request, indicate that this was successful. + return res.sendStatus(204); } - // Coral is guarenteed at this point. + // Coral is guaranteed at this point. const { now } = req.coral!; // Decode the token. const decoded = jwt.decode(token, {}); if (!decoded) { - // TODO: (wyattjoh) return a better error. - throw new Error( - "logout requires a token on the request, token was invalid" - ); + // Invalid token on request, indicate that this was successful. + return res.sendStatus(204); } // Grab the JTI from the decoded token. @@ -94,9 +94,12 @@ export async function handleLogout(redis: Redis, req: Request, res: Response) { if (validFor > 0) { // Invalidate the token, the expiry is in the future and it needs to be // revoked. - await revokeJWT(redis, jti, validFor); + await revokeJWT(redis, jti, validFor, now); } + // Clear the cookie. + res.clearCookie(COOKIE_NAME, generateCookieOptions(req, new Date(0))); + return res.sendStatus(204); } @@ -108,16 +111,29 @@ export async function handleSuccessfulLogin( next: NextFunction ) { try { + // Coral is guaranteed at this point. + const coral = req.coral!; + // Tenant is guaranteed at this point. - const tenant = req.coral!.tenant!; + const tenant = coral.tenant!; + + // Compute the expiry date. + const expiresIn = DateTime.fromJSDate(coral.now).plus({ days: 1 }); // Grab the token. - const token = await signTokenString(signingConfig, user, tenant); + const token = await signTokenString(signingConfig, user, tenant, { + expiresIn: Math.floor(expiresIn.toSeconds()), + }); // Set the cache control headers. 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()) + ); // Send back the details! res.json({ token }); @@ -126,6 +142,16 @@ export async function handleSuccessfulLogin( } } +const generateCookieOptions = ( + req: Request, + expiresIn: Date +): CookieOptions => ({ + path: "/api", + httpOnly: true, + secure: req.secure, + expires: expiresIn, +}); + export async function handleOAuth2Callback( err: Error | null, user: User | null, @@ -147,8 +173,18 @@ export async function handleOAuth2Callback( // Tenant is guaranteed at this point. const tenant = req.coral!.tenant!; + // Compute the expiry date. + const expiresIn = DateTime.fromJSDate(req.coral!.now).plus({ days: 1 }); + // Grab the token. - const token = await signTokenString(signingConfig, user, tenant); + const token = await signTokenString(signingConfig, user, tenant, { + expiresIn: Math.floor(expiresIn.toSeconds()), + }); + res.cookie( + COOKIE_NAME, + token, + generateCookieOptions(req, expiresIn.toJSDate()) + ); // Send back the details! res.redirect(path + `#accessToken=${token}`); diff --git a/src/core/server/app/middleware/passport/strategies/jwt.ts b/src/core/server/app/middleware/passport/strategies/jwt.ts index be289f08d..388cd9e2c 100644 --- a/src/core/server/app/middleware/passport/strategies/jwt.ts +++ b/src/core/server/app/middleware/passport/strategies/jwt.ts @@ -2,7 +2,11 @@ import jwt from "jsonwebtoken"; import { Strategy } from "passport-strategy"; import { AppOptions } from "coral-server/app"; -import { TenantNotFoundError, TokenInvalidError } from "coral-server/errors"; +import { + JWTRevokedError, + TenantNotFoundError, + TokenInvalidError, +} from "coral-server/errors"; import { Tenant } from "coral-server/models/tenant"; import { User } from "coral-server/models/user"; import { extractTokenFromRequest } from "coral-server/services/jwt"; @@ -54,7 +58,7 @@ export function createVerifiers( ]; } -export function verifyAndRetrieveUser( +export async function verifyAndRetrieveUser( verifiers: Array>, tenant: Tenant, tokenString: string, @@ -65,11 +69,21 @@ export function verifyAndRetrieveUser( throw new TokenInvalidError(tokenString, "token could not be decoded"); } - // Try to verify the token. - for (const verifier of verifiers) { - if (verifier.supports(token, tenant)) { - return verifier.verify(tokenString, token, tenant, now); + try { + // Try to verify the token. + for (const verifier of verifiers) { + if (verifier.supports(token, tenant)) { + return await verifier.verify(tokenString, token, tenant, now); + } } + } catch (err) { + // When the JWT was revoked, just indicate that there is no user on the + // request rather than erroring out. + if (err instanceof JWTRevokedError) { + return null; + } + + throw err; } // No verifier could be found. diff --git a/src/core/server/app/middleware/passport/strategies/verifiers/jwt.ts b/src/core/server/app/middleware/passport/strategies/verifiers/jwt.ts index 5e8dca242..21d232d82 100644 --- a/src/core/server/app/middleware/passport/strategies/verifiers/jwt.ts +++ b/src/core/server/app/middleware/passport/strategies/verifiers/jwt.ts @@ -6,7 +6,7 @@ import { Db } from "mongodb"; import { Tenant } from "coral-server/models/tenant"; import { retrieveUser } from "coral-server/models/user"; import { - checkJWTRevoked, + isJWTRevoked, JWTSigningConfig, StandardClaims, verifyJWT, @@ -74,7 +74,9 @@ export class JWTVerifier implements Verifier { if (!token.pat) { // Check to see if the token has been revoked, as these tokens can be // revoked. - await checkJWTRevoked(this.redis, token.jti); + if (await isJWTRevoked(this.redis, token.jti)) { + return null; + } } // Find the user. diff --git a/src/core/server/app/router/index.ts b/src/core/server/app/router/index.ts index 7f114b74e..a202479e9 100644 --- a/src/core/server/app/router/index.ts +++ b/src/core/server/app/router/index.ts @@ -1,3 +1,4 @@ +import cookies from "cookie-parser"; import express, { Router } from "express"; import { register } from "prom-client"; @@ -16,7 +17,12 @@ export function createRouter(app: AppOptions, options: RouterOptions) { const router = express.Router(); // Attach the API router. - router.use("/api", noCacheMiddleware, createAPIRouter(app, options)); + router.use( + "/api", + noCacheMiddleware, + cookies(), + createAPIRouter(app, options) + ); // Attach the GraphiQL if enabled. if (app.config.get("enable_graphiql")) { diff --git a/src/core/server/errors/index.ts b/src/core/server/errors/index.ts index c13cd0016..968a9e4a9 100644 --- a/src/core/server/errors/index.ts +++ b/src/core/server/errors/index.ts @@ -497,6 +497,15 @@ export class InvalidCredentialsError extends CoralError { } } +export class JWTRevokedError extends CoralError { + constructor(jti: string) { + super({ + code: ERROR_CODES.AUTHENTICATION_ERROR, + context: { pvt: { jti } }, + }); + } +} + export class AuthenticationError extends CoralError { constructor(reason: string) { super({ diff --git a/src/core/server/errors/translations.ts b/src/core/server/errors/translations.ts index a29730f0b..9a30e45f2 100644 --- a/src/core/server/errors/translations.ts +++ b/src/core/server/errors/translations.ts @@ -45,4 +45,5 @@ export const ERROR_TRANSLATIONS: Record = { PASSWORD_RESET_TOKEN_EXPIRED: "error-passwordResetTokenExpired", EMAIL_CONFIRM_TOKEN_EXPIRED: "error-emailConfirmTokenExpired", RATE_LIMIT_EXCEEDED: "error-rateLimitExceeded", + JWT_REVOKED: "error-jwtRevoked", }; diff --git a/src/core/server/services/jwt/index.spec.ts b/src/core/server/services/jwt/index.spec.ts index 4fb91cd3f..6fc9c687b 100644 --- a/src/core/server/services/jwt/index.spec.ts +++ b/src/core/server/services/jwt/index.spec.ts @@ -26,6 +26,7 @@ describe("extractJWTFromRequest", () => { it("extracts the token from query string", () => { const req = { url: "", + headers: {}, }; expect(extractTokenFromRequest((req as any) as Request)).toEqual(null); @@ -37,6 +38,7 @@ describe("extractJWTFromRequest", () => { it("does not extract the token from query string when it's disabled", () => { const req = { url: "https://coral.coralproject.net/api?accessToken=token", + headers: {}, }; expect(extractTokenFromRequest((req as any) as Request, true)).toEqual( diff --git a/src/core/server/services/jwt/index.ts b/src/core/server/services/jwt/index.ts index 30aabc66d..565c0a223 100644 --- a/src/core/server/services/jwt/index.ts +++ b/src/core/server/services/jwt/index.ts @@ -1,3 +1,5 @@ +import cookie from "cookie"; +import { IncomingMessage } from "http"; import { Redis } from "ioredis"; import Joi from "joi"; import jwt, { SignOptions, VerifyOptions } from "jsonwebtoken"; @@ -6,11 +8,14 @@ import uuid from "uuid/v4"; import { Omit } from "coral-common/types"; import { Config } from "coral-server/config"; -import { AuthenticationError, TokenInvalidError } from "coral-server/errors"; +import { + AuthenticationError, + JWTRevokedError, + TokenInvalidError, +} from "coral-server/errors"; import { Tenant } from "coral-server/models/tenant"; import { User } from "coral-server/models/user"; import { Request } from "coral-server/types/express"; -import { IncomingMessage } from "http"; /** * The following Claim Names are registered in the IANA "JSON Web Token @@ -213,7 +218,6 @@ export const signTokenString = async ( ) => jwt.sign({}, secret, { jwtid: uuid(), - // TODO: (wyattjoh) evaluate allowing configuration? expiresIn: "1 day", ...options, issuer: tenant.id, @@ -241,6 +245,8 @@ export async function signString( } /** + * 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. * * @param req the request to extract the JWT from * @param excludeQuery when true, does not pull from the query params @@ -248,6 +254,69 @@ export async function signString( export function extractTokenFromRequest( req: Request | IncomingMessage, excludeQuery: boolean = false +): string | null { + return ( + extractJWTFromRequestHeaders(req, excludeQuery) || + extractJWTFromRequestCookie(req) + ); +} + +/** + * 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 + * IncomingMessage. + * + * @param req a request to test if it is an Express Request or not. + */ +export function isExpressRequest( + req: Request | IncomingMessage +): req is Request { + // Only Express Request objects contain an `app` field. + if (typeof (req as Request).app === "undefined") { + return false; + } + + 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; + } + + // 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; +} + +/** + * + * @param req the request to extract the JWT from + * @param excludeQuery when true, does not pull from the query params + */ +function extractJWTFromRequestHeaders( + req: Request | IncomingMessage, + excludeQuery: boolean = false ) { const options: BearerOptions = { basic: "password", @@ -267,18 +336,53 @@ function generateJTIRevokedKey(jti: string) { return `jtir:${jti}`; } -export async function revokeJWT(redis: Redis, jti: string, validFor: number) { +/** + * revokeJWT will place the token into a blacklist until it expires. + * + * @param redis the Redis instance to revoke the JWT with + * @param jti the JTI claim of the JWT token being revoked + * @param validFor number of seconds that the token was valid for + * @param now the current date + */ +export async function revokeJWT( + redis: Redis, + jti: string, + validFor: number, + now = new Date() +) { await redis.setex( generateJTIRevokedKey(jti), Math.ceil(validFor), - Date.now() + now.valueOf() ); } -export async function checkJWTRevoked(redis: Redis, jti: string) { +/** + * isJWTRevoked will check to see if the given token referenced by the JWT has + * been revoked or not. + * + * @param redis the Redis instance to check to see if the token was revoked + * @param jti the JTI claim of the JWT token being tested + */ +export async function isJWTRevoked(redis: Redis, jti: string) { const expiredAtString = await redis.get(generateJTIRevokedKey(jti)); if (expiredAtString) { - throw new AuthenticationError("JWT was revoked"); + return true; + } + + return false; +} + +/** + * checkJWTRevoked will test the JWT's JTI to see if it's revoked, if it is, it + * will throw an error. + * + * @param redis the Redis instance to check to see if the token was revoked + * @param jti the JTI claim of the JWT token being tested + */ +export async function checkJWTRevoked(redis: Redis, jti: string) { + if (await isJWTRevoked(redis, jti)) { + throw new JWTRevokedError(jti); } }