diff --git a/backend/package-lock.json b/backend/package-lock.json index 7d10cbf..fdf4b21 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "dotenv": "^17.3.1", "express": "^4.18.2", "express-session": "^1.17.3", + "jsonwebtoken": "^9.0.3", "openid-client": "^5.6.1", "zod": "^3.22.4" }, @@ -20,12 +21,285 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/express-session": "^1.17.10", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.2.3", "prisma": "^6.19.2", "tsx": "^4.7.0", "typescript": "^5.3.3" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", @@ -43,6 +317,159 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@prisma/client": { "version": "6.19.2", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", @@ -209,6 +636,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -216,6 +654,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.2.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", @@ -316,6 +761,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -570,6 +1021,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -828,6 +1288,21 @@ "node": ">= 0.6" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1007,6 +1482,97 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1423,6 +1989,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", diff --git a/backend/package.json b/backend/package.json index 937cd14..eea2416 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "dotenv": "^17.3.1", "express": "^4.18.2", "express-session": "^1.17.3", + "jsonwebtoken": "^9.0.3", "openid-client": "^5.6.1", "zod": "^3.22.4" }, @@ -22,6 +23,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/express-session": "^1.17.10", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.2.3", "prisma": "^6.19.2", "tsx": "^4.7.0", diff --git a/backend/src/auth/jwt.ts b/backend/src/auth/jwt.ts new file mode 100644 index 0000000..39ac82e --- /dev/null +++ b/backend/src/auth/jwt.ts @@ -0,0 +1,45 @@ +import jwt from 'jsonwebtoken'; +import { config } from '../config'; +import type { AuthenticatedUser } from '../types'; + +export interface JwtPayload { + sub: string; + username: string; + fullName: string | null; + email: string; +} + +/** + * Mint a backend-signed JWT for a native (iOS) client. + * The token is self-contained — no IDP call is needed to verify it. + */ +export function signBackendJwt(user: AuthenticatedUser): string { + const payload: JwtPayload = { + sub: user.id, + username: user.username, + fullName: user.fullName, + email: user.email, + }; + + return jwt.sign(payload, config.jwt.secret, { + expiresIn: config.jwt.expiresIn, + algorithm: 'HS256', + }); +} + +/** + * Verify a backend-signed JWT and return the encoded user. + * Throws if the token is invalid or expired. + */ +export function verifyBackendJwt(token: string): AuthenticatedUser { + const payload = jwt.verify(token, config.jwt.secret, { + algorithms: ['HS256'], + }) as JwtPayload; + + return { + id: payload.sub, + username: payload.username, + fullName: payload.fullName, + email: payload.email, + }; +} diff --git a/backend/src/auth/oidc.ts b/backend/src/auth/oidc.ts index 0ef0a3f..8e3bfab 100644 --- a/backend/src/auth/oidc.ts +++ b/backend/src/auth/oidc.ts @@ -2,6 +2,9 @@ import { Issuer, generators, Client, TokenSet } from 'openid-client'; import { config } from '../config'; import type { AuthenticatedUser } from '../types'; +// Note: bearer-token (JWT) verification for native clients lives in auth/jwt.ts. +// This module is responsible solely for the OIDC protocol flows. + let oidcClient: Client | null = null; export async function initializeOIDC(): Promise { @@ -160,48 +163,3 @@ export async function getUserInfo(tokenSet: TokenSet): Promise { - try { - const client = getOIDCClient(); - await client.userinfo(tokenSet); - return true; - } catch { - return false; - } -} - -// Cache userinfo responses to avoid hitting the OIDC provider on every request. -// Entries expire after 5 minutes. The cache is keyed by the raw access token. -const userinfoCache = new Map(); -const USERINFO_CACHE_TTL_MS = 5 * 60 * 1000; - -export async function verifyBearerToken(accessToken: string): Promise { - const cached = userinfoCache.get(accessToken); - if (cached && Date.now() < cached.expiresAt) { - return cached.user; - } - - const client = getOIDCClient(); - - let userInfo: Awaited>; - try { - userInfo = await client.userinfo(accessToken); - } catch (err) { - // Remove any stale cache entry for this token - userinfoCache.delete(accessToken); - throw err; - } - - const id = String(userInfo.sub); - const username = String(userInfo.preferred_username || userInfo.name || id); - const email = String(userInfo.email || ''); - const fullName = String(userInfo.name || '') || null; - - if (!email) { - throw new Error('Email not provided by OIDC provider'); - } - - const user: AuthenticatedUser = { id, username, fullName, email }; - userinfoCache.set(accessToken, { user, expiresAt: Date.now() + USERINFO_CACHE_TTL_MS }); - return user; -} \ No newline at end of file diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 8361038..53c3fcb 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -25,6 +25,13 @@ export const config = { maxAge: 24 * 60 * 60 * 1000, // 24 hours }, + jwt: { + // Dedicated secret for backend-issued JWTs. Falls back to SESSION_SECRET so + // existing single-secret deployments work without any config change. + secret: process.env.JWT_SECRET || process.env.SESSION_SECRET || "default-secret-change-in-production", + expiresIn: 30 * 24 * 60 * 60, // 30 days in seconds + }, + cors: { origin: process.env.APP_URL || "http://localhost:5173", credentials: true, diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 05ced03..4a9dd8c 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { prisma } from '../prisma/client'; import type { AuthenticatedRequest, AuthenticatedUser } from '../types'; -import { getOIDCClient, verifyBearerToken } from '../auth/oidc'; +import { verifyBackendJwt } from '../auth/jwt'; export async function requireAuth( req: AuthenticatedRequest, @@ -14,18 +14,16 @@ export async function requireAuth( return next(); } - // 2. Bearer token auth (iOS / native clients) + // 2. Bearer JWT auth (iOS / native clients) const authHeader = req.headers.authorization; - console.log('[requireAuth] authorization header:', authHeader ? `${authHeader.slice(0, 20)}…` : '(none)'); if (authHeader?.startsWith('Bearer ')) { - const accessToken = authHeader.slice(7); + const token = authHeader.slice(7); try { - const user = await verifyBearerToken(accessToken); - req.user = user; + // Verify the backend-signed JWT locally — no IDP network call needed. + req.user = verifyBackendJwt(token); return next(); } catch (err) { const message = err instanceof Error ? err.message : String(err); - console.error('[requireAuth] verifyBearerToken failed:', err); res.status(401).json({ error: `Unauthorized: ${message}` }); return; } diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 4fe9111..d502c03 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -7,8 +7,9 @@ import { exchangeNativeCode, getUserInfo, } from "../auth/oidc"; +import { signBackendJwt } from "../auth/jwt"; import { requireAuth, syncUser } from "../middleware/auth"; -import type { AuthenticatedRequest, AuthenticatedUser } from "../types"; +import type { AuthenticatedRequest } from "../types"; import type { AuthSession } from "../auth/oidc"; const router = Router(); @@ -119,9 +120,10 @@ router.get("/me", requireAuth, (req: AuthenticatedRequest, res) => { res.json(req.user); }); -// POST /auth/token - Exchange authorization code for tokens (native app flow) -// Session state is retrieved from the in-memory store by state value, so no -// session cookie is required from the native client. +// POST /auth/token - Exchange OIDC authorization code for a backend JWT (native app flow). +// The iOS app calls this after the OIDC redirect; it receives a backend-signed JWT which +// it then uses as a Bearer token for all subsequent API requests. The backend verifies +// this JWT locally — no per-request IDP call is needed. router.post("/token", async (req, res) => { try { await ensureOIDC(); @@ -140,15 +142,16 @@ router.post("/token", async (req, res) => { } const tokenSet = await exchangeNativeCode(code, oidcSession.codeVerifier, redirect_uri); - const user = await getUserInfo(tokenSet); await syncUser(user); + // Mint a backend JWT. The iOS app stores this and sends it as Bearer . + const backendJwt = signBackendJwt(user); + res.json({ - access_token: tokenSet.access_token, - id_token: tokenSet.id_token, - token_type: tokenSet.token_type, - expires_in: tokenSet.expires_in, + access_token: backendJwt, + token_type: "Bearer", + expires_in: 30 * 24 * 60 * 60, // 30 days user, }); } catch (error) { diff --git a/ios/TimeTracker/TimeTracker/Core/Auth/AuthManager.swift b/ios/TimeTracker/TimeTracker/Core/Auth/AuthManager.swift index 2739349..0be4157 100644 --- a/ios/TimeTracker/TimeTracker/Core/Auth/AuthManager.swift +++ b/ios/TimeTracker/TimeTracker/Core/Auth/AuthManager.swift @@ -11,6 +11,7 @@ final class AuthManager: ObservableObject { private let keychain: Keychain private let apiClient = APIClient() + /// The backend-issued JWT. Sent as `Authorization: Bearer ` on every API call. var accessToken: String? { get { try? keychain.get(AppConstants.KeychainKeys.accessToken) } set { @@ -22,17 +23,6 @@ final class AuthManager: ObservableObject { } } - var idToken: String? { - get { try? keychain.get(AppConstants.KeychainKeys.idToken) } - set { - if let value = newValue { - try? keychain.set(value, key: AppConstants.KeychainKeys.idToken) - } else { - try? keychain.remove(AppConstants.KeychainKeys.idToken) - } - } - } - private init() { self.keychain = Keychain(service: "com.timetracker.app") .accessibility(.whenUnlockedThisDeviceOnly) @@ -66,7 +56,9 @@ final class AuthManager: ObservableObject { } func logout() async throws { - try await apiClient.requestVoid( + // Best-effort server-side logout; the backend JWT is stateless so the + // real security comes from clearing the local token. + try? await apiClient.requestVoid( endpoint: APIEndpoint.logout, method: .post, authenticated: true @@ -76,14 +68,12 @@ final class AuthManager: ObservableObject { func clearAuth() { accessToken = nil - idToken = nil currentUser = nil isAuthenticated = false } func handleTokenResponse(_ response: TokenResponse) async { accessToken = response.accessToken - idToken = response.idToken currentUser = response.user isAuthenticated = true } diff --git a/ios/TimeTracker/TimeTracker/Core/Auth/AuthService.swift b/ios/TimeTracker/TimeTracker/Core/Auth/AuthService.swift index f6e9c3b..8b17a90 100644 --- a/ios/TimeTracker/TimeTracker/Core/Auth/AuthService.swift +++ b/ios/TimeTracker/TimeTracker/Core/Auth/AuthService.swift @@ -31,8 +31,8 @@ final class AuthService: NSObject { let callbackScheme = URL(string: AppConfig.authCallbackURL)?.scheme ?? "timetracker" - // Use a shared (non-ephemeral) session so the backend session cookie set during - // /auth/login is automatically included in the /auth/token POST. + // Use an ephemeral session — we only need the redirect URL back with the + // authorization code; no cookies or shared state are needed. let webAuthSession = ASWebAuthenticationSession( url: authURL, callbackURLScheme: callbackScheme @@ -69,9 +69,8 @@ final class AuthService: NSObject { } webAuthSession.presentationContextProvider = self - // prefersEphemeralWebBrowserSession = false ensures the session cookie from - // /auth/login is retained and sent with the subsequent /auth/token request. - webAuthSession.prefersEphemeralWebBrowserSession = false + // Ephemeral session: no shared cookies or browsing data with Safari. + webAuthSession.prefersEphemeralWebBrowserSession = true self.authSession = webAuthSession @@ -138,8 +137,8 @@ final class AuthService: NSObject { request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - // state is sent so the backend can look up and validate the original session. - // code_verifier is not sent — the backend uses its own verifier from the session. + // state is sent so the backend can look up the original PKCE session. + // code_verifier is NOT sent — the backend holds it in the in-memory session. let body: [String: Any] = [ "code": code, "state": state, @@ -198,14 +197,12 @@ extension Notification.Name { struct TokenResponse: Codable { let accessToken: String - let idToken: String? let tokenType: String let expiresIn: Int? let user: User enum CodingKeys: String, CodingKey { case accessToken = "access_token" - case idToken = "id_token" case tokenType = "token_type" case expiresIn = "expires_in" case user diff --git a/ios/TimeTracker/TimeTracker/Core/Constants.swift b/ios/TimeTracker/TimeTracker/Core/Constants.swift index 123180b..fa3920a 100644 --- a/ios/TimeTracker/TimeTracker/Core/Constants.swift +++ b/ios/TimeTracker/TimeTracker/Core/Constants.swift @@ -13,7 +13,6 @@ enum AppConstants { enum KeychainKeys { static let accessToken = "accessToken" - static let idToken = "idToken" } }