diff --git a/README.md b/README.md index fdedcbac..1f03aae0 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,53 @@ 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_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. +- `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. +- `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. + +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,9 +89,12 @@ 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 ``` +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/nodemon.json b/nodemon.json index 7432e790..9d2904f6 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,5 +1,12 @@ { "watch": ["src"], "ext": "ts", - "exec": "ts-node ./src/index.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", + "ORY_SDK_URL": "http://localhost:4000" + } } diff --git a/package-lock.json b/package-lock.json index b5007fd4..095c3664 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/client": "1.2.4", "@ory/elements-markup": "0.1.0-beta.4", "@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,7 +44,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", @@ -170,9 +169,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 +195,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 +221,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 +235,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 +266,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 +292,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 +362,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 +374,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 +385,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 +395,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 +449,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 +461,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 +772,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" } @@ -984,9 +983,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 +999,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 +1136,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 +1179,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 +1217,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 +1353,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 +1634,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 +1853,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", + "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": { - "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==", - "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 +2069,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 +4617,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 +4897,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 +5084,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 +5515,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 +5532,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 +5549,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 +5841,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 +5935,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 +5959,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..6d05573e 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/client": "1.2.4", "@ory/elements-markup": "0.1.0-beta.4", "@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/index.ts b/src/index.ts index 18a5c366..380ef342 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,36 @@ 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_NAME || "").length === 0 || + 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_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=...`", + ) } 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/index.ts b/src/pkg/index.ts index 491c6761..bf3eb583 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" @@ -31,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, } } @@ -84,7 +91,7 @@ export const redirectOnSoftError = 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/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/pkg/sdk/index.ts b/src/pkg/sdk/index.ts index cc488fe2..3e9ce5ba 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,12 +26,22 @@ 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( new Configuration({ basePath: apiBaseIdentityUrl, + ...(process.env.ORY_ADMIN_API_TOKEN && { + accessToken: process.env.ORY_ADMIN_API_TOKEN, + }), }), ), } 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..a8b42d67 100644 --- a/src/routes/consent.ts +++ b/src/routes/consent.ts @@ -1,60 +1,127 @@ // Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 -import { defaultConfig, RouteCreator, RouteRegistrator } from "../pkg" -import { register404Route } from "./404" +import { defaultConfig, logger, RouteCreator, RouteRegistrator } from "../pkg" 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 csrf from "csurf" +import { doubleCsrf, DoubleCsrfCookieOptions } from "csrf-csrf" +import { Request, Response, NextFunction } from "express" -async function createOAuth2ConsentRequestSession( - grantScopes: string[], - consentRequest: OAuth2ConsentRequest, - identityApi: IdentityApi, -): Promise { - // The session allows us to set session data for id and access tokens +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 id_token: { [key: string]: any } = {} +const cookieName = process.env.CSRF_COOKIE_NAME || "__Host-ax-x-csrf-token" +const cookieSecret = process.env.CSRF_COOKIE_SECRET - if (consentRequest.subject && grantScopes.length > 0) { - const identity = ( - await identityApi.getIdentity({ id: consentRequest.subject }) - ).data +// Sets up csrf protection +const { + 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", "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 +}) - if (grantScopes.indexOf("email") > -1) { - // Client may check email of user - id_token.email = identity.traits["email"] || "" - } - if (grantScopes.indexOf("phone") > -1) { - // Client may check phone number of user - id_token.phone = identity.traits["phone"] || "" +// Error handling, validation error interception +const csrfErrorHandler = ( + error: unknown, + req: Request, + res: Response, + 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.", + ), + ) + } else { + next() + } +} + +const extractSession = ( + req: Request, + grantScope: string[], +): AcceptOAuth2ConsentRequestSession => { + const session: AcceptOAuth2ConsentRequestSession = { + access_token: {}, + id_token: {}, + } + + const identity = req.session?.identity + if (!identity) { + return session + } + + 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 + } } } - 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 (grantScope.includes("profile")) { + if (identity.traits.username) { + session.id_token.preferred_username = identity.traits.username + } + + if (identity.traits.website) { + session.id_token.website = identity.traits.website + } + + 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) => { - console.log("createConsentRoute") + (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, 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. @@ -71,7 +138,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 @@ -89,15 +155,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 @@ -122,11 +184,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: req.csrfToken(), + 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, @@ -141,36 +213,16 @@ 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) + const { oauth2, isOAuthConsentRouteEnabled } = 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) - ) + if (!isOAuthConsentRouteEnabled()) { + res.redirect("404") + return } - 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: @@ -178,95 +230,100 @@ 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_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) } -// Sets up csrf protection -const csrfProtection = csrf({ - cookie: { - sameSite: "lax", - }, -}) - var parseForm = bodyParser.urlencoded({ extended: false }) export const registerConsentRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { - if (process.env.HYDRA_ADMIN_URL) { - console.log("found HYDRA_ADMIN_URL") - return app.get( - "/consent", - csrfProtection, - createConsentRoute(createHelpers), - ) - } else { - return register404Route - } + return app.get( + "/consent", + doubleCsrfProtection, + createConsentRoute(createHelpers), + ) } export const registerConsentPostRoute: RouteRegistrator = function ( app, createHelpers = defaultConfig, ) { - if (process.env.HYDRA_ADMIN_URL) { - return app.post( - "/consent", - parseForm, - csrfProtection, - createConsentPostRoute(createHelpers), - ) - } else { - return register404Route - } + return app.post( + "/consent", + parseForm, + doubleCsrfProtection, + csrfErrorHandler, + createConsentPostRoute(createHelpers), + ) } diff --git a/src/routes/sessions.ts b/src/routes/sessions.ts index 08f5e526..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 !== "" @@ -41,18 +43,17 @@ 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>( - (traits, [key, value]) => { - traits[key] = - typeof value === "object" ? JSON.stringify(value) : value - return traits - }, - {}, - ), - "signup date": session?.identity.created_at || "", + ...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" ? "two-factor used (aal2)" 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 } }