From a7eb53f65a9ddbb4d40d351ae71c7425dd3397c6 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:55:30 +0200 Subject: [PATCH 1/8] feat: update csurf -> csrf-csrf --- package-lock.json | 329 ++++++++++++++--------------------------- package.json | 11 +- src/pkg/index.ts | 52 +++---- src/pkg/ui.ts | 9 +- src/routes/consent.ts | 46 ++++-- src/routes/sessions.ts | 13 +- 6 files changed, 182 insertions(+), 278 deletions(-) diff --git a/package-lock.json b/package-lock.json index b5007fd4..7bb04931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,22 @@ { "name": "@ory/kratos-selfservice-ui-node", - "version": "0.13.0-11", + "version": "0.13.0-12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ory/kratos-selfservice-ui-node", - "version": "0.13.0-11", + "version": "0.13.0-12", "license": "Apache-2.0", "dependencies": { - "@ory/client": "1.1.50", - "@ory/elements-markup": "0.1.0-beta.4", + "@ory/client": "1.2.4", + "@ory/elements-markup": "file:../elements3/packages/markup", "@ory/integrations": "1.1.5", "accept-language-parser": "1.5.0", "axios": "1.2.6", "body-parser": "1.20.2", "cookie-parser": "1.4.6", - "csurf": "1.11.0", + "csrf-csrf": "3.0.0", "express": "4.18.2", "express-handlebars": "6.0.7", "express-jwt": "8.4.1", @@ -30,7 +30,6 @@ "@types/axios": "0.14.0", "@types/body-parser": "1.19.2", "@types/cookie-parser": "1.4.3", - "@types/csurf": "1.11.2", "@types/express": "4.17.17", "@types/express-handlebars": "6.0.0", "@types/handlebars-helpers": "0.5.3", @@ -45,13 +44,22 @@ "prettier-plugin-packagejson": "2.4.2", "tough-cookie": "4.1.3", "ts-node": "10.9.1", - "typescript": "4.9.5" + "typescript": "5.2.2" }, "engines": { "node": ">=16.16.0", "npm": ">=8.11.0" } }, + "../elements3/packages/markup": { + "name": "@ory/elements-markup", + "version": "0.0.0", + "license": "Apache License 2.0", + "engines": { + "node": ">=16.16.0", + "npm": ">=8.11.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -170,9 +178,9 @@ } }, "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -196,9 +204,9 @@ } }, "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -222,9 +230,9 @@ } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -236,9 +244,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.15.tgz", - "integrity": "sha512-l1UiX4UyHSFsYt17iQ3Se5pQQZZHa22zyIXURmvkmLCD4t/aU+dvNWHatKac/D9Vm9UES7nvIqHs4jZqKviUmQ==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.17.tgz", + "integrity": "sha512-XouDDhQESrLHTpnBtCKExJdyY4gJCdrvH2Pyv8r8kovX2U8G0dRUOT45T9XlbLtuu9CLXP15eusnkprhoPV5iQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", @@ -267,9 +275,9 @@ } }, "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -293,9 +301,9 @@ } }, "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -363,9 +371,9 @@ } }, "node_modules/@babel/helpers/node_modules/@babel/parser": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz", - "integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -375,9 +383,9 @@ } }, "node_modules/@babel/helpers/node_modules/@babel/traverse": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.15.tgz", - "integrity": "sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.17.tgz", + "integrity": "sha512-xK4Uwm0JnAMvxYZxOVecss85WxTEIbTa7bnGyf/+EgCL5Zt3U7htUpEOWv9detPlamGKuRzCqw74xVglDWpPdg==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", @@ -386,8 +394,8 @@ "@babel/helper-function-name": "^7.22.5", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15", + "@babel/parser": "^7.22.16", + "@babel/types": "^7.22.17", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -396,9 +404,9 @@ } }, "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -450,9 +458,9 @@ } }, "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz", - "integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -462,9 +470,9 @@ } }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz", - "integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==", + "version": "7.22.17", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.17.tgz", + "integrity": "sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", @@ -773,9 +781,9 @@ } }, "node_modules/@ory/client": { - "version": "1.1.50", - "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.1.50.tgz", - "integrity": "sha512-dWarxntqv0xLn2B2yaDCaLDpPrzi45BtGSdSZymObI/wPa3pyRjheEFbGWEuW5E72PBwGlWps9xLanhKaIJHJA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@ory/client/-/client-1.2.4.tgz", + "integrity": "sha512-iA3HTzzNJtsotIAKO9Jo4eeZs4ElXnI3F0p7iqZhMq5rGubWxRv2SC+vLgGOhDevkzsVI2UXYAbkXxloUxai5A==", "dependencies": { "axios": "^0.21.4" } @@ -789,13 +797,8 @@ } }, "node_modules/@ory/elements-markup": { - "version": "0.1.0-beta.4", - "resolved": "https://registry.npmjs.org/@ory/elements-markup/-/elements-markup-0.1.0-beta.4.tgz", - "integrity": "sha512-NdxMPZgbWBs9Vzjk18DXke9GsRpoRBHhMnNUhjymynS2q/E8F+qvSennMxlkvFb7KiEEeBQ6G5eqM4oE4Tucbg==", - "engines": { - "node": ">=16.16.0", - "npm": ">=8.11.0" - } + "resolved": "../elements3/packages/markup", + "link": true }, "node_modules/@ory/integrations": { "version": "1.1.5", @@ -984,9 +987,9 @@ } }, "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "version": "3.4.36", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", + "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", "dependencies": { "@types/node": "*" } @@ -1000,15 +1003,6 @@ "@types/express": "*" } }, - "node_modules/@types/csurf": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@types/csurf/-/csurf-1.11.2.tgz", - "integrity": "sha512-9bc98EnwmC1S0aSJiA8rWwXtgXtXHHOQOsGHptImxFgqm6CeH+mIOunHRg6+/eg2tlmDMX3tY7XrWxo2M/nUNQ==", - "dev": true, - "dependencies": { - "@types/express-serve-static-core": "*" - } - }, "node_modules/@types/express": { "version": "4.17.17", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", @@ -1146,9 +1140,9 @@ } }, "node_modules/@vue/compiler-core/node_modules/@babel/parser": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz", - "integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "peer": true, "bin": { @@ -1189,9 +1183,9 @@ } }, "node_modules/@vue/compiler-sfc/node_modules/@babel/parser": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz", - "integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "peer": true, "bin": { @@ -1227,9 +1221,9 @@ } }, "node_modules/@vue/reactivity-transform/node_modules/@babel/parser": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz", - "integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==", + "version": "7.22.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", + "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", "dev": true, "peer": true, "bin": { @@ -1363,14 +1357,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", - "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "get-intrinsic": "^1.2.1", "is-array-buffer": "^3.0.2", "is-shared-array-buffer": "^1.0.2" @@ -1643,9 +1638,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001525", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001525.tgz", - "integrity": "sha512-/3z+wB4icFt3r0USMwxujAqRvaD/B7rvGTsKhbhSQErVrJvkZCLhgNLJxU8MevahQVH6hCU9FsHdNUFbiwmE7Q==", + "version": "1.0.30001532", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001532.tgz", + "integrity": "sha512-FbDFnNat3nMnrROzqrsg314zhqN5LGQ1kyyMk2opcrwGbVGpHRhgCWtAgD5YJUqNAiQ+dklreil/c3Qf1dfCTw==", "funding": [ { "type": "opencollective", @@ -1862,84 +1857,12 @@ "semver": "bin/semver" } }, - "node_modules/csrf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", - "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", - "dependencies": { - "rndm": "1.2.0", - "tsscmp": "1.0.6", - "uid-safe": "2.1.5" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/csurf": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz", - "integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==", - "deprecated": "Please use another csrf package", - "dependencies": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "csrf": "3.1.0", - "http-errors": "~1.7.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/csurf/node_modules/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/csurf/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/csurf/node_modules/http-errors": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", - "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "node_modules/csrf-csrf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/csrf-csrf/-/csrf-csrf-3.0.0.tgz", + "integrity": "sha512-fAF+gxIRKVlmVIOhbfQ/hPyza1RZz5rhqvP658CLdKZXAUnrK67bo2ZwGOumTNYQ6kfyamOeFsVDx3zx1GJNfQ==", "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/csurf/node_modules/setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "node_modules/csurf/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/csurf/node_modules/toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "engines": { - "node": ">=0.6" + "http-errors": "^2.0.0" } }, "node_modules/dashdash": { @@ -2150,9 +2073,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.508", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz", - "integrity": "sha512-FFa8QKjQK/A5QuFr2167myhMesGrhlOBD+3cYNxO9/S4XzHEXesyTD/1/xF644gC8buFPz3ca6G1LOQD0tZrrg==", + "version": "1.4.513", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.513.tgz", + "integrity": "sha512-cOB0xcInjm+E5qIssHeXJ29BaUyWpMyFKT5RB3bsLENDheCja0wMkHJyiPl0NBE/VzDI7JDuNEQWhe6RitEUcw==", "dev": true }, "node_modules/enabled": { @@ -4698,14 +4621,6 @@ } ] }, - "node_modules/random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -4986,11 +4901,6 @@ "node": ">=0.10.0" } }, - "node_modules/rndm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", - "integrity": "sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==" - }, "node_modules/run-applescript": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", @@ -5178,13 +5088,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", - "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", + "get-intrinsic": "^1.2.1", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -5609,14 +5519,14 @@ } }, "node_modules/string.prototype.padend": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.4.tgz", - "integrity": "sha512-67otBXoksdjsnXXRUq+KMVTdlVRZ2af422Y0aTyTjVaoQkGr3mxl2Bc5emi7dOQ3OGVVQQskmLEWwFXwommpNw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -5626,14 +5536,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -5643,28 +5553,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5935,14 +5845,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "engines": { - "node": ">=0.6.x" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6037,16 +5939,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -6061,17 +5963,6 @@ "node": ">=0.8.0" } }, - "node_modules/uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "dependencies": { - "random-bytes": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index f4f63f51..3e102e03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ory/kratos-selfservice-ui-node", - "version": "0.13.0-11", + "version": "0.13.0-12", "description": "A reference implementation of a selfservice UI for ORY Kratos in node.js", "homepage": "https://github.com/ory/kratos-selfservice-ui-node#readme", "bugs": { @@ -27,14 +27,14 @@ }, "prettier": "ory-prettier-styles", "dependencies": { - "@ory/client": "1.1.50", - "@ory/elements-markup": "0.1.0-beta.4", + "@ory/client": "1.2.4", + "@ory/elements-markup": "file:../elements3/packages/markup", "@ory/integrations": "1.1.5", "accept-language-parser": "1.5.0", "axios": "1.2.6", "body-parser": "1.20.2", "cookie-parser": "1.4.6", - "csurf": "1.11.0", + "csrf-csrf": "3.0.0", "express": "4.18.2", "express-handlebars": "6.0.7", "express-jwt": "8.4.1", @@ -48,7 +48,6 @@ "@types/axios": "0.14.0", "@types/body-parser": "1.19.2", "@types/cookie-parser": "1.4.3", - "@types/csurf": "1.11.2", "@types/express": "4.17.17", "@types/express-handlebars": "6.0.0", "@types/handlebars-helpers": "0.5.3", @@ -63,7 +62,7 @@ "prettier-plugin-packagejson": "2.4.2", "tough-cookie": "4.1.3", "ts-node": "10.9.1", - "typescript": "4.9.5" + "typescript": "5.2.2" }, "engines": { "node": ">=16.16.0", diff --git a/src/pkg/index.ts b/src/pkg/index.ts index 491c6761..15d609a5 100644 --- a/src/pkg/index.ts +++ b/src/pkg/index.ts @@ -10,6 +10,7 @@ import { ButtonLink, Divider, MenuLink, Typography } from "@ory/elements-markup" import { filterNodesByGroups } from "@ory/integrations/ui" import { AxiosError } from "axios" import { NextFunction, Response } from "express" +import { UnknownObject } from "express-handlebars/types" export * from "./logger" export * from "./middleware" @@ -21,8 +22,7 @@ export const getUrlForFlow = ( flow: string, query?: URLSearchParams, ) => - `${removeTrailingSlash(base)}/self-service/${flow}/browser${ - query ? `?${query.toString()}` : "" + `${removeTrailingSlash(base)}/self-service/${flow}/browser${query ? `?${query.toString()}` : "" }` export const defaultConfig: RouteOptionsCreator = () => { @@ -54,37 +54,37 @@ const isErrorAuthenticatorAssuranceLevel = ( // or 403 error code. export const redirectOnSoftError = (res: Response, next: NextFunction, redirectTo: string) => - (err: AxiosError) => { - if (!err.response) { - next(err) - return - } + (err: AxiosError) => { + if (!err.response) { + next(err) + return + } - if ( - err.response.status === 404 || - err.response.status === 410 || - err.response.status === 403 - ) { - // in some cases Kratos will require us to redirect to a different page when the session_aal2_required - // for example, when recovery redirects us to settings - // but settings requires us to redirect to login?aal=aal2 - const authenticatorAssuranceLevelError = err.response.data as unknown if ( - isErrorAuthenticatorAssuranceLevel(authenticatorAssuranceLevelError) + err.response.status === 404 || + err.response.status === 410 || + err.response.status === 403 ) { - res.redirect( - authenticatorAssuranceLevelError.redirect_browser_to || redirectTo, - ) + // in some cases Kratos will require us to redirect to a different page when the session_aal2_required + // for example, when recovery redirects us to settings + // but settings requires us to redirect to login?aal=aal2 + const authenticatorAssuranceLevelError = err.response.data as unknown + if ( + isErrorAuthenticatorAssuranceLevel(authenticatorAssuranceLevelError) + ) { + res.redirect( + authenticatorAssuranceLevelError.redirect_browser_to || redirectTo, + ) + return + } + res.redirect(`${redirectTo}`) return } - res.redirect(`${redirectTo}`) - return - } - next(err) - } + next(err) + } -export const handlebarsHelpers = { +export const handlebarsHelpers: UnknownObject = { jsonPretty: (context: any) => JSON.stringify(context, null, 2), onlyNodes: ( nodes: Array, diff --git a/src/pkg/ui.ts b/src/pkg/ui.ts index 0c53b8eb..51d59d43 100644 --- a/src/pkg/ui.ts +++ b/src/pkg/ui.ts @@ -1,14 +1,7 @@ // Copyright © 2022 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { UiNodeInputAttributes, UiNode, Session } from "@ory/client" +import { Session } from "@ory/client" import { Nav } from "@ory/elements-markup" -import { - isUiNodeAnchorAttributes, - isUiNodeImageAttributes, - isUiNodeInputAttributes, - isUiNodeScriptAttributes, - isUiNodeTextAttributes, -} from "@ory/integrations/ui" type NavigationMenuProps = { navTitle: string diff --git a/src/routes/consent.ts b/src/routes/consent.ts index a39613a3..d3301778 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -10,7 +10,34 @@ import { } from "@ory/client" import { UserConsentCard } from "@ory/elements-markup" import bodyParser from "body-parser" -import csrf from "csurf" +import { doubleCsrf } from "csrf-csrf" +import { Request, Response, NextFunction } from "express" + + +// Sets up csrf protection +const { + generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token. + invalidCsrfTokenError, + doubleCsrfProtection, // This is the default CSRF protection middleware. +} = doubleCsrf({ + getSecret: () => "VERY_SECRET_VALUE", // A function that optionally takes the request and returns a secret + cookieName: "ax-x-csrf-token", // The name of the cookie to be used, recommend using Host prefix. + cookieOptions: { + sameSite: "lax", // Recommend you make this strict if posible + secure: true, + }, + ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected. + getTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request +}); + +// Error handling, validation error interception +const csrfErrorHandler = (error: unknown, req: Request, res: Response, next: NextFunction) => { + if (error == invalidCsrfTokenError) { + next(new Error("csrf validation error")) + } else { + next(); + } +}; async function createOAuth2ConsentRequestSession( grantScopes: string[], @@ -126,7 +153,7 @@ export const createConsentRoute: RouteCreator = res.render("consent", { card: UserConsentCard({ consent: body, - csrfToken: req.csrfToken(), + csrfToken: generateToken(req, res), cardImage: body.client?.logo_uri || "/ory-logo.svg", client_name: body.client?.client_name || "unknown client", requested_scope: body.requested_scope, @@ -230,16 +257,11 @@ export const createConsentPostRoute: RouteCreator = .catch(next) } -// Sets up csrf protection -const csrfProtection = csrf({ - cookie: { - sameSite: "lax", - }, -}) + var parseForm = bodyParser.urlencoded({ extended: false }) -export const registerConsentRoute: RouteRegistrator = function ( +export const registerConsentRoute: RouteRegistrator = function( app, createHelpers = defaultConfig, ) { @@ -247,7 +269,6 @@ export const registerConsentRoute: RouteRegistrator = function ( console.log("found HYDRA_ADMIN_URL") return app.get( "/consent", - csrfProtection, createConsentRoute(createHelpers), ) } else { @@ -255,7 +276,7 @@ export const registerConsentRoute: RouteRegistrator = function ( } } -export const registerConsentPostRoute: RouteRegistrator = function ( +export const registerConsentPostRoute: RouteRegistrator = function( app, createHelpers = defaultConfig, ) { @@ -263,7 +284,8 @@ export const registerConsentPostRoute: RouteRegistrator = function ( return app.post( "/consent", parseForm, - csrfProtection, + doubleCsrfProtection, + csrfErrorHandler, createConsentPostRoute(createHelpers), ) } else { diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index 08f5e526..8c7489d2 100644 --- a/src/routes/sessions.ts +++ b/src/routes/sessions.ts @@ -24,7 +24,7 @@ export const createSessionsRoute: RouteCreator = ).data.logout_url || "" const identityCredentialTrait = - session?.identity.traits.email || session?.identity.traits.username || "" + session?.identity?.traits.email || session?.identity?.traits.username || "" const sessionText = identityCredentialTrait !== "" @@ -41,10 +41,10 @@ export const createSessionsRoute: RouteCreator = color: "foregroundMuted", }), traits: { - id: session?.identity.id, + id: session?.identity?.id, // sometimes the identity schema could contain recursive objects // for this use case we will just stringify the object instead of recursively flatten the object - ...Object.entries(session?.identity.traits).reduce>( + ...Object.entries(session?.identity?.traits).reduce>( (traits, [key, value]) => { traits[key] = typeof value === "object" ? JSON.stringify(value) : value @@ -52,7 +52,7 @@ export const createSessionsRoute: RouteCreator = }, {}, ), - "signup date": session?.identity.created_at || "", + "signup date": session?.identity?.created_at || "", "authentication level": session?.authenticator_assurance_level === "aal2" ? "two-factor used (aal2)" @@ -71,9 +71,8 @@ export const createSessionsRoute: RouteCreator = authMethods: session?.authentication_methods?.reduce( (methods, method, i) => { methods.push({ - [`authentication method used`]: `${method.method} (${ - method.completed_at && new Date(method.completed_at).toUTCString() - })`, + [`authentication method used`]: `${method.method} (${method.completed_at && new Date(method.completed_at).toUTCString() + })`, }) return methods }, From 84e422659543f4bfb68adc1c1f9ec1715561a4bf Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:58:58 +0200 Subject: [PATCH 2/8] style: format --- package-lock.json | 20 +++++++---------- package.json | 2 +- src/pkg/index.ts | 49 +++++++++++++++++++++--------------------- src/routes/consent.ts | 27 +++++++++++------------ src/routes/sessions.ts | 24 +++++++++++---------- 5 files changed, 60 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bb04931..095c3664 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@ory/client": "1.2.4", - "@ory/elements-markup": "file:../elements3/packages/markup", + "@ory/elements-markup": "0.1.0-beta.4", "@ory/integrations": "1.1.5", "accept-language-parser": "1.5.0", "axios": "1.2.6", @@ -51,15 +51,6 @@ "npm": ">=8.11.0" } }, - "../elements3/packages/markup": { - "name": "@ory/elements-markup", - "version": "0.0.0", - "license": "Apache License 2.0", - "engines": { - "node": ">=16.16.0", - "npm": ">=8.11.0" - } - }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -797,8 +788,13 @@ } }, "node_modules/@ory/elements-markup": { - "resolved": "../elements3/packages/markup", - "link": true + "version": "0.1.0-beta.4", + "resolved": "https://registry.npmjs.org/@ory/elements-markup/-/elements-markup-0.1.0-beta.4.tgz", + "integrity": "sha512-NdxMPZgbWBs9Vzjk18DXke9GsRpoRBHhMnNUhjymynS2q/E8F+qvSennMxlkvFb7KiEEeBQ6G5eqM4oE4Tucbg==", + "engines": { + "node": ">=16.16.0", + "npm": ">=8.11.0" + } }, "node_modules/@ory/integrations": { "version": "1.1.5", diff --git a/package.json b/package.json index 3e102e03..6d05573e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "prettier": "ory-prettier-styles", "dependencies": { "@ory/client": "1.2.4", - "@ory/elements-markup": "file:../elements3/packages/markup", + "@ory/elements-markup": "0.1.0-beta.4", "@ory/integrations": "1.1.5", "accept-language-parser": "1.5.0", "axios": "1.2.6", diff --git a/src/pkg/index.ts b/src/pkg/index.ts index 15d609a5..34371e34 100644 --- a/src/pkg/index.ts +++ b/src/pkg/index.ts @@ -22,7 +22,8 @@ export const getUrlForFlow = ( flow: string, query?: URLSearchParams, ) => - `${removeTrailingSlash(base)}/self-service/${flow}/browser${query ? `?${query.toString()}` : "" + `${removeTrailingSlash(base)}/self-service/${flow}/browser${ + query ? `?${query.toString()}` : "" }` export const defaultConfig: RouteOptionsCreator = () => { @@ -54,36 +55,36 @@ const isErrorAuthenticatorAssuranceLevel = ( // or 403 error code. export const redirectOnSoftError = (res: Response, next: NextFunction, redirectTo: string) => - (err: AxiosError) => { - if (!err.response) { - next(err) - return - } + (err: AxiosError) => { + if (!err.response) { + next(err) + return + } + if ( + err.response.status === 404 || + err.response.status === 410 || + err.response.status === 403 + ) { + // in some cases Kratos will require us to redirect to a different page when the session_aal2_required + // for example, when recovery redirects us to settings + // but settings requires us to redirect to login?aal=aal2 + const authenticatorAssuranceLevelError = err.response.data as unknown if ( - err.response.status === 404 || - err.response.status === 410 || - err.response.status === 403 + isErrorAuthenticatorAssuranceLevel(authenticatorAssuranceLevelError) ) { - // in some cases Kratos will require us to redirect to a different page when the session_aal2_required - // for example, when recovery redirects us to settings - // but settings requires us to redirect to login?aal=aal2 - const authenticatorAssuranceLevelError = err.response.data as unknown - if ( - isErrorAuthenticatorAssuranceLevel(authenticatorAssuranceLevelError) - ) { - res.redirect( - authenticatorAssuranceLevelError.redirect_browser_to || redirectTo, - ) - return - } - res.redirect(`${redirectTo}`) + res.redirect( + authenticatorAssuranceLevelError.redirect_browser_to || redirectTo, + ) return } - - next(err) + res.redirect(`${redirectTo}`) + return } + next(err) + } + export const handlebarsHelpers: UnknownObject = { jsonPretty: (context: any) => JSON.stringify(context, null, 2), onlyNodes: ( diff --git a/src/routes/consent.ts b/src/routes/consent.ts index d3301778..b7768100 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -13,7 +13,6 @@ import bodyParser from "body-parser" import { doubleCsrf } from "csrf-csrf" import { Request, Response, NextFunction } from "express" - // Sets up csrf protection const { generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token. @@ -23,21 +22,26 @@ const { getSecret: () => "VERY_SECRET_VALUE", // A function that optionally takes the request and returns a secret cookieName: "ax-x-csrf-token", // The name of the cookie to be used, recommend using Host prefix. cookieOptions: { - sameSite: "lax", // Recommend you make this strict if posible + sameSite: "lax", // Recommend you make this strict if posible secure: true, }, ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected. getTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request -}); +}) // Error handling, validation error interception -const csrfErrorHandler = (error: unknown, req: Request, res: Response, next: NextFunction) => { +const csrfErrorHandler = ( + error: unknown, + req: Request, + res: Response, + next: NextFunction, +) => { if (error == invalidCsrfTokenError) { next(new Error("csrf validation error")) } else { - next(); + next() } -}; +} async function createOAuth2ConsentRequestSession( grantScopes: string[], @@ -257,26 +261,21 @@ export const createConsentPostRoute: RouteCreator = .catch(next) } - - var parseForm = bodyParser.urlencoded({ extended: false }) -export const registerConsentRoute: RouteRegistrator = function( +export const registerConsentRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { if (process.env.HYDRA_ADMIN_URL) { console.log("found HYDRA_ADMIN_URL") - return app.get( - "/consent", - createConsentRoute(createHelpers), - ) + return app.get("/consent", createConsentRoute(createHelpers)) } else { return register404Route } } -export const registerConsentPostRoute: RouteRegistrator = function( +export const registerConsentPostRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index 8c7489d2..c8056c39 100644 --- a/src/routes/sessions.ts +++ b/src/routes/sessions.ts @@ -24,7 +24,9 @@ export const createSessionsRoute: RouteCreator = ).data.logout_url || "" const identityCredentialTrait = - session?.identity?.traits.email || session?.identity?.traits.username || "" + session?.identity?.traits.email || + session?.identity?.traits.username || + "" const sessionText = identityCredentialTrait !== "" @@ -44,14 +46,13 @@ export const createSessionsRoute: RouteCreator = id: session?.identity?.id, // sometimes the identity schema could contain recursive objects // for this use case we will just stringify the object instead of recursively flatten the object - ...Object.entries(session?.identity?.traits).reduce>( - (traits, [key, value]) => { - traits[key] = - typeof value === "object" ? JSON.stringify(value) : value - return traits - }, - {}, - ), + ...Object.entries(session?.identity?.traits).reduce< + Record + >((traits, [key, value]) => { + traits[key] = + typeof value === "object" ? JSON.stringify(value) : value + return traits + }, {}), "signup date": session?.identity?.created_at || "", "authentication level": session?.authenticator_assurance_level === "aal2" @@ -71,8 +72,9 @@ export const createSessionsRoute: RouteCreator = authMethods: session?.authentication_methods?.reduce( (methods, method, i) => { methods.push({ - [`authentication method used`]: `${method.method} (${method.completed_at && new Date(method.completed_at).toUTCString() - })`, + [`authentication method used`]: `${method.method} (${ + method.completed_at && new Date(method.completed_at).toUTCString() + })`, }) return methods }, From e93f20d7b403d72076bb6b57a84528d8d021c414 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:58:59 +0200 Subject: [PATCH 3/8] feat: add required environment variables --- README.md | 46 ++++++++++++++++++++++++++++++++++++------- nodemon.json | 8 +++++++- src/index.ts | 37 +++++++++++++++++++++++++--------- src/pkg/sdk/index.ts | 15 +++++++------- src/routes/consent.ts | 45 ++++++++++++++++++++++++++++++------------ 5 files changed, 114 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index fdedcbac..64027e65 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,50 @@ registration, account recovery, ... screens, please check out the ## Configuration -This application can be configured using two environment variables: +Below is a list of environment variables required by the Express.js service to +function properly. -- `KRATOS_PUBLIC_URL` (required): The URL where ORY Kratos's Public API is - located at. If this app and ORY Kratos are running in the same private - network, this should be the private network address (e.g. +In a local development run of the service using `npm run start`, some of these +values will be set by nodemon and is configured by the `nodemon.json` file. + +When using this UI with an Ory Network project, you can use `ORY_SDK_URL` +instead of `KRATOS_PUBLIC_URL` and `HYDRA_ADMIN_URL`. + +Ory Identities requires the following variables to be set: + +- `ORY_SDK_URL` or `KRATOS_PUBLIC_URL` (required): The URL where ORY Kratos's + Public API is located at. If this app and ORY Kratos are running in the same + private network, this should be the private network address (e.g. `kratos-public.svc.cluster.local`). +- `KRATOS_BROWSER_URL` (optional) The browser accessible URL where ORY Kratos's + public API is located, only needed if it differs from `KRATOS_PUBLIC_URL` + +Ory OAuth2 requires more setup to get CSRF cookies on the `/consent` endpoint. + +- `ORY_SDK_URL` or `HYDRA_ADMIN_URL` (optional): The URL where Ory Hydra's + Public API is located at. If this app and Ory Hydra are running in the same + private network, this should be the private network address (e.g. + `hydra-admin.svc.cluster.local`) +- `COOKIE_SECRET` (required): Required for signing cookies. Must be a string + with at least 8 alphanumerical characters. +- `CSRF_COOKIE_SECRET` (optional): Required for the Consent route to set a CSRF + cookie with a hashed value. The value must be a string with at least 8 + alphanumerical characters. +- `ORY_ADMIN_API_TOKEN` (optional): When using with an Ory Network project, you + should add the `ORY_ADMIN_API_TOKEN` for OAuth2 Consent flows. +- `CSRF_COOKIE_NAME` (optional): By default the CSRF cookie will be set to + `ax-x-csrf-token`. +- `DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES` (optional) This environment + variables should only be used in local development when you do not have HTTPS + setup. This sets the CSRF cookies to `secure: false`, required for running + locally. + +Getting TLS working: + - `TLS_CERT_PATH` (optional): Path to certificate file. Should be set up together with `TLS_KEY_PATH` to enable HTTPS. - `TLS_KEY_PATH` (optional): Path to key file Should be set up together with `TLS_CERT_PATH` to enable HTTPS. -- `KRATOS_BROWSER_URL` (optional) The browser accessible URL where ORY Kratos's - public API is located, only needed if it differs from `KRATOS_PUBLIC_URL` This is the easiest mode as it requires no additional set up. This app runs on port `:4455` and ORY Kratos `KRATOS_PUBLIC_URL` URL. @@ -54,7 +86,7 @@ recommended. To run this app with dummy data and no real connection to ORY Kratos, use: ```shell script -$ NODE_ENV=stub npm start +NODE_ENV=stub npm start ``` ### Test with ORY Kratos diff --git a/nodemon.json b/nodemon.json index 7432e790..98a40b93 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,11 @@ { "watch": ["src"], "ext": "ts", - "exec": "ts-node ./src/index.ts" + "exec": "ts-node ./src/index.ts", + "env": { + "COOKIE_SECRET": "I_AM_VERY_SECRET", + "CSRF_COOKIE_SECRET": "I_AM_VERY_SECRET_TOO", + "DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES": "true", + "ORY_SDK_URL": "http://localhost:4000" + } } diff --git a/src/index.ts b/src/index.ts index 18a5c366..e7b9c4c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,7 +35,7 @@ const app = express() const router = express.Router() app.use(middlewareLogger) -app.use(cookieParser()) +app.use(cookieParser(process.env.COOKIE_SECRET || "")) app.use(addFavicon(defaultConfig)) app.use(detectLanguage) app.set("view engine", "hbs") @@ -79,13 +79,32 @@ let listener = (proto: "http" | "https") => () => { console.log(`Listening on ${proto}://0.0.0.0:${port}`) } -if (process.env.TLS_CERT_PATH?.length && process.env.TLS_KEY_PATH?.length) { - const options = { - cert: fs.readFileSync(process.env.TLS_CERT_PATH), - key: fs.readFileSync(process.env.TLS_KEY_PATH), - } - - https.createServer(options, app).listen(port, listener("https")) +// When using the Ory Admin API Token, we assume that this application is also +// handling OAuth2 Consent requests. In that case we need to ensure that the +// COOKIE_SECRET and CSRF_COOKIE_SECRET environment variables are set. +if ( + (process.env.ORY_ADMIN_API_TOKEN && + String(process.env.COOKIE_SECRET || "").length < 8) || + String(process.env.CSRF_COOKIE_SECRET || "").length < 8 +) { + console.error( + "Cannot start the server without the required environment variables!", + ) + console.error( + "COOKIE_SECRET must be set and be at least 8 alphanumerical character `export COOKIE_SECRET=...`", + ) + console.error( + "CSRF_COOKIE_SECRET must be set and be at least 8 alphanumerical character `export CSRF_COOKIE_SECRET=...`", + ) } else { - app.listen(port, listener("http")) + if (process.env.TLS_CERT_PATH?.length && process.env.TLS_KEY_PATH?.length) { + const options = { + cert: fs.readFileSync(process.env.TLS_CERT_PATH), + key: fs.readFileSync(process.env.TLS_KEY_PATH), + } + + https.createServer(options, app).listen(port, listener("https")) + } else { + app.listen(port, listener("http")) + } } diff --git a/src/pkg/sdk/index.ts b/src/pkg/sdk/index.ts index cc488fe2..4940748c 100644 --- a/src/pkg/sdk/index.ts +++ b/src/pkg/sdk/index.ts @@ -15,12 +15,6 @@ const apiBaseIdentityUrl = process.env.KRATOS_ADMIN_URL || baseUrlInternal export const apiBaseUrl = process.env.KRATOS_BROWSER_URL || apiBaseFrontendUrlInternal -const hydraBaseOptions: any = {} - -if (process.env.MOCK_TLS_TERMINATION) { - hydraBaseOptions.headers = { "X-Forwarded-Proto": "https" } -} - // Sets up the SDK const sdk = { basePath: apiBaseFrontendUrlInternal, @@ -32,7 +26,14 @@ const sdk = { oauth2: new OAuth2Api( new Configuration({ basePath: apiBaseOauth2UrlInternal, - baseOptions: hydraBaseOptions, + ...(process.env.ORY_ADMIN_API_TOKEN && { + accessToken: process.env.ORY_ADMIN_API_TOKEN, + }), + ...(process.env.MOCK_TLS_TERMINATION && { + baseOptions: { + "X-Forwarded-Proto": "https", + }, + }), }), ), identity: new IdentityApi( diff --git a/src/routes/consent.ts b/src/routes/consent.ts index b7768100..47ce9ab2 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -10,25 +10,43 @@ import { } from "@ory/client" import { UserConsentCard } from "@ory/elements-markup" import bodyParser from "body-parser" -import { doubleCsrf } from "csrf-csrf" +import { doubleCsrf, DoubleCsrfCookieOptions } from "csrf-csrf" import { Request, Response, NextFunction } from "express" +const cookieOptions: DoubleCsrfCookieOptions = { + sameSite: "lax", + signed: true, + // set secure cookies by default (recommended in production) + // can be disabled through DANGEROUSLY_DISABLE_SECURE_COOKIES=true env var + secure: true, + ...(process.env.DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES && { + secure: false, + }), +} + +const cookieName = process.env.CSRF_COOKIE_NAME || "ax-x-csrf-token" +const cookieSecret = process.env.CSRF_COOKIE_SECRET + // Sets up csrf protection const { generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token. invalidCsrfTokenError, doubleCsrfProtection, // This is the default CSRF protection middleware. } = doubleCsrf({ - getSecret: () => "VERY_SECRET_VALUE", // A function that optionally takes the request and returns a secret - cookieName: "ax-x-csrf-token", // The name of the cookie to be used, recommend using Host prefix. - cookieOptions: { - sameSite: "lax", // Recommend you make this strict if posible - secure: true, - }, + getSecret: () => cookieSecret || "", // A function that optionally takes the request and returns a secret + cookieName: cookieName, // The name of the cookie to be used, recommend using Host prefix. + cookieOptions, ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected. getTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request }) +// Checks if OAuth2 consent is enabled +// This is used to determine if the consent route should be registered +// We need to check if the environment variables are set +const isOAuthCosentEnabled = () => + (process.env.HYDRA_ADMIN_URL || process.env.ORY_SDK_URL) && + process.env.CSRF_COOKIE_SECRET + // Error handling, validation error interception const csrfErrorHandler = ( error: unknown, @@ -37,7 +55,11 @@ const csrfErrorHandler = ( next: NextFunction, ) => { if (error == invalidCsrfTokenError) { - next(new Error("csrf validation error")) + next( + new Error( + "A security violation was detected, please fill out the form again.", + ), + ) } else { next() } @@ -82,7 +104,6 @@ async function createOAuth2ConsentRequestSession( // A simple express handler that shows the Hydra consent screen. export const createConsentRoute: RouteCreator = (createHelpers) => (req, res, next) => { - console.log("createConsentRoute") res.locals.projectName = "An application requests access to your data!" const { oauth2, identity } = createHelpers(req, res) @@ -102,7 +123,6 @@ export const createConsentRoute: RouteCreator = trustedClients = String(process.env.TRUSTED_CLIENT_IDS).split(",") } - console.log("getOAuth2ConsentRequest", challenge) // This section processes consent requests and either shows the consent UI or // accepts the consent request right away if the user has given consent to this // app before @@ -267,8 +287,7 @@ export const registerConsentRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { - if (process.env.HYDRA_ADMIN_URL) { - console.log("found HYDRA_ADMIN_URL") + if (isOAuthCosentEnabled()) { return app.get("/consent", createConsentRoute(createHelpers)) } else { return register404Route @@ -279,7 +298,7 @@ export const registerConsentPostRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { - if (process.env.HYDRA_ADMIN_URL) { + if (isOAuthCosentEnabled()) { return app.post( "/consent", parseForm, From eceec918b70789c0a96e377def4edf716c84e788 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 14 Sep 2023 11:03:11 +0200 Subject: [PATCH 4/8] chore: update csrf_cookie_name Co-authored-by: Arne Luenser --- src/routes/consent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/consent.ts b/src/routes/consent.ts index 47ce9ab2..360edb97 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -24,7 +24,7 @@ const cookieOptions: DoubleCsrfCookieOptions = { }), } -const cookieName = process.env.CSRF_COOKIE_NAME || "ax-x-csrf-token" +const cookieName = process.env.CSRF_COOKIE_NAME || "__Host-ax-x-csrf-token" const cookieSecret = process.env.CSRF_COOKIE_SECRET // Sets up csrf protection From 1b075854ec16157fab5684238e6418a797e537fc Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 14 Sep 2023 11:03:24 +0200 Subject: [PATCH 5/8] chore: update readme Co-authored-by: Arne Luenser --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64027e65..6dcf582a 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Ory OAuth2 requires more setup to get CSRF cookies on the `/consent` endpoint. - `DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES` (optional) This environment variables should only be used in local development when you do not have HTTPS setup. This sets the CSRF cookies to `secure: false`, required for running - locally. + locally. When using this setting, you must also set `CSRF_COOKIE_NAME` to a name without the `__Host-` prefix. Getting TLS working: From c7410bfd63f920d8ff00e31b8a614f34526eec49 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 14 Sep 2023 11:08:04 +0200 Subject: [PATCH 6/8] chore: document the other environment variables --- README.md | 10 ++++++++-- src/routes/consent.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6dcf582a..fc7de5d1 100644 --- a/README.md +++ b/README.md @@ -41,14 +41,17 @@ Ory OAuth2 requires more setup to get CSRF cookies on the `/consent` endpoint. - `CSRF_COOKIE_SECRET` (optional): Required for the Consent route to set a CSRF cookie with a hashed value. The value must be a string with at least 8 alphanumerical characters. +- `REMEMBER_CONSENT_SESSION_FOR_SECONDS` (optional): Sets the `remember_for` + value of the accept consent request in seconds. The default is 3600 seconds. - `ORY_ADMIN_API_TOKEN` (optional): When using with an Ory Network project, you should add the `ORY_ADMIN_API_TOKEN` for OAuth2 Consent flows. - `CSRF_COOKIE_NAME` (optional): By default the CSRF cookie will be set to - `ax-x-csrf-token`. + `__Host-ax-x-csrf-token`. - `DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES` (optional) This environment variables should only be used in local development when you do not have HTTPS setup. This sets the CSRF cookies to `secure: false`, required for running - locally. When using this setting, you must also set `CSRF_COOKIE_NAME` to a name without the `__Host-` prefix. + locally. When using this setting, you must also set `CSRF_COOKIE_NAME` to a + name without the `__Host-` prefix. Getting TLS working: @@ -89,6 +92,9 @@ To run this app with dummy data and no real connection to ORY Kratos, use: NODE_ENV=stub npm start ``` +If you would like to also generate fake data for the `id_token`, please set the +environment varialbe `export CONFORMITY_FAKE_CLAIMS=1` + ### Test with ORY Kratos The easiest way to test this app with a local installation of ORY Kratos is to diff --git a/src/routes/consent.ts b/src/routes/consent.ts index 360edb97..47f705ab 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -37,7 +37,7 @@ const { cookieName: cookieName, // The name of the cookie to be used, recommend using Host prefix. cookieOptions, ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected. - getTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request + getTokenFromRequest: (req: Request) => req.headers["x-csrf-token"], // A function that returns the token from the request }) // Checks if OAuth2 consent is enabled @@ -268,7 +268,7 @@ export const createConsentPostRoute: RouteCreator = remember: Boolean(req.body.remember), // When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire. - remember_for: process.env.REMEMBER_CONSENT_FOR_SECONDS + remember_for: process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS ? Number(process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS) : 3600, }, From 5934457f26167170281bedaaea8fe4729d6327e4 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:03:05 +0200 Subject: [PATCH 7/8] refactor: cleanup consent code --- README.md | 4 +- nodemon.json | 1 + src/index.ts | 4 + src/pkg/sdk/index.ts | 3 + src/routes/consent.ts | 260 +++++++++++++++++++---------------- src/routes/stub/oidc-cert.ts | 2 - types/express/index.d.ts | 1 + 7 files changed, 152 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index fc7de5d1..1f03aae0 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Ory OAuth2 requires more setup to get CSRF cookies on the `/consent` endpoint. `hydra-admin.svc.cluster.local`) - `COOKIE_SECRET` (required): Required for signing cookies. Must be a string with at least 8 alphanumerical characters. +- `CSRF_COOKIE_NAME` (required): Change the cookie name to match your domain + using the `__HOST-example.com-x-csrf-token` format. - `CSRF_COOKIE_SECRET` (optional): Required for the Consent route to set a CSRF cookie with a hashed value. The value must be a string with at least 8 alphanumerical characters. @@ -45,8 +47,6 @@ Ory OAuth2 requires more setup to get CSRF cookies on the `/consent` endpoint. value of the accept consent request in seconds. The default is 3600 seconds. - `ORY_ADMIN_API_TOKEN` (optional): When using with an Ory Network project, you should add the `ORY_ADMIN_API_TOKEN` for OAuth2 Consent flows. -- `CSRF_COOKIE_NAME` (optional): By default the CSRF cookie will be set to - `__Host-ax-x-csrf-token`. - `DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES` (optional) This environment variables should only be used in local development when you do not have HTTPS setup. This sets the CSRF cookies to `secure: false`, required for running diff --git a/nodemon.json b/nodemon.json index 98a40b93..9d2904f6 100644 --- a/nodemon.json +++ b/nodemon.json @@ -3,6 +3,7 @@ "ext": "ts", "exec": "ts-node ./src/index.ts", "env": { + "CSRF_COOKIE_NAME": "ax-csrf-cookie", "COOKIE_SECRET": "I_AM_VERY_SECRET", "CSRF_COOKIE_SECRET": "I_AM_VERY_SECRET_TOO", "DANGEROUSLY_DISABLE_SECURE_CSRF_COOKIES": "true", diff --git a/src/index.ts b/src/index.ts index e7b9c4c1..380ef342 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,6 +85,7 @@ let listener = (proto: "http" | "https") => () => { if ( (process.env.ORY_ADMIN_API_TOKEN && String(process.env.COOKIE_SECRET || "").length < 8) || + String(process.env.CSRF_COOKIE_NAME || "").length === 0 || String(process.env.CSRF_COOKIE_SECRET || "").length < 8 ) { console.error( @@ -93,6 +94,9 @@ if ( console.error( "COOKIE_SECRET must be set and be at least 8 alphanumerical character `export COOKIE_SECRET=...`", ) + console.error( + "CSRF_COOKIE_NAME must be set! Prefix the name to scope it to your domain `__HOST-` `export CSRF_COOKIE_NAME=...`", + ) console.error( "CSRF_COOKIE_SECRET must be set and be at least 8 alphanumerical character `export CSRF_COOKIE_SECRET=...`", ) diff --git a/src/pkg/sdk/index.ts b/src/pkg/sdk/index.ts index 4940748c..3e9ce5ba 100644 --- a/src/pkg/sdk/index.ts +++ b/src/pkg/sdk/index.ts @@ -39,6 +39,9 @@ const sdk = { identity: new IdentityApi( new Configuration({ basePath: apiBaseIdentityUrl, + ...(process.env.ORY_ADMIN_API_TOKEN && { + accessToken: process.env.ORY_ADMIN_API_TOKEN, + }), }), ), } diff --git a/src/routes/consent.ts b/src/routes/consent.ts index 47f705ab..655255d0 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -1,13 +1,9 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { defaultConfig, RouteCreator, RouteRegistrator } from "../pkg" +import { defaultConfig, logger, RouteCreator, RouteRegistrator } from "../pkg" import { register404Route } from "./404" import { oidcConformityMaybeFakeSession } from "./stub/oidc-cert" -import { - AcceptOAuth2ConsentRequestSession, - IdentityApi, - OAuth2ConsentRequest, -} from "@ory/client" +import { AcceptOAuth2ConsentRequestSession } from "@ory/client" import { UserConsentCard } from "@ory/elements-markup" import bodyParser from "body-parser" import { doubleCsrf, DoubleCsrfCookieOptions } from "csrf-csrf" @@ -29,15 +25,17 @@ const cookieSecret = process.env.CSRF_COOKIE_SECRET // Sets up csrf protection const { - generateToken, // Use this in your routes to provide a CSRF hash + token cookie and token. invalidCsrfTokenError, doubleCsrfProtection, // This is the default CSRF protection middleware. } = doubleCsrf({ getSecret: () => cookieSecret || "", // A function that optionally takes the request and returns a secret cookieName: cookieName, // The name of the cookie to be used, recommend using Host prefix. cookieOptions, - ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected. - getTokenFromRequest: (req: Request) => req.headers["x-csrf-token"], // A function that returns the token from the request + ignoredMethods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"], // A list of request methods that will not be protected. + getTokenFromRequest: (req: Request) => { + logger.debug("Getting CSRF token from request", { body: req.body }) + return req.body._csrf + }, // A function that returns the token from the request }) // Checks if OAuth2 consent is enabled @@ -55,6 +53,9 @@ const csrfErrorHandler = ( next: NextFunction, ) => { if (error == invalidCsrfTokenError) { + logger.debug("The CSRF token is invalid or could not be found.", { + req: req, + }) next( new Error( "A security violation was detected, please fill out the form again.", @@ -65,48 +66,64 @@ const csrfErrorHandler = ( } } -async function createOAuth2ConsentRequestSession( - grantScopes: string[], - consentRequest: OAuth2ConsentRequest, - identityApi: IdentityApi, -): Promise { - // The session allows us to set session data for id and access tokens +const extractSession = ( + req: Request, + grantScope: string[], +): AcceptOAuth2ConsentRequestSession => { + const session: AcceptOAuth2ConsentRequestSession = { + access_token: {}, + id_token: {}, + } - const id_token: { [key: string]: any } = {} + const identity = req.session?.identity + if (!identity) { + return session + } - if (consentRequest.subject && grantScopes.length > 0) { - const identity = ( - await identityApi.getIdentity({ id: consentRequest.subject }) - ).data + if (grantScope.includes("email")) { + const addresses = identity.verifiable_addresses || [] + if (addresses.length > 0) { + const address = addresses[0] + if (address.via === "email") { + session.id_token.email = address.value + session.id_token.email_verified = address.verified + } + } + } - if (grantScopes.indexOf("email") > -1) { - // Client may check email of user - id_token.email = identity.traits["email"] || "" + if (grantScope.includes("profile")) { + if (identity.traits.username) { + session.id_token.preferred_username = identity.traits.username } - if (grantScopes.indexOf("phone") > -1) { - // Client may check phone number of user - id_token.phone = identity.traits["phone"] || "" + + if (identity.traits.website) { + session.id_token.website = identity.traits.website } - } - return { - // This data will be available when introspecting the token. Try to avoid sensitive information here, - // unless you limit who can introspect tokens. - access_token: { - // foo: 'bar' - }, + if (typeof identity.traits.name === "object") { + if (identity.traits.name.first) { + session.id_token.given_name = identity.traits.name.first + } + if (identity.traits.name.last) { + session.id_token.family_name = identity.traits.name.last + } + } else if (typeof identity.traits.name === "string") { + session.id_token.name = identity.traits.name + } - // This data will be available in the ID token. - id_token, + if (identity.updated_at) { + session.id_token.updated_at = identity.updated_at + } } + return session } // A simple express handler that shows the Hydra consent screen. export const createConsentRoute: RouteCreator = - (createHelpers) => (req, res, next) => { + (createHelpers) => (req: Request, res: Response, next: NextFunction) => { res.locals.projectName = "An application requests access to your data!" - const { oauth2, identity } = createHelpers(req, res) + const { oauth2 } = createHelpers(req, res) const { consent_challenge } = req.query // The challenge is used to fetch information about the consent request from ORY hydraAdmin. @@ -140,15 +157,11 @@ export const createConsentRoute: RouteCreator = // You can apply logic here, for example grant another scope, or do whatever... // ... - let grantScope: string[] = body.requested_scope || [] + let grantScope = req.body.grant_scope if (!Array.isArray(grantScope)) { grantScope = [grantScope] } - const session = await createOAuth2ConsentRequestSession( - grantScope, - body, - identity, - ) + const session = extractSession(req, grantScope) // Now it's time to grant the consent request. You could also deny the request if something went terribly wrong return oauth2 @@ -173,11 +186,21 @@ export const createConsentRoute: RouteCreator = }) } + // this should never happen + if (!req.csrfToken) { + next( + new Error( + "Expected CSRF token middleware to be set but received none.", + ), + ) + return + } + // If consent can't be skipped we MUST show the consent UI. res.render("consent", { card: UserConsentCard({ consent: body, - csrfToken: generateToken(req, res), + csrfToken: req.csrfToken(true), cardImage: body.client?.logo_uri || "/ory-logo.svg", client_name: body.client?.client_name || "unknown client", requested_scope: body.requested_scope, @@ -192,36 +215,11 @@ export const createConsentRoute: RouteCreator = } export const createConsentPostRoute: RouteCreator = - (createHelpers) => (req, res, next) => { + (createHelpers) => async (req, res, next) => { // The challenge is a hidden input field, so we have to retrieve it from the request body - const challenge = req.body.consent_challenge - const { oauth2, identity } = createHelpers(req, res) - - // Let's see if the user decided to accept or reject the consent request.. - if (req.body.submit === "Deny access") { - // Looks like the consent request was denied by the user - return ( - oauth2 - .rejectOAuth2ConsentRequest({ - consentChallenge: challenge, - rejectOAuth2Request: { - error: "access_denied", - error_description: "The resource owner denied the request", - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the browser back to hydra! - res.redirect(String(body.redirect_to)) - }) - // This will handle any error that happens when making HTTP calls to hydra - .catch(next) - ) - } + const { oauth2 } = createHelpers(req, res) - let grantScope = req.body.grant_scope - if (!Array.isArray(grantScope)) { - grantScope = [grantScope] - } + const { consent_challenge: challenge, consent_action, remember } = req.body // Here is also the place to add data to the ID or access token. For example, // if the scope 'profile' is added, add the family and given name to the ID Token claims: @@ -229,55 +227,75 @@ export const createConsentPostRoute: RouteCreator = // session.id_token.family_name = 'Doe' // session.id_token.given_name = 'John' // } + let grantScope = req.body.grant_scope + if (!Array.isArray(grantScope)) { + grantScope = [grantScope] + } + + // extractSession only gets the sesseion data from the request + // You can extract more data from the Ory Identities admin API + const session = extractSession(req, grantScope) // Let's fetch the consent request again to be able to set `grantAccessTokenAudience` properly. - oauth2 - .getOAuth2ConsentRequest({ consentChallenge: challenge }) - // This will be called if the HTTP request was successful - .then(async ({ data: body }) => { - const session = await createOAuth2ConsentRequestSession( - grantScope, - body, - identity, - ) - return oauth2 - .acceptOAuth2ConsentRequest({ - consentChallenge: challenge, - acceptOAuth2ConsentRequest: { - // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes - // are requested accidentally. - grant_scope: grantScope, - - // If the environment variable CONFORMITY_FAKE_CLAIMS is set we are assuming that - // the app is built for the automated OpenID Connect Conformity Test Suite. You - // can peak inside the code for some ideas, but be aware that all data is fake - // and this only exists to fake a login system which works in accordance to OpenID Connect. - // - // If that variable is not set, the session will be used as-is. - session: oidcConformityMaybeFakeSession( - grantScope, - body, - session, - ), - - // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. - grant_access_token_audience: body.requested_access_token_audience, - - // This tells hydra to remember this consent request and allow the same client to request the same - // scopes from the same user, without showing the UI, in the future. - remember: Boolean(req.body.remember), - - // When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire. - remember_for: process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS - ? Number(process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS) - : 3600, - }, - }) - .then(({ data: body }) => { - // All we need to do now is to redirect the user back! - res.redirect(String(body.redirect_to)) - }) + // Let's see if the user decided to accept or reject the consent request.. + if (consent_action === "accept") { + await oauth2 + .getOAuth2ConsentRequest({ + consentChallenge: challenge, + }) + .then(async ({ data: body }) => { + return oauth2 + .acceptOAuth2ConsentRequest({ + consentChallenge: challenge, + acceptOAuth2ConsentRequest: { + // We can grant all scopes that have been requested - hydra already checked for us that no additional scopes + // are requested accidentally. + grant_scope: grantScope, + + // If the environment variable CONFORMITY_FAKE_CLAIMS is set we are assuming that + // the app is built for the automated OpenID Connect Conformity Test Suite. You + // can peak inside the code for some ideas, but be aware that all data is fake + // and this only exists to fake a login system which works in accordance to OpenID Connect. + // + // If that variable is not set, the session will be used as-is. + session: oidcConformityMaybeFakeSession(grantScope, session), + + // ORY Hydra checks if requested audiences are allowed by the client, so we can simply echo this. + grant_access_token_audience: + body.requested_access_token_audience, + + // This tells hydra to remember this consent request and allow the same client to request the same + // scopes from the same user, without showing the UI, in the future. + remember: Boolean(remember), + + // When this "remember" sesion expires, in seconds. Set this to 0 so it will never expire. + remember_for: process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS + ? Number(process.env.REMEMBER_CONSENT_SESSION_FOR_SECONDS) + : 3600, + }, + }) + .then(({ data: body }) => { + // All we need to do now is to redirect the user back! + res.redirect(String(body.redirect_to)) + }) + }) + .catch(next) + } + + // Looks like the consent request was denied by the user + await oauth2 + .rejectOAuth2ConsentRequest({ + consentChallenge: challenge, + rejectOAuth2Request: { + error: "access_denied", + error_description: "The resource owner denied the request", + }, }) + .then(({ data: body }) => { + // All we need to do now is to redirect the browser back to hydra! + res.redirect(String(body.redirect_to)) + }) + // This will handle any error that happens when making HTTP calls to hydra .catch(next) } @@ -288,7 +306,11 @@ export const registerConsentRoute: RouteRegistrator = function ( createHelpers = defaultConfig, ) { if (isOAuthCosentEnabled()) { - return app.get("/consent", createConsentRoute(createHelpers)) + return app.get( + "/consent", + doubleCsrfProtection, + createConsentRoute(createHelpers), + ) } else { return register404Route } diff --git a/src/routes/stub/oidc-cert.ts b/src/routes/stub/oidc-cert.ts index 8b3c871f..0b22b441 100644 --- a/src/routes/stub/oidc-cert.ts +++ b/src/routes/stub/oidc-cert.ts @@ -5,7 +5,6 @@ OpenID Connect Conformance test suite. You can use it for inspiration, but please do not use it in production as is. */ import { - OAuth2ConsentRequest, OAuth2LoginRequest, AcceptOAuth2ConsentRequestSession, } from "@ory/client" @@ -28,7 +27,6 @@ export const oidcConformityMaybeFakeAcr = ( export const oidcConformityMaybeFakeSession = ( grantScope: string[], - request: OAuth2ConsentRequest, session: AcceptOAuth2ConsentRequestSession, ): AcceptOAuth2ConsentRequestSession => { if (process.env.CONFORMITY_FAKE_CLAIMS !== "1") { diff --git a/types/express/index.d.ts b/types/express/index.d.ts index b02e4e9f..5fdbb911 100644 --- a/types/express/index.d.ts +++ b/types/express/index.d.ts @@ -3,5 +3,6 @@ import { Session } from "@ory/client" declare module "express" { export interface Request { session?: Session + csrfToken?: (overwrite?: boolean) => string } } From 7c9429d9a8f5dd9b00f3fdb8741ac54458b25fd4 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:30:45 +0200 Subject: [PATCH 8/8] chore: move consent enabled check --- src/pkg/index.ts | 6 +++++ src/pkg/route.ts | 6 +++++ src/routes/consent.ts | 55 ++++++++++++++++++++----------------------- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/pkg/index.ts b/src/pkg/index.ts index 34371e34..bf3eb583 100644 --- a/src/pkg/index.ts +++ b/src/pkg/index.ts @@ -32,6 +32,12 @@ export const defaultConfig: RouteOptionsCreator = () => { kratosBrowserUrl: apiBaseUrl, faviconUrl: "favico.png", faviconType: "image/png", + isOAuthConsentRouteEnabled: () => + (process.env.HYDRA_ADMIN_URL || process.env.ORY_SDK_URL) && + process.env.CSRF_COOKIE_SECRET && + process.env.CSRF_COOKIE_NAME + ? true + : false, ...sdk, } } diff --git a/src/pkg/route.ts b/src/pkg/route.ts index 74a6fb2f..8fcdd30b 100644 --- a/src/pkg/route.ts +++ b/src/pkg/route.ts @@ -10,6 +10,12 @@ export interface RouteOptions { identity: IdentityApi apiBaseUrl: string kratosBrowserUrl: string + + // Checks if OAuth2 consent route is enabled + // This is used to determine if the consent route should be registered + // We need to check if the required environment variables are set + isOAuthConsentRouteEnabled: () => boolean + logoUrl?: string faviconUrl?: string faviconType?: string diff --git a/src/routes/consent.ts b/src/routes/consent.ts index 655255d0..a8b42d67 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -1,7 +1,6 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 import { defaultConfig, logger, RouteCreator, RouteRegistrator } from "../pkg" -import { register404Route } from "./404" import { oidcConformityMaybeFakeSession } from "./stub/oidc-cert" import { AcceptOAuth2ConsentRequestSession } from "@ory/client" import { UserConsentCard } from "@ory/elements-markup" @@ -38,13 +37,6 @@ const { }, // A function that returns the token from the request }) -// Checks if OAuth2 consent is enabled -// This is used to determine if the consent route should be registered -// We need to check if the environment variables are set -const isOAuthCosentEnabled = () => - (process.env.HYDRA_ADMIN_URL || process.env.ORY_SDK_URL) && - process.env.CSRF_COOKIE_SECRET - // Error handling, validation error interception const csrfErrorHandler = ( error: unknown, @@ -123,7 +115,13 @@ export const createConsentRoute: RouteCreator = (createHelpers) => (req: Request, res: Response, next: NextFunction) => { res.locals.projectName = "An application requests access to your data!" - const { oauth2 } = createHelpers(req, res) + const { oauth2, isOAuthConsentRouteEnabled } = createHelpers(req, res) + + if (!isOAuthConsentRouteEnabled()) { + res.redirect("404") + return + } + const { consent_challenge } = req.query // The challenge is used to fetch information about the consent request from ORY hydraAdmin. @@ -217,7 +215,12 @@ export const createConsentRoute: RouteCreator = export const createConsentPostRoute: RouteCreator = (createHelpers) => async (req, res, next) => { // The challenge is a hidden input field, so we have to retrieve it from the request body - const { oauth2 } = createHelpers(req, res) + const { oauth2, isOAuthConsentRouteEnabled } = createHelpers(req, res) + + if (!isOAuthConsentRouteEnabled()) { + res.redirect("404") + return + } const { consent_challenge: challenge, consent_action, remember } = req.body @@ -305,30 +308,22 @@ export const registerConsentRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { - if (isOAuthCosentEnabled()) { - return app.get( - "/consent", - doubleCsrfProtection, - createConsentRoute(createHelpers), - ) - } else { - return register404Route - } + return app.get( + "/consent", + doubleCsrfProtection, + createConsentRoute(createHelpers), + ) } export const registerConsentPostRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { - if (isOAuthCosentEnabled()) { - return app.post( - "/consent", - parseForm, - doubleCsrfProtection, - csrfErrorHandler, - createConsentPostRoute(createHelpers), - ) - } else { - return register404Route - } + return app.post( + "/consent", + parseForm, + doubleCsrfProtection, + csrfErrorHandler, + createConsentPostRoute(createHelpers), + ) }