diff --git a/.github/workflows/test-edge-function.yml b/.github/workflows/test-edge-function.yml new file mode 100644 index 000000000..f07244480 --- /dev/null +++ b/.github/workflows/test-edge-function.yml @@ -0,0 +1,33 @@ +name: "Test edge function compatibility" +on: push +jobs: + test: + runs-on: ubuntu-latest + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.netlify_auth_token }} + NETLIFY_SITE_ID: ${{ secrets.netlify_site_id }} + TEST_DEPLOYED_VERSION: true + defaults: + run: + working-directory: examples/next/with-emailpassword + steps: + - uses: actions/checkout@v2 + - run: echo $GITHUB_REF_NAME + - run: npm install git+https://github.com:supertokens/supertokens-node.git#$GITHUB_SHA + - run: npm install + - run: npm install mocha@6.1.4 jsdom-global@3.0.2 puppeteer@^11.0.0 isomorphic-fetch@^3.0.0 + - run: netlify deploy --alias 0 --build --json --auth=$NETLIFY_AUTH_TOKEN > deployInfo.json + - run: cat deployInfo.json + - run: | + ( \ + (echo "=========== Test attempt 1 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) || \ + (echo "=========== Test attempt 2 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) || \ + (echo "=========== Test attempt 3 ===========" && npx mocha --no-config --timeout 80000 test/**/*.test.js) \ + ) + - name: The job has failed + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: screenshots + path: | + ./**/*screenshot.jpeg diff --git a/.npmignore b/.npmignore index b0fd4847c..683ee5888 100644 --- a/.npmignore +++ b/.npmignore @@ -17,4 +17,5 @@ coreDriverInterfaceSupported.json examples/ add-ts-no-check.js docs/ -.prettierignore \ No newline at end of file +.prettierignore +updateLibInExample \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 80babb56b..2aed35ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds support for configuring multiple frontend domains to be used with the same backend - Added a new `origin` property to `appInfo`, this can be configured to be a function which allows you to conditionally return the value of the frontend domain. This property will replace `websiteDomain` in a future release of `supertokens-node` - `websiteDomain` inside `appInfo` is now optional. Using `origin` is recommended over using `websiteDomain`. This is not a breaking change and using `websiteDomain` will continue to work +- Added a "custom" framework you can use in framework normally not supported by our SDK +- Added a next13 app router compatible request handler. ### Fixed - Fixed an issue where calling signinup for thirdparty recipe would result in a "clone is not a function" error +### Changes + +- Using built-in fetch whenever available instead of cross-fetch +- Improved edge-function support + ## [16.2.1] - 2023-10-06 - Slight refactors logic to code for social providers to make it consistent across all providers diff --git a/examples/aws/with-emailpassword/test/basic.test.js b/examples/aws/with-emailpassword/test/basic.test.js index 34f06c029..31bb96004 100644 --- a/examples/aws/with-emailpassword/test/basic.test.js +++ b/examples/aws/with-emailpassword/test/basic.test.js @@ -86,7 +86,8 @@ describe("SuperTokens Example Basic tests", function () { ]); await submitForm(page); await page.waitForNavigation(); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/express/with-emailpassword/test/basic.test.js b/examples/express/with-emailpassword/test/basic.test.js index b40f68c48..bc33830fb 100644 --- a/examples/express/with-emailpassword/test/basic.test.js +++ b/examples/express/with-emailpassword/test/basic.test.js @@ -79,7 +79,8 @@ describe("SuperTokens Example Basic tests", function () { { name: "password", value: testPW }, ]); await submitForm(page); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/fastify/with-emailpassword/test/basic.test.js b/examples/fastify/with-emailpassword/test/basic.test.js index b40f68c48..bc33830fb 100644 --- a/examples/fastify/with-emailpassword/test/basic.test.js +++ b/examples/fastify/with-emailpassword/test/basic.test.js @@ -79,7 +79,8 @@ describe("SuperTokens Example Basic tests", function () { { name: "password", value: testPW }, ]); await submitForm(page); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/hapi/with-emailpassword/test/basic.test.js b/examples/hapi/with-emailpassword/test/basic.test.js index 010816c0e..5a4fc5f87 100644 --- a/examples/hapi/with-emailpassword/test/basic.test.js +++ b/examples/hapi/with-emailpassword/test/basic.test.js @@ -80,7 +80,8 @@ describe("SuperTokens Example Basic tests", function () { ]); await submitForm(page); await page.waitForNavigation(); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/koa/with-emailpassword/test/basic.test.js b/examples/koa/with-emailpassword/test/basic.test.js index b40f68c48..bc33830fb 100644 --- a/examples/koa/with-emailpassword/test/basic.test.js +++ b/examples/koa/with-emailpassword/test/basic.test.js @@ -79,7 +79,8 @@ describe("SuperTokens Example Basic tests", function () { { name: "password", value: testPW }, ]); await submitForm(page); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/loopback/with-emailpassword/test/basic.test.js b/examples/loopback/with-emailpassword/test/basic.test.js index 010816c0e..5a4fc5f87 100644 --- a/examples/loopback/with-emailpassword/test/basic.test.js +++ b/examples/loopback/with-emailpassword/test/basic.test.js @@ -80,7 +80,8 @@ describe("SuperTokens Example Basic tests", function () { ]); await submitForm(page); await page.waitForNavigation(); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/nest/with-emailpassword/test/basic.test.js b/examples/nest/with-emailpassword/test/basic.test.js index 010816c0e..5a4fc5f87 100644 --- a/examples/nest/with-emailpassword/test/basic.test.js +++ b/examples/nest/with-emailpassword/test/basic.test.js @@ -80,7 +80,8 @@ describe("SuperTokens Example Basic tests", function () { ]); await submitForm(page); await page.waitForNavigation(); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".sessionButton"); let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); diff --git a/examples/next/with-emailpassword/config/appInfo.ts b/examples/next/with-emailpassword/config/appInfo.ts index e148fa890..ed09e30c7 100644 --- a/examples/next/with-emailpassword/config/appInfo.ts +++ b/examples/next/with-emailpassword/config/appInfo.ts @@ -2,7 +2,12 @@ const port = process.env.APP_PORT || 3000; const apiBasePath = "/api/auth/"; -export const websiteDomain = process.env.APP_URL || process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${port}`; +export const websiteDomain = + process.env.APP_URL || + process.env.NEXT_PUBLIC_APP_URL || + process.env.DEPLOY_URL || + process.env.URL || + `http://localhost:${port}`; export const appInfo = { appName: "SuperTokens Demo App", diff --git a/examples/next/with-emailpassword/netlify.toml b/examples/next/with-emailpassword/netlify.toml new file mode 100644 index 000000000..00649df24 --- /dev/null +++ b/examples/next/with-emailpassword/netlify.toml @@ -0,0 +1,4 @@ +[build] + command = "NEXT_PUBLIC_APP_URL=$DEPLOY_URL npm run build" + functions = "netlify/functions" + publish = ".next" diff --git a/examples/next/with-emailpassword/package-lock.json b/examples/next/with-emailpassword/package-lock.json index e702c675b..8a5aa6b80 100644 --- a/examples/next/with-emailpassword/package-lock.json +++ b/examples/next/with-emailpassword/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "hasInstallScript": true, "dependencies": { "next": "latest", "react": "^18.2.0", diff --git a/examples/next/with-emailpassword/package.json b/examples/next/with-emailpassword/package.json index 7ea83a771..462e4d850 100644 --- a/examples/next/with-emailpassword/package.json +++ b/examples/next/with-emailpassword/package.json @@ -1,6 +1,7 @@ { "private": true, "scripts": { + "postinstall": "../../../updateLibInExample", "dev": "next dev", "build": "next build", "start": "next start" diff --git a/examples/next/with-emailpassword/test/basic.test.js b/examples/next/with-emailpassword/test/basic.test.js index 25087c794..3708671c8 100644 --- a/examples/next/with-emailpassword/test/basic.test.js +++ b/examples/next/with-emailpassword/test/basic.test.js @@ -34,8 +34,19 @@ const EmailPassword = require("supertokens-node/recipe/emailpassword"); // Run the tests in a DOM environment. require("jsdom-global")(); -const apiDomain = "http://localhost:3001"; -const websiteDomain = "http://localhost:3000"; +let deployInfo; + +if (process.env.TEST_DEPLOYED_VERSION) { + deployInfo = require("../deployInfo.json"); + + if (!deployInfo.deploy_url) { + throw new Error("Deployment failed or json error. " + JSON.stringify(deployInfo)); + } +} + +const apiDomain = deployInfo?.deploy_url ?? "http://localhost:3000"; +const websiteDomain = deployInfo?.deploy_url ?? "http://localhost:3000"; + SuperTokensNode.init({ supertokens: { // We are running these tests without running a local ST instance @@ -80,8 +91,19 @@ describe("SuperTokens Example Basic tests", function () { ]); await submitForm(page); await page.waitForNavigation(); - const user = await EmailPassword.getUserByEmail("public", email); + const userList = await SuperTokensNode.listUsersByAccountInfo("public", { email }); + const user = userList[0]; const callApiBtn = await page.waitForSelector(".ProtectedHome_sessionButton__ihFAK"); + + // we save the cookies.. + let originalCookies = (await page._client.send("Network.getAllCookies")).cookies; + + // we set the old cookies with invalid access token + originalCookies = originalCookies.map((c) => + c.name === "sAccessToken" || c.name === "st-access-token" ? { ...c, value: "broken" } : c + ); + await page.setCookie(...originalCookies); + let setAlertContent; let alertContent = new Promise((res) => (setAlertContent = res)); page.on("dialog", async (dialog) => { diff --git a/framework/custom/index.d.ts b/framework/custom/index.d.ts new file mode 100644 index 000000000..0279b7e19 --- /dev/null +++ b/framework/custom/index.d.ts @@ -0,0 +1,3 @@ +export * from "../../lib/build/framework/custom"; +import * as _default from "../../lib/build/framework/custom"; +export default _default; diff --git a/framework/custom/index.js b/framework/custom/index.js new file mode 100644 index 000000000..2cb267700 --- /dev/null +++ b/framework/custom/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/framework/custom")); diff --git a/lib/build/framework/custom/framework.d.ts b/lib/build/framework/custom/framework.d.ts new file mode 100644 index 000000000..82e383dae --- /dev/null +++ b/lib/build/framework/custom/framework.d.ts @@ -0,0 +1,109 @@ +// @ts-nocheck +import type { HTTPMethod } from "../../types"; +import { BaseRequest } from "../request"; +import { BaseResponse } from "../response"; +import { SessionContainerInterface } from "../../recipe/session/types"; +declare type RequestInfo = { + url: string; + method: HTTPMethod; + headers: Headers; + cookies: Record; + query: Record; + getJSONBody: () => Promise; + getFormBody: () => Promise; + setSession?: (session: SessionContainerInterface) => void; +}; +export declare class PreParsedRequest extends BaseRequest { + private request; + private _session?; + get session(): SessionContainerInterface | undefined; + set session(value: SessionContainerInterface | undefined); + constructor(request: RequestInfo); + getFormData: () => Promise; + getKeyValueFromQuery: (key: string) => string | undefined; + getJSONBody: () => Promise; + getMethod: () => HTTPMethod; + getCookieValue: (key: string) => string | undefined; + getHeaderValue: (key: string) => string | undefined; + getOriginalURL: () => string; +} +export declare type CookieInfo = { + key: string; + value: string; + domain: string | undefined; + secure: boolean; + httpOnly: boolean; + expires: number; + path: string; + sameSite: "strict" | "lax" | "none"; +}; +export declare class CollectingResponse extends BaseResponse { + statusCode: number; + readonly headers: Headers; + readonly cookies: CookieInfo[]; + body?: string; + constructor(); + sendHTMLResponse: (html: string) => void; + setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; + removeHeader: (key: string) => void; + setCookie: ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: "strict" | "lax" | "none" + ) => void; + /** + * @param {number} statusCode + */ + setStatusCode: (statusCode: number) => void; + sendJSONResponse: (content: any) => void; +} +export declare type NextFunction = (err?: any) => void; +export declare const middleware: ( + wrapRequest?: (req: OrigReqType) => BaseRequest, + wrapResponse?: (req: OrigRespType) => BaseResponse +) => ( + request: OrigReqType, + response: OrigRespType, + next?: NextFunction | undefined +) => Promise< + | { + handled: boolean; + error?: undefined; + } + | { + error: any; + handled?: undefined; + } +>; +export declare const errorHandler: () => ( + err: any, + request: BaseRequest, + response: BaseResponse, + next: NextFunction +) => Promise; +export declare const CustomFrameworkWrapper: { + middleware: ( + wrapRequest?: (req: OrigReqType) => BaseRequest, + wrapResponse?: (req: OrigRespType) => BaseResponse + ) => ( + request: OrigReqType, + response: OrigRespType, + next?: NextFunction | undefined + ) => Promise< + | { + handled: boolean; + error?: undefined; + } + | { + error: any; + handled?: undefined; + } + >; + errorHandler: () => (err: any, request: BaseRequest, response: BaseResponse, next: NextFunction) => Promise; +}; +export {}; diff --git a/lib/build/framework/custom/framework.js b/lib/build/framework/custom/framework.js new file mode 100644 index 000000000..91f4ada97 --- /dev/null +++ b/lib/build/framework/custom/framework.js @@ -0,0 +1,162 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CustomFrameworkWrapper = exports.errorHandler = exports.middleware = exports.CollectingResponse = exports.PreParsedRequest = void 0; +const utils_1 = require("../../utils"); +const request_1 = require("../request"); +const response_1 = require("../response"); +const supertokens_1 = __importDefault(require("../../supertokens")); +class PreParsedRequest extends request_1.BaseRequest { + constructor(request) { + super(); + this.getFormData = async () => { + return this.request.getFormBody(); + }; + this.getKeyValueFromQuery = (key) => { + if (this.request.query === undefined) { + return undefined; + } + let value = this.request.query[key]; + if (value === undefined || typeof value !== "string") { + return undefined; + } + return value; + }; + this.getJSONBody = async () => { + return this.request.getJSONBody(); + }; + this.getMethod = () => { + return utils_1.normaliseHttpMethod(this.request.method); + }; + this.getCookieValue = (key) => { + return this.request.cookies[key]; + }; + this.getHeaderValue = (key) => { + var _a; + return (_a = this.request.headers.get(key)) !== null && _a !== void 0 ? _a : undefined; + }; + this.getOriginalURL = () => { + return this.request.url; + }; + this.original = request; + this.request = request; + } + get session() { + return this._session; + } + set session(value) { + this._session = value; + if (value !== undefined && this.request.setSession !== undefined) { + this.request.setSession(value); + } + } +} +exports.PreParsedRequest = PreParsedRequest; +class CollectingResponse extends response_1.BaseResponse { + constructor() { + super(); + this.sendHTMLResponse = (html) => { + this.headers.set("Content-Type", "text/html"); + this.body = html; + }; + this.setHeader = (key, value, allowDuplicateKey) => { + if (allowDuplicateKey) { + this.headers.append(key, value); + } else { + this.headers.set(key, value); + } + }; + this.removeHeader = (key) => { + this.headers.delete(key); + }; + this.setCookie = (key, value, domain, secure, httpOnly, expires, path, sameSite) => { + this.cookies.push({ key, value, domain, secure, httpOnly, expires, path, sameSite }); + }; + /** + * @param {number} statusCode + */ + this.setStatusCode = (statusCode) => { + this.statusCode = statusCode; + }; + this.sendJSONResponse = (content) => { + this.headers.set("Content-Type", "application/json"); + this.body = JSON.stringify(content); + }; + this.headers = new Headers(); + this.statusCode = 200; + this.cookies = []; + } +} +exports.CollectingResponse = CollectingResponse; +const identity = (i) => i; +const middleware = (wrapRequest = identity, wrapResponse = identity) => { + return async (request, response, next) => { + const wrappedReq = wrapRequest(request); + const wrappedResp = wrapResponse(response); + let supertokens; + const userContext = utils_1.makeDefaultUserContextFromAPI(wrappedReq); + try { + supertokens = supertokens_1.default.getInstanceOrThrowError(); + const result = await supertokens.middleware(wrappedReq, wrappedResp, userContext); + if (!result) { + if (next) { + next(); + } + return { handled: false }; + } + return { handled: true }; + } catch (err) { + if (supertokens) { + try { + await supertokens.errorHandler(err, wrappedReq, wrappedResp, userContext); + return { handled: true }; + } catch (_a) { + if (next) { + next(err); + } + return { error: err }; + } + } else { + if (next) { + next(err); + } + return { error: err }; + } + } + }; +}; +exports.middleware = middleware; +const errorHandler = () => { + return async (err, request, response, next) => { + let supertokens = supertokens_1.default.getInstanceOrThrowError(); + const userContext = utils_1.makeDefaultUserContextFromAPI(request); + try { + await supertokens.errorHandler(err, request, response, userContext); + } catch (err) { + return next(err); + } + }; +}; +exports.errorHandler = errorHandler; +exports.CustomFrameworkWrapper = { + middleware: exports.middleware, + errorHandler: exports.errorHandler, +}; diff --git a/lib/build/framework/custom/index.d.ts b/lib/build/framework/custom/index.d.ts new file mode 100644 index 000000000..7c3b1f1fa --- /dev/null +++ b/lib/build/framework/custom/index.d.ts @@ -0,0 +1,25 @@ +// @ts-nocheck +export { PreParsedRequest, CollectingResponse } from "./framework"; +export declare const middleware: ( + wrapRequest?: (req: OrigReqType) => import("..").BaseRequest, + wrapResponse?: (req: OrigRespType) => import("..").BaseResponse +) => ( + request: OrigReqType, + response: OrigRespType, + next?: import("./framework").NextFunction | undefined +) => Promise< + | { + handled: boolean; + error?: undefined; + } + | { + error: any; + handled?: undefined; + } +>; +export declare const errorHandler: () => ( + err: any, + request: import("..").BaseRequest, + response: import("..").BaseResponse, + next: import("./framework").NextFunction +) => Promise; diff --git a/lib/build/framework/custom/index.js b/lib/build/framework/custom/index.js new file mode 100644 index 000000000..31170e351 --- /dev/null +++ b/lib/build/framework/custom/index.js @@ -0,0 +1,33 @@ +"use strict"; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.errorHandler = exports.middleware = exports.CollectingResponse = exports.PreParsedRequest = void 0; +const framework_1 = require("./framework"); +var framework_2 = require("./framework"); +Object.defineProperty(exports, "PreParsedRequest", { + enumerable: true, + get: function () { + return framework_2.PreParsedRequest; + }, +}); +Object.defineProperty(exports, "CollectingResponse", { + enumerable: true, + get: function () { + return framework_2.CollectingResponse; + }, +}); +exports.middleware = framework_1.CustomFrameworkWrapper.middleware; +exports.errorHandler = framework_1.CustomFrameworkWrapper.errorHandler; diff --git a/lib/build/framework/fastify/index.d.ts b/lib/build/framework/fastify/index.d.ts index 3cca3a1a2..fb2d3fe21 100644 --- a/lib/build/framework/fastify/index.d.ts +++ b/lib/build/framework/fastify/index.d.ts @@ -1,18 +1,21 @@ // @ts-nocheck /// export type { SessionRequest } from "./framework"; -export declare const plugin: import("fastify").FastifyPluginCallback, import("http").Server>; +export declare const plugin: import("fastify").FastifyPluginCallback< + Record, + import("fastify").RawServerDefault +>; export declare const errorHandler: () => ( err: any, req: import("fastify").FastifyRequest< import("fastify/types/route").RouteGenericInterface, - import("http").Server, + import("fastify").RawServerDefault, import("http").IncomingMessage >, res: import("fastify").FastifyReply< - import("http").Server, + import("fastify").RawServerDefault, import("http").IncomingMessage, - import("http").ServerResponse, + import("http").ServerResponse, import("fastify/types/route").RouteGenericInterface, unknown > diff --git a/lib/build/framework/types.d.ts b/lib/build/framework/types.d.ts index f685b5e0a..9ea5d4ea5 100644 --- a/lib/build/framework/types.d.ts +++ b/lib/build/framework/types.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -export declare type TypeFramework = "express" | "fastify" | "hapi" | "loopback" | "koa" | "awsLambda"; +export declare type TypeFramework = "express" | "fastify" | "hapi" | "loopback" | "koa" | "awsLambda" | "custom"; import { BaseRequest, BaseResponse } from "."; export declare let SchemaFramework: { type: string; diff --git a/lib/build/framework/types.js b/lib/build/framework/types.js index 282217eef..9a77275ad 100644 --- a/lib/build/framework/types.js +++ b/lib/build/framework/types.js @@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.SchemaFramework = void 0; exports.SchemaFramework = { type: "string", - enum: ["express", "fastify", "hapi", "loopback", "koa", "awsLambda"], + enum: ["express", "fastify", "hapi", "loopback", "koa", "awsLambda", "custom"], }; diff --git a/lib/build/framework/utils.d.ts b/lib/build/framework/utils.d.ts index 636a97724..123ddf23c 100644 --- a/lib/build/framework/utils.d.ts +++ b/lib/build/framework/utils.d.ts @@ -45,7 +45,7 @@ export declare function setCookieForServerResponse( expires: number, path: string, sameSite: "strict" | "lax" | "none" -): ServerResponse; +): ServerResponse; export declare function getCookieValueToSetInHeader( prev: string | string[] | undefined, val: string | string[], diff --git a/lib/build/nextjs.d.ts b/lib/build/nextjs.d.ts index 195a81422..7154203c4 100644 --- a/lib/build/nextjs.d.ts +++ b/lib/build/nextjs.d.ts @@ -1,9 +1,27 @@ // @ts-nocheck +declare type PartialNextRequest = { + method: string; + url: string; + headers: Headers; + formData: () => any; + json: () => any; + cookies: { + getAll: () => { + name: string; + value: string; + }[]; + }; +}; export default class NextJS { static superTokensNextWrapper( middleware: (next: (middlewareError?: any) => void) => Promise, request: any, response: any ): Promise; + static getAppDirRequestHandler( + NextResponse: typeof Response + ): (req: T) => Promise; } export declare let superTokensNextWrapper: typeof NextJS.superTokensNextWrapper; +export declare let getAppDirRequestHandler: typeof NextJS.getAppDirRequestHandler; +export {}; diff --git a/lib/build/nextjs.js b/lib/build/nextjs.js index adcc99506..6c0306f3c 100644 --- a/lib/build/nextjs.js +++ b/lib/build/nextjs.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.superTokensNextWrapper = void 0; +exports.getAppDirRequestHandler = exports.superTokensNextWrapper = void 0; /* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. * * This software is licensed under the Apache License, Version 2.0 (the @@ -15,7 +15,9 @@ exports.superTokensNextWrapper = void 0; * License for the specific language governing permissions and limitations * under the License. */ +const cookie_1 = require("cookie"); const express_1 = require("./framework/express"); +const custom_1 = require("./framework/custom"); function next(request, response, resolve, reject) { return async function (middlewareError) { if (middlewareError === undefined) { @@ -51,6 +53,49 @@ class NextJS { } }); } + static getAppDirRequestHandler(NextResponse) { + const stMiddleware = custom_1.middleware((req) => { + const query = Object.fromEntries(new URL(req.url).searchParams.entries()); + const cookies = Object.fromEntries(req.cookies.getAll().map((cookie) => [cookie.name, cookie.value])); + return new custom_1.PreParsedRequest({ + method: req.method, + url: req.url, + query: query, + headers: req.headers, + cookies, + getFormBody: () => req.formData(), + getJSONBody: () => req.json(), + }); + }); + return async function handleCall(req) { + const baseResponse = new custom_1.CollectingResponse(); + const { handled, error } = await stMiddleware(req, baseResponse); + if (error) { + throw error; + } + if (!handled) { + return new NextResponse("Not found", { status: 404 }); + } + for (const respCookie of baseResponse.cookies) { + baseResponse.headers.append( + "Set-Cookie", + cookie_1.serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + return new NextResponse(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); + }; + } } exports.default = NextJS; exports.superTokensNextWrapper = NextJS.superTokensNextWrapper; +exports.getAppDirRequestHandler = NextJS.getAppDirRequestHandler; diff --git a/lib/build/querier.js b/lib/build/querier.js index 6546e051b..9daa21bb8 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -1,40 +1,4 @@ "use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -56,7 +20,6 @@ exports.Querier = void 0; * License for the specific language governing permissions and limitations * under the License. */ -const cross_fetch_1 = __importStar(require("cross-fetch")); const utils_1 = require("./utils"); const version_1 = require("./version"); const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); @@ -84,7 +47,7 @@ class Querier { "api-key": Querier.apiKey, }; } - let response = await cross_fetch_1.default(url, { + let response = await utils_1.doFetch(url, { method: "GET", headers, }); @@ -129,7 +92,7 @@ class Querier { if (path.isARecipePath() && this.rIdToCore !== undefined) { headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); } - return cross_fetch_1.default(url, { + return utils_1.doFetch(url, { method: "POST", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -157,7 +120,7 @@ class Querier { const finalURL = new URL(url); const searchParams = new URLSearchParams(params); finalURL.search = searchParams.toString(); - return cross_fetch_1.default(finalURL.toString(), { + return utils_1.doFetch(finalURL.toString(), { method: "DELETE", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -187,7 +150,7 @@ class Querier { Object.entries(params).filter(([_, value]) => value !== undefined) ); finalURL.search = searchParams.toString(); - return await cross_fetch_1.default(finalURL.toString(), { + return utils_1.doFetch(finalURL.toString(), { method: "GET", headers, }); @@ -215,7 +178,7 @@ class Querier { Object.entries(params).filter(([_, value]) => value !== undefined) ); finalURL.search = searchParams.toString(); - return await cross_fetch_1.default(finalURL.toString(), { + return utils_1.doFetch(finalURL.toString(), { method: "GET", headers, }); @@ -238,7 +201,7 @@ class Querier { if (path.isARecipePath() && this.rIdToCore !== undefined) { headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); } - return cross_fetch_1.default(url, { + return utils_1.doFetch(url, { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -297,9 +260,9 @@ class Querier { err.message.includes("ECONNREFUSED") || err.code === "ECONNREFUSED") ) { - return await this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); + return this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); } - if (err instanceof cross_fetch_1.Response) { + if ("status" in err && "text" in err) { if (err.status === constants_1.RATE_LIMIT_STATUS_CODE) { const retriesLeft = retryInfoMap[url]; if (retriesLeft > 0) { @@ -307,7 +270,7 @@ class Querier { const attemptsMade = maxRetries - retriesLeft; const delay = 10 + 250 * attemptsMade; await new Promise((resolve) => setTimeout(resolve, delay)); - return await this.sendRequestHelper(path, method, requestFunc, numberOfTries, retryInfoMap); + return this.sendRequestHelper(path, method, requestFunc, numberOfTries, retryInfoMap); } } throw new Error( diff --git a/lib/build/recipe/dashboard/api/analytics.js b/lib/build/recipe/dashboard/api/analytics.js index 734a1c3be..1a4dba3db 100644 --- a/lib/build/recipe/dashboard/api/analytics.js +++ b/lib/build/recipe/dashboard/api/analytics.js @@ -24,7 +24,7 @@ const querier_1 = require("../../../querier"); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); const version_1 = require("../../../version"); const error_1 = __importDefault(require("../../../error")); -const cross_fetch_1 = __importDefault(require("cross-fetch")); +const utils_1 = require("../../../utils"); async function analyticsPost(_, ___, options, __) { // If telemetry is disabled, dont send any event if (!supertokens_1.default.getInstanceOrThrowError().telemetryEnabled) { @@ -76,7 +76,7 @@ async function analyticsPost(_, ___, options, __) { dashboardVersion, }; try { - await cross_fetch_1.default("https://api.supertokens.com/0/st/telemetry", { + await utils_1.doFetch("https://api.supertokens.com/0/st/telemetry", { method: "POST", body: JSON.stringify(data), headers: { diff --git a/lib/build/recipe/session/framework/custom.d.ts b/lib/build/recipe/session/framework/custom.d.ts new file mode 100644 index 000000000..23c82a5c2 --- /dev/null +++ b/lib/build/recipe/session/framework/custom.d.ts @@ -0,0 +1,10 @@ +// @ts-nocheck +import type { VerifySessionOptions } from ".."; +import { BaseRequest, BaseResponse } from "../../../framework"; +import { NextFunction } from "../../../framework/custom/framework"; +import { SessionContainerInterface } from "../types"; +export declare function verifySession< + T extends BaseRequest & { + session?: SessionContainerInterface; + } +>(options?: VerifySessionOptions): (req: T, res: BaseResponse, next?: NextFunction | undefined) => Promise; diff --git a/lib/build/recipe/session/framework/custom.js b/lib/build/recipe/session/framework/custom.js new file mode 100644 index 000000000..0615fe8b7 --- /dev/null +++ b/lib/build/recipe/session/framework/custom.js @@ -0,0 +1,50 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.verifySession = void 0; +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const recipe_1 = __importDefault(require("../recipe")); +const supertokens_1 = __importDefault(require("../../../supertokens")); +const utils_1 = require("../../../utils"); +function verifySession(options) { + return async (req, res, next) => { + const userContext = utils_1.makeDefaultUserContextFromAPI(req); + try { + const sessionRecipe = recipe_1.default.getInstanceOrThrowError(); + req.session = await sessionRecipe.verifySession(options, req, res, userContext); + if (next !== undefined) { + next(); + } + return undefined; + } catch (err) { + try { + const supertokens = supertokens_1.default.getInstanceOrThrowError(); + await supertokens.errorHandler(err, req, res, userContext); + return undefined; + } catch (_a) { + if (next !== undefined) { + next(err); + } + return err; + } + } + }; +} +exports.verifySession = verifySession; diff --git a/lib/build/recipe/session/sessionRequestFunctions.js b/lib/build/recipe/session/sessionRequestFunctions.js index b5fb40344..fee9b14bc 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.js +++ b/lib/build/recipe/session/sessionRequestFunctions.js @@ -20,11 +20,14 @@ const error_1 = __importDefault(require("./error")); const LEGACY_ID_REFRESH_TOKEN_COOKIE_NAME = "sIdRefreshToken"; async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, options, userContext }) { logger_1.logDebugMessage("getSession: Started"); - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); + const configuredFramework = supertokens_1.default.getInstanceOrThrowError().framework; + if (configuredFramework !== "custom") { + if (!req.wrapperUsed) { + req = framework_1.default[configuredFramework].wrapRequest(req); + } + if (!res.wrapperUsed) { + res = framework_1.default[configuredFramework].wrapResponse(res); + } } userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); logger_1.logDebugMessage("getSession: Wrapping done"); @@ -158,11 +161,14 @@ exports.getSessionFromRequest = getSessionFromRequest; */ async function refreshSessionInRequest({ res, req, userContext, config, recipeInterfaceImpl }) { logger_1.logDebugMessage("refreshSession: Started"); - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); + const configuredFramework = supertokens_1.default.getInstanceOrThrowError().framework; + if (configuredFramework !== "custom") { + if (!req.wrapperUsed) { + req = framework_1.default[configuredFramework].wrapRequest(req); + } + if (!res.wrapperUsed) { + res = framework_1.default[configuredFramework].wrapResponse(res); + } } userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); logger_1.logDebugMessage("refreshSession: Wrapping done"); @@ -324,11 +330,14 @@ async function createNewSessionInRequest({ tenantId, }) { logger_1.logDebugMessage("createNewSession: Started"); - if (!req.wrapperUsed) { - req = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapRequest(req); - } - if (!res.wrapperUsed) { - res = framework_1.default[supertokens_1.default.getInstanceOrThrowError().framework].wrapResponse(res); + const configuredFramework = supertokens_1.default.getInstanceOrThrowError().framework; + if (configuredFramework !== "custom") { + if (!req.wrapperUsed) { + req = framework_1.default[configuredFramework].wrapRequest(req); + } + if (!res.wrapperUsed) { + res = framework_1.default[configuredFramework].wrapResponse(res); + } } logger_1.logDebugMessage("createNewSession: Wrapping done"); userContext = utils_2.setRequestInUserContextIfNotDefined(userContext, req); diff --git a/lib/build/recipe/thirdparty/providers/gitlab.js b/lib/build/recipe/thirdparty/providers/gitlab.js index a7f121c5d..456cb51b7 100644 --- a/lib/build/recipe/thirdparty/providers/gitlab.js +++ b/lib/build/recipe/thirdparty/providers/gitlab.js @@ -20,7 +20,6 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const custom_1 = __importDefault(require("./custom")); -// import fetch from "cross-fetch"; // import NormalisedURLDomain from "../../../normalisedURLDomain"; function Gitlab(input) { if (input.config.name === undefined) { @@ -96,7 +95,7 @@ exports.default = Gitlab; // }) { // let accessToken = accessTokenAPIResponse.access_token; // let authHeader = `Bearer ${accessToken}`; -// let response = await fetch(baseUrl + "/api/v4/user", { +// let response = await doFetch(baseUrl + "/api/v4/user", { // method: "get", // headers: { // Authorization: authHeader, diff --git a/lib/build/recipe/thirdparty/providers/utils.js b/lib/build/recipe/thirdparty/providers/utils.js index a561eedf4..1d6c81d8d 100644 --- a/lib/build/recipe/thirdparty/providers/utils.js +++ b/lib/build/recipe/thirdparty/providers/utils.js @@ -42,11 +42,11 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.discoverOIDCEndpoints = exports.verifyIdTokenFromJWKSEndpointAndGetPayload = exports.doPostRequest = exports.doGetRequest = void 0; -const cross_fetch_1 = __importDefault(require("cross-fetch")); const jose = __importStar(require("jose")); const normalisedURLDomain_1 = __importDefault(require("../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); const logger_1 = require("../../../logger"); +const utils_1 = require("../../../utils"); async function doGetRequest(url, queryParams, headers) { logger_1.logDebugMessage( `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` @@ -56,7 +56,7 @@ async function doGetRequest(url, queryParams, headers) { } const finalURL = new URL(url); finalURL.search = new URLSearchParams(queryParams).toString(); - let response = await cross_fetch_1.default(finalURL.toString(), { + let response = await utils_1.doFetch(finalURL.toString(), { headers: headers, }); const stringResponse = await response.text(); @@ -82,7 +82,7 @@ async function doPostRequest(url, params, headers) { `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` ); const body = new URLSearchParams(params).toString(); - let response = await cross_fetch_1.default(url, { + let response = await utils_1.doFetch(url, { method: "POST", body, headers, diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index 6d796ecce..b7b2b17ed 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -3,6 +3,7 @@ import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject } from "./types import type { BaseRequest, BaseResponse } from "./framework"; import { User } from "./user"; import { SessionContainer } from "./recipe/session"; +export declare const doFetch: typeof fetch; export declare function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined; export declare function maxVersion(version1: string, version2: string): string; export declare function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedAppinfo; diff --git a/lib/build/utils.js b/lib/build/utils.js index 09bb91fbc..656ec8013 100644 --- a/lib/build/utils.js +++ b/lib/build/utils.js @@ -41,13 +41,20 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.doesRequestSupportFDI = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = void 0; +exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.doesRequestSupportFDI = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = exports.doFetch = void 0; const psl = __importStar(require("psl")); const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); const logger_1 = require("./logger"); const constants_1 = require("./constants"); const cross_fetch_1 = __importDefault(require("cross-fetch")); +const doFetch = (...args) => { + if (typeof fetch !== "undefined") { + return fetch(...args); + } + return cross_fetch_1.default(...args); +}; +exports.doFetch = doFetch; function getLargestVersionFromIntersection(v1, v2) { let intersection = v1.filter((value) => v2.indexOf(value) !== -1); if (intersection.length === 0) { @@ -303,7 +310,7 @@ async function postWithFetch(url, headers, body, { successLog, errorLogHeader }) let error; let resp; try { - const fetchResp = await cross_fetch_1.default(url, { + const fetchResp = await exports.doFetch(url, { method: "POST", body: JSON.stringify(body), headers, diff --git a/lib/ts/framework/custom/framework.ts b/lib/ts/framework/custom/framework.ts new file mode 100644 index 000000000..14b48dfa1 --- /dev/null +++ b/lib/ts/framework/custom/framework.ts @@ -0,0 +1,217 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import type { HTTPMethod } from "../../types"; +import { makeDefaultUserContextFromAPI, normaliseHttpMethod } from "../../utils"; +import { BaseRequest } from "../request"; +import { BaseResponse } from "../response"; +import SuperTokens from "../../supertokens"; +import { SessionContainerInterface } from "../../recipe/session/types"; + +type RequestInfo = { + url: string; + method: HTTPMethod; + headers: Headers; + cookies: Record; + query: Record; + getJSONBody: () => Promise; + getFormBody: () => Promise; + setSession?: (session: SessionContainerInterface) => void; +}; + +export class PreParsedRequest extends BaseRequest { + private request: RequestInfo; + + private _session?: SessionContainerInterface | undefined; + public get session(): SessionContainerInterface | undefined { + return this._session; + } + public set session(value: SessionContainerInterface | undefined) { + this._session = value; + if (value !== undefined && this.request.setSession !== undefined) { + this.request.setSession(value); + } + } + + constructor(request: RequestInfo) { + super(); + this.original = request; + this.request = request; + } + + getFormData = async (): Promise => { + return this.request.getFormBody(); + }; + + getKeyValueFromQuery = (key: string): string | undefined => { + if (this.request.query === undefined) { + return undefined; + } + let value = this.request.query[key]; + if (value === undefined || typeof value !== "string") { + return undefined; + } + return value; + }; + + getJSONBody = async (): Promise => { + return this.request.getJSONBody(); + }; + + getMethod = (): HTTPMethod => { + return normaliseHttpMethod(this.request.method); + }; + + getCookieValue = (key: string): string | undefined => { + return this.request.cookies[key]; + }; + + getHeaderValue = (key: string): string | undefined => { + return this.request.headers.get(key) ?? undefined; + }; + + getOriginalURL = (): string => { + return this.request.url; + }; +} + +export type CookieInfo = { + key: string; + value: string; + domain: string | undefined; + secure: boolean; + httpOnly: boolean; + expires: number; + path: string; + sameSite: "strict" | "lax" | "none"; +}; + +export class CollectingResponse extends BaseResponse { + public statusCode: number; + public readonly headers: Headers; + public readonly cookies: CookieInfo[]; + public body?: string; + + constructor() { + super(); + this.headers = new Headers(); + this.statusCode = 200; + this.cookies = []; + } + + sendHTMLResponse = (html: string) => { + this.headers.set("Content-Type", "text/html"); + this.body = html; + }; + + setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { + if (allowDuplicateKey) { + this.headers.append(key, value); + } else { + this.headers.set(key, value); + } + }; + + removeHeader = (key: string) => { + this.headers.delete(key); + }; + + setCookie = ( + key: string, + value: string, + domain: string | undefined, + secure: boolean, + httpOnly: boolean, + expires: number, + path: string, + sameSite: "strict" | "lax" | "none" + ) => { + this.cookies.push({ key, value, domain, secure, httpOnly, expires, path, sameSite }); + }; + + /** + * @param {number} statusCode + */ + setStatusCode = (statusCode: number) => { + this.statusCode = statusCode; + }; + + sendJSONResponse = (content: any) => { + this.headers.set("Content-Type", "application/json"); + this.body = JSON.stringify(content); + }; +} + +export type NextFunction = (err?: any) => void; + +const identity = (i: any) => i; +export const middleware = ( + wrapRequest: (req: OrigReqType) => BaseRequest = identity, + wrapResponse: (req: OrigRespType) => BaseResponse = identity +) => { + return async (request: OrigReqType, response: OrigRespType, next?: NextFunction) => { + const wrappedReq = wrapRequest(request); + const wrappedResp = wrapResponse(response); + let supertokens; + const userContext = makeDefaultUserContextFromAPI(wrappedReq); + + try { + supertokens = SuperTokens.getInstanceOrThrowError(); + const result = await supertokens.middleware(wrappedReq, wrappedResp, userContext); + if (!result) { + if (next) { + next(); + } + return { handled: false }; + } + return { handled: true }; + } catch (err) { + if (supertokens) { + try { + await supertokens.errorHandler(err, wrappedReq, wrappedResp, userContext); + return { handled: true }; + } catch { + if (next) { + next(err); + } + return { error: err }; + } + } else { + if (next) { + next(err); + } + return { error: err }; + } + } + }; +}; + +export const errorHandler = () => { + return async (err: any, request: BaseRequest, response: BaseResponse, next: NextFunction) => { + let supertokens = SuperTokens.getInstanceOrThrowError(); + const userContext = makeDefaultUserContextFromAPI(request); + + try { + await supertokens.errorHandler(err, request, response, userContext); + } catch (err) { + return next(err); + } + }; +}; + +export const CustomFrameworkWrapper = { + middleware, + errorHandler, +}; diff --git a/lib/ts/framework/custom/index.ts b/lib/ts/framework/custom/index.ts new file mode 100644 index 000000000..0f01e9556 --- /dev/null +++ b/lib/ts/framework/custom/index.ts @@ -0,0 +1,20 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +import { CustomFrameworkWrapper } from "./framework"; +export { PreParsedRequest, CollectingResponse } from "./framework"; + +export const middleware = CustomFrameworkWrapper.middleware; +export const errorHandler = CustomFrameworkWrapper.errorHandler; diff --git a/lib/ts/framework/types.ts b/lib/ts/framework/types.ts index 5707c65d5..df3b82a35 100644 --- a/lib/ts/framework/types.ts +++ b/lib/ts/framework/types.ts @@ -12,12 +12,12 @@ * License for the specific language governing permissions and limitations * under the License. */ -export type TypeFramework = "express" | "fastify" | "hapi" | "loopback" | "koa" | "awsLambda"; +export type TypeFramework = "express" | "fastify" | "hapi" | "loopback" | "koa" | "awsLambda" | "custom"; import { BaseRequest, BaseResponse } from "."; export let SchemaFramework = { type: "string", - enum: ["express", "fastify", "hapi", "loopback", "koa", "awsLambda"], + enum: ["express", "fastify", "hapi", "loopback", "koa", "awsLambda", "custom"], }; export interface Framework { diff --git a/lib/ts/nextjs.ts b/lib/ts/nextjs.ts index fc05d6d52..180e7b10b 100644 --- a/lib/ts/nextjs.ts +++ b/lib/ts/nextjs.ts @@ -12,7 +12,10 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { serialize } from "cookie"; import { errorHandler } from "./framework/express"; +import { CollectingResponse, PreParsedRequest, middleware } from "./framework/custom"; +import { HTTPMethod } from "./types"; function next( request: any, response: any, @@ -32,6 +35,21 @@ function next( }); }; } + +type PartialNextRequest = { + method: string; + url: string; + headers: Headers; + formData: () => any; + json: () => any; + cookies: { + getAll: () => { + name: string; + value: string; + }[]; + }; +}; + export default class NextJS { static async superTokensNextWrapper( middleware: (next: (middlewareError?: any) => void) => Promise, @@ -58,5 +76,57 @@ export default class NextJS { } }); } + + static getAppDirRequestHandler(NextResponse: typeof Response) { + const stMiddleware = middleware((req) => { + const query = Object.fromEntries(new URL(req.url).searchParams.entries()); + const cookies: Record = Object.fromEntries( + req.cookies.getAll().map((cookie) => [cookie.name, cookie.value]) + ); + + return new PreParsedRequest({ + method: req.method as HTTPMethod, + url: req.url, + query: query, + headers: req.headers, + cookies, + getFormBody: () => req.formData(), + getJSONBody: () => req.json(), + }); + }); + + return async function handleCall(req: T) { + const baseResponse = new CollectingResponse(); + + const { handled, error } = await stMiddleware(req, baseResponse); + + if (error) { + throw error; + } + if (!handled) { + return new NextResponse("Not found", { status: 404 }); + } + + for (const respCookie of baseResponse.cookies) { + baseResponse.headers.append( + "Set-Cookie", + serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + + return new NextResponse(baseResponse.body, { + headers: baseResponse.headers, + status: baseResponse.statusCode, + }); + }; + } } export let superTokensNextWrapper = NextJS.superTokensNextWrapper; +export let getAppDirRequestHandler = NextJS.getAppDirRequestHandler; diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index a4ba3b334..59ed5f95e 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -12,9 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -import fetch, { Response } from "cross-fetch"; - -import { getLargestVersionFromIntersection } from "./utils"; +import { doFetch, getLargestVersionFromIntersection } from "./utils"; import { cdiSupported } from "./version"; import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; @@ -58,7 +56,7 @@ export class Querier { "api-key": Querier.apiKey, }; } - let response = await fetch(url, { + let response = await doFetch(url, { method: "GET", headers, }); @@ -132,7 +130,7 @@ export class Querier { rid: this.rIdToCore, }; } - return fetch(url, { + return doFetch(url, { method: "POST", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -167,7 +165,7 @@ export class Querier { const searchParams = new URLSearchParams(params); finalURL.search = searchParams.toString(); - return fetch(finalURL.toString(), { + return doFetch(finalURL.toString(), { method: "DELETE", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -206,7 +204,7 @@ export class Querier { Object.entries(params).filter(([_, value]) => value !== undefined) as string[][] ); finalURL.search = searchParams.toString(); - return await fetch(finalURL.toString(), { + return doFetch(finalURL.toString(), { method: "GET", headers, }); @@ -243,7 +241,7 @@ export class Querier { Object.entries(params).filter(([_, value]) => value !== undefined) as string[][] ); finalURL.search = searchParams.toString(); - return await fetch(finalURL.toString(), { + return doFetch(finalURL.toString(), { method: "GET", headers, }); @@ -273,7 +271,7 @@ export class Querier { }; } - return fetch(url, { + return doFetch(url, { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -350,10 +348,10 @@ export class Querier { err.message.includes("ECONNREFUSED") || err.code === "ECONNREFUSED") ) { - return await this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); + return this.sendRequestHelper(path, method, requestFunc, numberOfTries - 1, retryInfoMap); } - if (err instanceof Response) { + if ("status" in err && "text" in err) { if (err.status === RATE_LIMIT_STATUS_CODE) { const retriesLeft = retryInfoMap[url]; @@ -365,7 +363,7 @@ export class Querier { await new Promise((resolve) => setTimeout(resolve, delay)); - return await this.sendRequestHelper(path, method, requestFunc, numberOfTries, retryInfoMap); + return this.sendRequestHelper(path, method, requestFunc, numberOfTries, retryInfoMap); } } diff --git a/lib/ts/recipe/dashboard/api/analytics.ts b/lib/ts/recipe/dashboard/api/analytics.ts index 12a8f4986..720ba8783 100644 --- a/lib/ts/recipe/dashboard/api/analytics.ts +++ b/lib/ts/recipe/dashboard/api/analytics.ts @@ -19,7 +19,7 @@ import { Querier } from "../../../querier"; import NormalisedURLPath from "../../../normalisedURLPath"; import { version as SDKVersion } from "../../../version"; import STError from "../../../error"; -import fetch from "cross-fetch"; +import { doFetch } from "../../../utils"; export type Response = { status: "OK"; @@ -88,7 +88,7 @@ export default async function analyticsPost( }; try { - await fetch("https://api.supertokens.com/0/st/telemetry", { + await doFetch("https://api.supertokens.com/0/st/telemetry", { method: "POST", body: JSON.stringify(data), headers: { diff --git a/lib/ts/recipe/session/framework/custom.ts b/lib/ts/recipe/session/framework/custom.ts new file mode 100644 index 000000000..65db71ffd --- /dev/null +++ b/lib/ts/recipe/session/framework/custom.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +import Session from "../recipe"; +import type { VerifySessionOptions } from ".."; +import SuperTokens from "../../../supertokens"; +import { makeDefaultUserContextFromAPI } from "../../../utils"; +import { BaseRequest, BaseResponse } from "../../../framework"; +import { NextFunction } from "../../../framework/custom/framework"; +import { SessionContainerInterface } from "../types"; + +export function verifySession( + options?: VerifySessionOptions +) { + return async (req: T, res: BaseResponse, next?: NextFunction) => { + const userContext = makeDefaultUserContextFromAPI(req); + + try { + const sessionRecipe = Session.getInstanceOrThrowError(); + req.session = await sessionRecipe.verifySession(options, req, res, userContext); + if (next !== undefined) { + next(); + } + return undefined; + } catch (err) { + try { + const supertokens = SuperTokens.getInstanceOrThrowError(); + await supertokens.errorHandler(err, req, res, userContext); + return undefined; + } catch { + if (next !== undefined) { + next(err); + } + return err; + } + } + }; +} diff --git a/lib/ts/recipe/session/sessionRequestFunctions.ts b/lib/ts/recipe/session/sessionRequestFunctions.ts index 0aa25b946..0f58a948e 100644 --- a/lib/ts/recipe/session/sessionRequestFunctions.ts +++ b/lib/ts/recipe/session/sessionRequestFunctions.ts @@ -38,11 +38,15 @@ export async function getSessionFromRequest({ userContext?: any; }): Promise { logDebugMessage("getSession: Started"); - if (!res.wrapperUsed) { - res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req); + const configuredFramework = SuperTokens.getInstanceOrThrowError().framework; + if (configuredFramework !== "custom") { + if (!req.wrapperUsed) { + req = frameworks[configuredFramework].wrapRequest(req); + } + + if (!res.wrapperUsed) { + res = frameworks[configuredFramework].wrapResponse(res); + } } userContext = setRequestInUserContextIfNotDefined(userContext, req); logDebugMessage("getSession: Wrapping done"); @@ -202,11 +206,15 @@ export async function refreshSessionInRequest({ recipeInterfaceImpl: RecipeInterface; }) { logDebugMessage("refreshSession: Started"); - if (!res.wrapperUsed) { - res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res); - } - if (!req.wrapperUsed) { - req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req); + const configuredFramework = SuperTokens.getInstanceOrThrowError().framework; + if (configuredFramework !== "custom") { + if (!req.wrapperUsed) { + req = frameworks[configuredFramework].wrapRequest(req); + } + + if (!res.wrapperUsed) { + res = frameworks[configuredFramework].wrapResponse(res); + } } userContext = setRequestInUserContextIfNotDefined(userContext, req); logDebugMessage("refreshSession: Wrapping done"); @@ -365,13 +373,17 @@ export async function createNewSessionInRequest({ tenantId: string; }) { logDebugMessage("createNewSession: Started"); - if (!req.wrapperUsed) { - req = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapRequest(req); - } + const configuredFramework = SuperTokens.getInstanceOrThrowError().framework; + if (configuredFramework !== "custom") { + if (!req.wrapperUsed) { + req = frameworks[configuredFramework].wrapRequest(req); + } - if (!res.wrapperUsed) { - res = frameworks[SuperTokens.getInstanceOrThrowError().framework].wrapResponse(res); + if (!res.wrapperUsed) { + res = frameworks[configuredFramework].wrapResponse(res); + } } + logDebugMessage("createNewSession: Wrapping done"); userContext = setRequestInUserContextIfNotDefined(userContext, req); diff --git a/lib/ts/recipe/thirdparty/providers/gitlab.ts b/lib/ts/recipe/thirdparty/providers/gitlab.ts index ef8552386..a390be591 100644 --- a/lib/ts/recipe/thirdparty/providers/gitlab.ts +++ b/lib/ts/recipe/thirdparty/providers/gitlab.ts @@ -15,7 +15,6 @@ import { TypeProvider, ProviderInput } from "../types"; import NewProvider from "./custom"; -// import fetch from "cross-fetch"; // import NormalisedURLDomain from "../../../normalisedURLDomain"; export default function Gitlab(input: ProviderInput): TypeProvider { @@ -102,7 +101,7 @@ export default function Gitlab(input: ProviderInput): TypeProvider { // }) { // let accessToken = accessTokenAPIResponse.access_token; // let authHeader = `Bearer ${accessToken}`; -// let response = await fetch(baseUrl + "/api/v4/user", { +// let response = await doFetch(baseUrl + "/api/v4/user", { // method: "get", // headers: { // Authorization: authHeader, diff --git a/lib/ts/recipe/thirdparty/providers/utils.ts b/lib/ts/recipe/thirdparty/providers/utils.ts index 8f772a7a5..e1ed8a685 100644 --- a/lib/ts/recipe/thirdparty/providers/utils.ts +++ b/lib/ts/recipe/thirdparty/providers/utils.ts @@ -1,10 +1,10 @@ -import fetch from "cross-fetch"; import * as jose from "jose"; import { ProviderConfigForClientType } from "../types"; import NormalisedURLDomain from "../../../normalisedURLDomain"; import NormalisedURLPath from "../../../normalisedURLPath"; import { logDebugMessage } from "../../../logger"; +import { doFetch } from "../../../utils"; export async function doGetRequest( url: string, @@ -26,7 +26,7 @@ export async function doGetRequest( } const finalURL = new URL(url); finalURL.search = new URLSearchParams(queryParams).toString(); - let response = await fetch(finalURL.toString(), { + let response = await doFetch(finalURL.toString(), { headers: headers, }); @@ -66,7 +66,7 @@ export async function doPostRequest( ); const body = new URLSearchParams(params).toString(); - let response = await fetch(url, { + let response = await doFetch(url, { method: "POST", body, headers, diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts index e620e2b2a..d3771ff3b 100644 --- a/lib/ts/utils.ts +++ b/lib/ts/utils.ts @@ -6,10 +6,17 @@ import NormalisedURLPath from "./normalisedURLPath"; import type { BaseRequest, BaseResponse } from "./framework"; import { logDebugMessage } from "./logger"; import { HEADER_FDI, HEADER_RID } from "./constants"; -import fetch from "cross-fetch"; +import crossFetch from "cross-fetch"; import { User } from "./user"; import { SessionContainer } from "./recipe/session"; +export const doFetch: typeof fetch = (...args) => { + if (typeof fetch !== "undefined") { + return fetch(...args); + } + return crossFetch(...args); +}; + export function getLargestVersionFromIntersection(v1: string[], v2: string[]): string | undefined { let intersection = v1.filter((value) => v2.indexOf(value) !== -1); if (intersection.length === 0) { @@ -299,7 +306,7 @@ export async function postWithFetch( let error; let resp: { status: number; body: any }; try { - const fetchResp = await fetch(url, { + const fetchResp = await doFetch(url, { method: "POST", body: JSON.stringify(body), headers, diff --git a/recipe/session/framework/custom/index.d.ts b/recipe/session/framework/custom/index.d.ts new file mode 100644 index 000000000..737d51302 --- /dev/null +++ b/recipe/session/framework/custom/index.d.ts @@ -0,0 +1,3 @@ +export * from "../../../../lib/build/recipe/session/framework/custom"; +import * as _default from "../../../../lib/build/recipe/session/framework/custom"; +export default _default; diff --git a/recipe/session/framework/custom/index.js b/recipe/session/framework/custom/index.js new file mode 100644 index 000000000..92a33d0b5 --- /dev/null +++ b/recipe/session/framework/custom/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../../lib/build/recipe/session/framework/custom")); diff --git a/test/framework/custom.test.js b/test/framework/custom.test.js new file mode 100644 index 000000000..ad842219c --- /dev/null +++ b/test/framework/custom.test.js @@ -0,0 +1,193 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse } = require("../utils"); +let assert = require("assert"); +let { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +let SuperTokens = require("../.."); +let CustomFramework = require("../../framework/custom"); +let Session = require("../../recipe/session"); +let { verifySession } = require("../../recipe/session/framework/custom"); + +describe(`Custom framework: ${printPath("[test/framework/custom.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + afterEach(async function () { + try { + await this.server.close(); + } catch (err) {} + }); + after(async function () { + await killAllST(); + await cleanST(); + }); + + // - check if session verify middleware responds with a nice error even without the global error handler + it("test session verify middleware without error handler added", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + framework: "custom", + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], + }); + + const req = new CustomFramework.PreParsedRequest({ + method: "get", + url: "/verify", + query: {}, + headers: new Headers(), + cookies: {}, + getFormBody: () => {}, + getJSONBody: () => {}, + }); + const resp = new CustomFramework.CollectingResponse(); + + const verifyResult = await verifySession()(req, resp); + + assert.strictEqual(verifyResult, undefined); + assert.strictEqual(resp.statusCode, 401); + assert.deepStrictEqual(JSON.parse(resp.body), { message: "unauthorised" }); + }); + + // check basic usage of session + it("test basic usage of sessions", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + framework: "custom", + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], + }); + + const middleware = CustomFramework.middleware(); + + const req = new CustomFramework.PreParsedRequest({ + method: "get", + url: "/session/create", + query: {}, + headers: new Headers(), + cookies: {}, + getFormBody: () => {}, + getJSONBody: () => {}, + }); + const resp = new CustomFramework.CollectingResponse(); + + await Session.createNewSession(req, resp, "public", SuperTokens.convertToRecipeUserId("testUserId")); + + let res = extractInfoFromResponse(resp); + + assert(res.accessToken !== undefined); + assert(res.antiCsrf !== undefined); + assert(res.refreshToken !== undefined); + + const req2 = new CustomFramework.PreParsedRequest({ + method: "get", + url: "/session/refresh", + query: {}, + headers: new Headers([["anti-csrf", res.antiCsrf]]), + cookies: { + sAccessToken: res.accessToken, + sRefreshToken: res.refreshToken, + }, + getFormBody: () => {}, + getJSONBody: () => {}, + }); + const resp2 = new CustomFramework.CollectingResponse(); + + let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); + assert(verifyState3 === undefined); + + await Session.refreshSession(req2, resp2); + let res2 = extractInfoFromResponse(resp2); + + assert(res2.accessToken !== undefined); + assert(res2.antiCsrf !== undefined); + assert(res2.refreshToken !== undefined); + + const req3 = new CustomFramework.PreParsedRequest({ + method: "get", + url: "/session/verify", + query: {}, + headers: new Headers(), + cookies: { + sAccessToken: res2.accessToken, + }, + getFormBody: () => {}, + getJSONBody: () => {}, + }); + const resp3 = new CustomFramework.CollectingResponse(); + await verifySession()(req3, resp3); + + let res3 = extractInfoFromResponse(resp3); + let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); + + assert(verifyState !== undefined); + assert(res3.accessToken !== undefined); + + ProcessState.getInstance().reset(); + + const req4 = new CustomFramework.PreParsedRequest({ + method: "get", + url: "/session/verify", + query: {}, + headers: new Headers(), + cookies: { + sAccessToken: res3.accessToken, + }, + getFormBody: () => {}, + getJSONBody: () => {}, + }); + const resp4 = new CustomFramework.CollectingResponse(); + await verifySession()(req4, resp4); + let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); + assert(verifyState2 === undefined); + + const req5 = new CustomFramework.PreParsedRequest({ + method: "get", + url: "/session/verify", + query: {}, + headers: new Headers(), + cookies: { + sAccessToken: res3.accessToken, + }, + getFormBody: () => {}, + getJSONBody: () => {}, + }); + const resp5 = new CustomFramework.CollectingResponse(); + await verifySession()(req5, resp5); + await req5.session.revokeSession(); + let sessionRevokedResponseExtracted = extractInfoFromResponse(resp5); + assert.strictEqual(sessionRevokedResponseExtracted.accessTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); + assert.strictEqual(sessionRevokedResponseExtracted.refreshTokenExpiry, "Thu, 01 Jan 1970 00:00:00 GMT"); + assert.strictEqual(sessionRevokedResponseExtracted.accessToken, ""); + assert.strictEqual(sessionRevokedResponseExtracted.refreshToken, ""); + }); +}); diff --git a/test/utils.js b/test/utils.js index 42ab19a96..39bbf39ed 100644 --- a/test/utils.js +++ b/test/utils.js @@ -41,6 +41,7 @@ const { join } = require("path"); const users = require("./users.json"); let assert = require("assert"); +const { CollectingResponse } = require("../framework/custom"); module.exports.printPath = function (path) { return `${createFormat([consoleOptions.yellow, consoleOptions.italic, consoleOptions.dim])}${path}${createFormat([ @@ -83,7 +84,7 @@ module.exports.setKeyValueInConfig = async function (key, value) { }; module.exports.extractInfoFromResponse = function (res) { - let antiCsrf = res.headers["anti-csrf"]; + let headers; let accessToken = undefined; let refreshToken = undefined; let accessTokenExpiry = undefined; @@ -95,49 +96,69 @@ module.exports.extractInfoFromResponse = function (res) { let accessTokenHttpOnly = false; let idRefreshTokenHttpOnly = false; let refreshTokenHttpOnly = false; - let frontToken = res.headers["front-token"]; - let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; - cookies = cookies === undefined ? [] : cookies; - if (!Array.isArray(cookies)) { - cookies = [cookies]; - } - cookies.forEach((i) => { - if (i.split(";")[0].split("=")[0] === "sAccessToken") { - /** - * if token is sAccessToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0=.eyJzZXNzaW9uSGFuZGxlIjoiMWI4NDBhOTAtMjVmYy00ZjQ4LWE2YWMtMDc0MDIzZjNjZjQwIiwidXNlcklkIjoiIiwicmVmcmVzaFRva2VuSGFzaDEiOiJjYWNhZDNlMGNhMDVkNzRlNWYzNTc4NmFlMGQ2MzJjNDhmMTg1YmZmNmUxNThjN2I2OThkZDYwMzA1NzAyYzI0IiwidXNlckRhdGEiOnt9LCJhbnRpQ3NyZlRva2VuIjoiYTA2MjRjYWItZmIwNy00NTFlLWJmOTYtNWQ3YzU2MjMwZTE4IiwiZXhwaXJ5VGltZSI6MTYyNjUxMjM3NDU4NiwidGltZUNyZWF0ZWQiOjE2MjY1MDg3NzQ1ODYsImxtcnQiOjE2MjY1MDg3NzQ1ODZ9.f1sCkjt0OduS6I6FBQDBLV5zhHXpCU2GXnbe+8OCU6HKG00TX5CM8AyFlOlqzSHABZ7jES/+5k0Ff/rdD34cczlNqICcC4a23AjJg2a097rFrh8/8V7J5fr4UrHLIM4ojZNFz1NyVyDK/ooE6I7soHshEtEVr2XsnJ4q3d+fYs2wwx97PIT82hfHqgbRAzvlv952GYt+OH4bWQE4vTzDqGN7N2OKpn9l2fiCB1Ytzr3ocHRqKuQ8f6xW1n575Q1sSs9F9TtD7lrKfFQH+//6lyKFe2Q1SDc7YU4pE5Cy9Kc/LiqiTU+gsGIJL5qtMzUTG4lX38ugF4QDyNjDBMqCKw==; Max-Age=3599; Expires=Sat, 17 Jul 2021 08:59:34 GMT; Secure; HttpOnly; SameSite=Lax; Path=/' - * i.split(";")[0].split("=")[1] will result in eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0 - */ - accessToken = decodeURIComponent(i.split(";")[0].split("=").slice(1).join("=")); - if (i.split(";")[2].includes("Expires=")) { - accessTokenExpiry = i.split(";")[2].split("=")[1]; - } else if (i.split(";")[2].includes("expires=")) { - accessTokenExpiry = i.split(";")[2].split("=")[1]; - } else { - accessTokenExpiry = i.split(";")[3].split("=")[1]; - } - if (i.split(";")[1].includes("Domain=")) { - accessTokenDomain = i.split(";")[1].split("=")[1]; - } - accessTokenHttpOnly = i.split(";").findIndex((j) => j.includes("HttpOnly")) !== -1; - } else if (i.split(";")[0].split("=")[0] === "sRefreshToken") { - refreshToken = i.split(";")[0].split("=").slice(1).join("="); - if (i.split(";")[2].includes("Expires=")) { - refreshTokenExpiry = i.split(";")[2].split("=")[1]; - } else if (i.split(";")[2].includes("expires=")) { - refreshTokenExpiry = i.split(";")[2].split("=")[1]; - } else { - refreshTokenExpiry = i.split(";")[3].split("=")[1]; - } - if (i.split(";")[1].includes("Domain=")) { - refreshTokenDomain = i.split(";")[1].split("=").slice(1).join("="); - } - refreshTokenHttpOnly = i.split(";").findIndex((j) => j.includes("HttpOnly")) !== -1; + if (res instanceof CollectingResponse) { + const accessTokenCookie = res.cookies.find((info) => info.key === "sAccessToken"); + if (accessTokenCookie) { + accessToken = accessTokenCookie.value; + accessTokenExpiry = new Date(accessTokenCookie.expires).toUTCString(); + accessTokenDomain = accessTokenCookie.domain; + accessTokenHttpOnly = accessTokenCookie.httpOnly; } - }); + const refreshTokenCookie = res.cookies.find((info) => info.key === "sRefreshToken"); + if (refreshTokenCookie) { + refreshToken = refreshTokenCookie.value; + refreshTokenExpiry = new Date(refreshTokenCookie.expires).toUTCString(); + refreshTokenDomain = refreshTokenCookie.domain; + refreshTokenHttpOnly = refreshTokenCookie.httpOnly; + } + headers = Object.fromEntries(res.headers.entries()); + } else { + let cookies = res.headers["set-cookie"] || res.headers["Set-Cookie"]; + cookies = cookies === undefined ? [] : cookies; + if (!Array.isArray(cookies)) { + cookies = [cookies]; + } + + cookies.forEach((i) => { + if (i.split(";")[0].split("=")[0] === "sAccessToken") { + /** + * if token is sAccessToken=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0=.eyJzZXNzaW9uSGFuZGxlIjoiMWI4NDBhOTAtMjVmYy00ZjQ4LWE2YWMtMDc0MDIzZjNjZjQwIiwidXNlcklkIjoiIiwicmVmcmVzaFRva2VuSGFzaDEiOiJjYWNhZDNlMGNhMDVkNzRlNWYzNTc4NmFlMGQ2MzJjNDhmMTg1YmZmNmUxNThjN2I2OThkZDYwMzA1NzAyYzI0IiwidXNlckRhdGEiOnt9LCJhbnRpQ3NyZlRva2VuIjoiYTA2MjRjYWItZmIwNy00NTFlLWJmOTYtNWQ3YzU2MjMwZTE4IiwiZXhwaXJ5VGltZSI6MTYyNjUxMjM3NDU4NiwidGltZUNyZWF0ZWQiOjE2MjY1MDg3NzQ1ODYsImxtcnQiOjE2MjY1MDg3NzQ1ODZ9.f1sCkjt0OduS6I6FBQDBLV5zhHXpCU2GXnbe+8OCU6HKG00TX5CM8AyFlOlqzSHABZ7jES/+5k0Ff/rdD34cczlNqICcC4a23AjJg2a097rFrh8/8V7J5fr4UrHLIM4ojZNFz1NyVyDK/ooE6I7soHshEtEVr2XsnJ4q3d+fYs2wwx97PIT82hfHqgbRAzvlv952GYt+OH4bWQE4vTzDqGN7N2OKpn9l2fiCB1Ytzr3ocHRqKuQ8f6xW1n575Q1sSs9F9TtD7lrKfFQH+//6lyKFe2Q1SDc7YU4pE5Cy9Kc/LiqiTU+gsGIJL5qtMzUTG4lX38ugF4QDyNjDBMqCKw==; Max-Age=3599; Expires=Sat, 17 Jul 2021 08:59:34 GMT; Secure; HttpOnly; SameSite=Lax; Path=/' + * i.split(";")[0].split("=")[1] will result in eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsInZlcnNpb24iOiIyIn0 + */ + accessToken = decodeURIComponent(i.split(";")[0].split("=").slice(1).join("=")); + if (i.split(";")[2].includes("Expires=")) { + accessTokenExpiry = i.split(";")[2].split("=")[1]; + } else if (i.split(";")[2].includes("expires=")) { + accessTokenExpiry = i.split(";")[2].split("=")[1]; + } else { + accessTokenExpiry = i.split(";")[3].split("=")[1]; + } + if (i.split(";")[1].includes("Domain=")) { + accessTokenDomain = i.split(";")[1].split("=")[1]; + } + accessTokenHttpOnly = i.split(";").findIndex((j) => j.includes("HttpOnly")) !== -1; + } else if (i.split(";")[0].split("=")[0] === "sRefreshToken") { + refreshToken = i.split(";")[0].split("=").slice(1).join("="); + if (i.split(";")[2].includes("Expires=")) { + refreshTokenExpiry = i.split(";")[2].split("=")[1]; + } else if (i.split(";")[2].includes("expires=")) { + refreshTokenExpiry = i.split(";")[2].split("=")[1]; + } else { + refreshTokenExpiry = i.split(";")[3].split("=")[1]; + } + if (i.split(";")[1].includes("Domain=")) { + refreshTokenDomain = i.split(";")[1].split("=").slice(1).join("="); + } + refreshTokenHttpOnly = i.split(";").findIndex((j) => j.includes("HttpOnly")) !== -1; + } + }); + } + let antiCsrf = headers["anti-csrf"]; + let frontToken = headers["front-token"]; - const refreshTokenFromHeader = res.headers["st-refresh-token"]; - const accessTokenFromHeader = res.headers["st-access-token"]; + const refreshTokenFromHeader = headers["st-refresh-token"]; + const accessTokenFromHeader = headers["st-access-token"]; const accessTokenFromAny = accessToken === undefined ? accessTokenFromHeader : accessToken; const refreshTokenFromAny = refreshToken === undefined ? refreshTokenFromHeader : refreshToken; diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index 79d8c9bb1..46646d354 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -1,10 +1,12 @@ import * as express from "express"; +import { NextApiRequest, NextApiResponse } from "next"; import Supertokens, { RecipeUserId, User, getUser } from "../.."; import Session, { RecipeInterface, SessionClaimValidator, VerifySessionOptions } from "../../recipe/session"; import EmailVerification from "../../recipe/emailverification"; import EmailPassword from "../../recipe/emailpassword"; import { verifySession } from "../../recipe/session/framework/express"; import { middleware, errorHandler, SessionRequest } from "../../framework/express"; +import customFramework, { CollectingResponse, PreParsedRequest } from "../../framework/custom"; import NextJS from "../../nextjs"; import ThirdPartyEmailPassword from "../../recipe/thirdpartyemailpassword"; import ThirdParty from "../../recipe/thirdparty"; @@ -29,6 +31,7 @@ import UserRoles from "../../recipe/userroles"; import Dashboard from "../../recipe/dashboard"; import JWT from "../../recipe/jwt"; import AccountLinking from "../../recipe/accountlinking"; +import { verifySession as customVerifySession } from "../../recipe/session/framework/custom"; UserRoles.init({ override: { @@ -915,10 +918,11 @@ Multitenancy.init({ }, }); -import { TypeInput } from "../../types"; +import { HTTPMethod, TypeInput } from "../../types"; import { TypeInput as SessionTypeInput } from "../../recipe/session/types"; import { TypeInput as EPTypeInput } from "../../recipe/emailpassword/types"; import SuperTokensError from "../../lib/build/error"; +import { serialize } from "cookie"; let app = express(); let sessionConfig: SessionTypeInput = { @@ -1827,3 +1831,55 @@ async function accountLinkingFuncsTest() { ), }; } + +const nextAppDirMiddleware = customFramework.middleware((req) => { + const query = Object.fromEntries(new URL(req.url!).searchParams.entries()); + + return new customFramework.PreParsedRequest({ + method: req.method as HTTPMethod, + url: req.url!, + query: query, + headers: req.headers, + cookies: req.cookies, + getFormBody: () => req.body, + getJSONBody: () => req.body, + }); +}); + +// We do not have Next13 typings here, but this is almost the exact same code we will have in the app dir example +async function handleCall(req: NextApiRequest): Promise { + const baseResponse = new customFramework.CollectingResponse(); + + const { handled, error } = await nextAppDirMiddleware(req, baseResponse); + + if (error) { + throw error; + } + if (!handled) { + return { + status: 404, + body: "Not Found", + }; + } + + for (const respCookie of baseResponse.cookies) { + baseResponse.headers.append( + "Set-Cookie", + serialize(respCookie.key, respCookie.value, { + domain: respCookie.domain, + expires: new Date(respCookie.expires), + httpOnly: respCookie.httpOnly, + path: respCookie.path, + sameSite: respCookie.sameSite, + secure: respCookie.secure, + }) + ); + } + + return { body: baseResponse.body, headers: baseResponse.headers, status: baseResponse.statusCode }; +} + +class NextResponse {} +NextJS.getAppDirRequestHandler(NextResponse); + +customVerifySession({ checkDatabase: true })(new PreParsedRequest({} as any), new CollectingResponse()); diff --git a/updateLibInExample b/updateLibInExample new file mode 100755 index 000000000..0e76b266d --- /dev/null +++ b/updateLibInExample @@ -0,0 +1,5 @@ +#!/bin/bash + +if ! [[ -z "${COMMIT_REF}" ]]; then + npm i git+$REPOSITORY_URL#$COMMIT_REF +fi