diff --git a/lib/build/querier.js b/lib/build/querier.js index 8f1a41977..038181ada 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -435,10 +435,12 @@ class Querier { strPath.startsWith(hydraAdmPathPrefix) || strPath.startsWith(exports.hydraPubPathPrefix); if (strPath.startsWith(exports.hydraPubPathPrefix)) { currentDomain = exports.hydraPubDomain; + currentBasePath = ""; strPath = strPath.replace(exports.hydraPubPathPrefix, "/oauth2"); } if (strPath.startsWith(hydraAdmPathPrefix)) { currentDomain = hydraAdmDomain; + currentBasePath = ""; strPath = strPath.replace(hydraAdmPathPrefix, "/admin"); } const url = currentDomain + currentBasePath + strPath; @@ -561,7 +563,6 @@ Querier.networkInterceptor = undefined; Querier.globalCacheTag = Date.now(); Querier.disableCache = false; async function handleHydraAPICall(response) { - console.log({ hydraResponse: response, text: await response.clone().text() }); const contentType = response.headers.get("Content-Type"); if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("application/json")) { return { diff --git a/lib/build/recipe/oauth2/api/auth.js b/lib/build/recipe/oauth2/api/auth.js index 90103ef66..e9ffab92a 100644 --- a/lib/build/recipe/oauth2/api/auth.js +++ b/lib/build/recipe/oauth2/api/auth.js @@ -13,8 +13,14 @@ * 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 }); const utils_1 = require("../../../utils"); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); async function authGET(apiImplementation, options, userContext) { if (apiImplementation.authGET === undefined) { return false; @@ -31,8 +37,19 @@ async function authGET(apiImplementation, options, userContext) { if ("redirectTo" in response) { // TODO: if (response.setCookie) { - for (const c of response.setCookie.replace(/, (\w+=)/, "\n$1").split("\n")) { - options.res.setHeader("set-cookie", c, true); + const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.setCookie); + const cookies = set_cookie_parser_1.default.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires).getTime(), + cookie.path || "/", + cookie.sameSite + ); } } options.res.original.redirect(response.redirectTo); diff --git a/lib/build/recipe/oauth2/recipeImplementation.js b/lib/build/recipe/oauth2/recipeImplementation.js index f4559d5de..d1f29aa71 100644 --- a/lib/build/recipe/oauth2/recipeImplementation.js +++ b/lib/build/recipe/oauth2/recipeImplementation.js @@ -257,20 +257,22 @@ function getRecipeInterface(querier, _config, _appInfo) { ); }, getOAuth2Clients: async function (input, userContext) { - let response = await querier.sendGetRequest( + var _a; + let response = await querier.sendGetRequestWithResponseHeaders( new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`), Object.assign(Object.assign({}, utils_1.transformObjectKeys(input, "snake-case")), { page_token: input.paginationToken, }), + {}, userContext ); - if (response.status === "OK") { + if (response.body.status === "OK") { // Pagination info is in the Link header, containing comma-separated links: // "first", "next" (if applicable). // Example: Link: ; rel="first", ; rel="next" // We parse the nextPaginationToken from the Link header using RegExp let nextPaginationToken; - const linkHeader = response.headers.get("link"); + const linkHeader = (_a = response.headers.get("link")) !== null && _a !== void 0 ? _a : ""; const nextLinkMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/); if (nextLinkMatch) { const url = nextLinkMatch[1]; @@ -279,14 +281,14 @@ function getRecipeInterface(querier, _config, _appInfo) { } return { status: "OK", - clients: response.data.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)), + clients: response.body.data.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)), nextPaginationToken, }; } else { return { status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, + error: response.body.data.error, + errorHint: response.body.data.errorHint, }; } }, diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index da0d059e0..380eb3d40 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -567,11 +567,13 @@ export class Querier { if (strPath.startsWith(hydraPubPathPrefix)) { currentDomain = hydraPubDomain; + currentBasePath = ""; strPath = strPath.replace(hydraPubPathPrefix, "/oauth2"); } if (strPath.startsWith(hydraAdmPathPrefix)) { currentDomain = hydraAdmDomain; + currentBasePath = ""; strPath = strPath.replace(hydraAdmPathPrefix, "/admin"); } @@ -653,7 +655,6 @@ export class Querier { } async function handleHydraAPICall(response: Response) { - console.log({ hydraResponse: response, text: await response.clone().text() }); const contentType = response.headers.get("Content-Type"); if (contentType?.startsWith("application/json")) { diff --git a/lib/ts/recipe/oauth2/api/auth.ts b/lib/ts/recipe/oauth2/api/auth.ts index 802d408ef..b1d133b12 100644 --- a/lib/ts/recipe/oauth2/api/auth.ts +++ b/lib/ts/recipe/oauth2/api/auth.ts @@ -16,6 +16,7 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; +import setCookieParser from "set-cookie-parser"; export default async function authGET( apiImplementation: APIInterface, @@ -38,8 +39,19 @@ export default async function authGET( if ("redirectTo" in response) { // TODO: if (response.setCookie) { - for (const c of response.setCookie.replace(/, (\w+=)/, "\n$1").split("\n")) { - options.res.setHeader("set-cookie", c, true); + const cookieStr = setCookieParser.splitCookiesString(response.setCookie); + const cookies = setCookieParser.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires!).getTime(), + cookie.path || "/", + cookie.sameSite as any + ); } } options.res.original.redirect(response.redirectTo); diff --git a/lib/ts/recipe/oauth2/recipeImplementation.ts b/lib/ts/recipe/oauth2/recipeImplementation.ts index 207ab1a02..4317e442d 100644 --- a/lib/ts/recipe/oauth2/recipeImplementation.ts +++ b/lib/ts/recipe/oauth2/recipeImplementation.ts @@ -265,23 +265,24 @@ export default function getRecipeInterface( }, getOAuth2Clients: async function (input, userContext) { - let response = await querier.sendGetRequest( + let response = await querier.sendGetRequestWithResponseHeaders( new NormalisedURLPath(`/recipe/oauth2/admin/clients`), { ...transformObjectKeys(input, "snake-case"), page_token: input.paginationToken, }, + {}, userContext ); - if (response.status === "OK") { + if (response.body.status === "OK") { // Pagination info is in the Link header, containing comma-separated links: // "first", "next" (if applicable). // Example: Link: ; rel="first", ; rel="next" // We parse the nextPaginationToken from the Link header using RegExp let nextPaginationToken: string | undefined; - const linkHeader = response.headers.get("link"); + const linkHeader = response.headers.get("link") ?? ""; const nextLinkMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/); if (nextLinkMatch) { @@ -292,14 +293,14 @@ export default function getRecipeInterface( return { status: "OK", - clients: response.data.map((client: any) => OAuth2Client.fromAPIResponse(client)), + clients: response.body.data.map((client: any) => OAuth2Client.fromAPIResponse(client)), nextPaginationToken, }; } else { return { status: "ERROR", - error: response.data.error, - errorHint: response.data.errorHint, + error: response.body.data.error, + errorHint: response.body.data.errorHint, }; } }, diff --git a/package-lock.json b/package-lock.json index d4f1b4611..d439ee1f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "nodemailer": "^6.7.2", "pkce-challenge": "^3.0.0", "psl": "1.8.0", + "set-cookie-parser": "^2.6.0", "supertokens-js-override": "^0.0.4", "twilio": "^4.19.3" }, @@ -39,6 +40,7 @@ "@types/koa-bodyparser": "^4.3.3", "@types/nodemailer": "^6.4.4", "@types/psl": "1.1.0", + "@types/set-cookie-parser": "^2.4.9", "@types/validator": "10.11.0", "aws-sdk-mock": "^5.4.0", "body-parser": "1.20.1", @@ -1713,6 +1715,15 @@ "@types/node": "*" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/type-is": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", @@ -7010,8 +7021,7 @@ "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -9568,6 +9578,15 @@ "@types/node": "*" } }, + "@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/type-is": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", @@ -13642,8 +13661,7 @@ "set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "set-function-length": { "version": "1.2.2", diff --git a/package.json b/package.json index 8f5377af5..2022a0fc6 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "nodemailer": "^6.7.2", "pkce-challenge": "^3.0.0", "psl": "1.8.0", + "set-cookie-parser": "^2.6.0", "supertokens-js-override": "^0.0.4", "twilio": "^4.19.3" }, @@ -143,6 +144,7 @@ "@types/koa-bodyparser": "^4.3.3", "@types/nodemailer": "^6.4.4", "@types/psl": "1.1.0", + "@types/set-cookie-parser": "^2.4.9", "@types/validator": "10.11.0", "aws-sdk-mock": "^5.4.0", "body-parser": "1.20.1", diff --git a/test/test-server/src/index.ts b/test/test-server/src/index.ts index 2856ffa26..0fdb993cb 100644 --- a/test/test-server/src/index.ts +++ b/test/test-server/src/index.ts @@ -19,6 +19,8 @@ import ThirdPartyRecipe from "../../../lib/build/recipe/thirdparty/recipe"; import { TypeInput as ThirdPartyTypeInput } from "../../../lib/build/recipe/thirdparty/types"; import { TypeInput as MFATypeInput } from "../../../lib/build/recipe/multifactorauth/types"; import TOTPRecipe from "../../../lib/build/recipe/totp/recipe"; +import OAuth2Recipe from "../../../lib/build/recipe/oauth2/recipe"; +import { TypeInput as OAuth2TypeInput } from "../../../lib/build/recipe/oauth2/types"; import UserMetadataRecipe from "../../../lib/build/recipe/usermetadata/recipe"; import SuperTokensRecipe from "../../../lib/build/supertokens"; import { RecipeListFunction } from "../../../lib/build/types"; @@ -32,6 +34,7 @@ import Session from "../../../recipe/session"; import { verifySession } from "../../../recipe/session/framework/express"; import ThirdParty from "../../../recipe/thirdparty"; import TOTP from "../../../recipe/totp"; +import OAuth2 from "../../../recipe/oauth2"; import accountlinkingRoutes from "./accountlinking"; import emailpasswordRoutes from "./emailpassword"; import emailverificationRoutes from "./emailverification"; @@ -39,6 +42,7 @@ import { logger } from "./logger"; import multiFactorAuthRoutes from "./multifactorauth"; import multitenancyRoutes from "./multitenancy"; import passwordlessRoutes from "./passwordless"; +import oAuth2Routes from "./oauth2"; import sessionRoutes from "./session"; import supertokensRoutes from "./supertokens"; import thirdPartyRoutes from "./thirdparty"; @@ -81,6 +85,7 @@ function STReset() { ProcessState.getInstance().reset(); MultiFactorAuthRecipe.reset(); TOTPRecipe.reset(); + OAuth2Recipe.reset(); SuperTokensRecipe.reset(); } @@ -237,6 +242,24 @@ function initST(config: any) { if (recipe.recipeId === "totp") { recipeList.push(TOTP.init(config)); } + if (recipe.recipeId === "oauth2") { + let initConfig: OAuth2TypeInput = { + ...config, + }; + if (initConfig.override?.functions) { + initConfig.override = { + ...initConfig.override, + functions: getFunc(`${initConfig.override.functions}`), + }; + } + if (initConfig.override?.apis) { + initConfig.override = { + ...initConfig.override, + apis: getFunc(`${initConfig.override.apis}`), + }; + } + recipeList.push(OAuth2.init(initConfig)); + } }); settings.recipeList = recipeList; @@ -318,6 +341,7 @@ app.use("/test/multifactorauth", multiFactorAuthRoutes); app.use("/test/thirdparty", thirdPartyRoutes); app.use("/test/totp", TOTPRoutes); app.use("/test/usermetadata", userMetadataRoutes); +app.use("/test/oauth2", oAuth2Routes); // *** Custom routes to help with session tests *** app.post("/create", async (req, res, next) => { diff --git a/test/test-server/src/oauth2.ts b/test/test-server/src/oauth2.ts new file mode 100644 index 000000000..d54dff6fe --- /dev/null +++ b/test/test-server/src/oauth2.ts @@ -0,0 +1,46 @@ +import { Router } from "express"; +import OAuth2 from "../../../recipe/oauth2"; +import { logger } from "./logger"; + +const namespace = "com.supertokens:node-test-server:oauth2"; +const { logDebugMessage } = logger(namespace); + +const router = Router() + .post("/getoauth2clients", async (req, res, next) => { + try { + logDebugMessage("OAuth2:getOAuth2Clients %j", req.body); + const response = await OAuth2.getOAuth2Clients(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/createoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2:createOAuth2Client %j", req.body); + const response = await OAuth2.createOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/updateoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2:updateOAuth2Client %j", req.body); + const response = await OAuth2.updateOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/deleteoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2:deleteOAuth2Client %j", req.body); + const response = await OAuth2.deleteOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }); + +export default router;