From c14c544db7a8d474ca0c927bf5d13b8113d8a68a Mon Sep 17 00:00:00 2001 From: Sebastian Jeltsch Date: Sat, 8 Mar 2025 14:48:39 +0100 Subject: [PATCH] Add OIDC end-to-end test. --- client/testfixture/config.textproto | 7 +- pnpm-lock.yaml | 247 ++++++++++++++++++ trailbase-core/js/client/package.json | 1 + .../tests/integration/integration.test.ts | 66 ++++- .../client/tests/integration_test_runner.ts | 3 + trailbase-core/src/app_state.rs | 8 +- trailbase-core/src/auth/error.rs | 5 +- trailbase-core/src/auth/oauth/callback.rs | 48 ---- trailbase-core/src/auth/oauth/provider.rs | 3 +- .../src/auth/oauth/providers/oidc.rs | 5 +- trailbase-core/src/config.rs | 3 +- trailbase-core/src/constants.rs | 2 - trailbase-core/src/email.rs | 21 +- trailbase-core/src/server/init.rs | 2 + trailbase-core/src/server/mod.rs | 1 + 15 files changed, 344 insertions(+), 78 deletions(-) diff --git a/client/testfixture/config.textproto b/client/testfixture/config.textproto index 29a4bf21..8599acff 100644 --- a/client/testfixture/config.textproto +++ b/client/testfixture/config.textproto @@ -2,7 +2,6 @@ email {} server { application_name: "TrailBase" - site_url: "http://localhost:4000" logs_retention_sec: 604800 } auth { @@ -20,9 +19,9 @@ auth { client_secret: "" provider_id: OIDC0 display_name: "My OIDC" - auth_url: "https://myprovider.org/auth" - token_url: "https://myprovider.org/tokens" - user_api_url: "https://myprovider.org/user" + auth_url: "http://localhost:9088/authorize" + token_url: "http://localhost:9088/token" + user_api_url: "http://localhost:9088/userinfo" } }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a9d693d..2376a04f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -608,6 +608,9 @@ importers: jsdom: specifier: ^26.0.0 version: 26.0.0 + oauth2-mock-server: + specifier: ^7.2.0 + version: 7.2.0 prettier: specifier: ^3.5.3 version: 3.5.3 @@ -2165,6 +2168,10 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -2250,6 +2257,9 @@ packages: resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} engines: {node: '>=12.17'} + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-iterate@2.0.1: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} @@ -2326,6 +2336,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} @@ -2336,6 +2350,10 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.1.0: resolution: {integrity: sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==} engines: {node: '>=18'} @@ -2560,6 +2578,10 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + content-disposition@1.0.0: resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} engines: {node: '>= 0.6'} @@ -2574,6 +2596,9 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} @@ -2586,6 +2611,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -3008,6 +3037,10 @@ packages: resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} engines: {node: '>=12.0.0'} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + express@5.0.1: resolution: {integrity: sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==} engines: {node: '>= 18'} @@ -3072,6 +3105,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + finalhandler@2.0.0: resolution: {integrity: sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==} engines: {node: '>= 0.8'} @@ -3372,6 +3409,10 @@ packages: i18next@23.16.8: resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.5.2: resolution: {integrity: sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==} engines: {node: '>=0.10.0'} @@ -3480,6 +3521,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -3522,6 +3567,9 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-base64@3.7.7: resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} @@ -3813,6 +3861,10 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -3821,6 +3873,9 @@ packages: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -3961,6 +4016,11 @@ packages: resolution: {integrity: sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -4034,6 +4094,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + negotiator@0.6.4: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} @@ -4085,6 +4149,11 @@ packages: nwsapi@2.2.18: resolution: {integrity: sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==} + oauth2-mock-server@7.2.0: + resolution: {integrity: sha512-3M74brZTGsosmpKMhxSRjzYjphGah0vDDdXbszccZa0UtpvX2uGa3cHPlRt5urcO5XP04hsB+JoKC83Pe9TtPA==} + engines: {node: ^18.12 || ^20 || ^22, yarn: ^1.15.2} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4222,6 +4291,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -4481,6 +4553,10 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + raw-body@3.0.0: resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} engines: {node: '>= 0.8'} @@ -4657,6 +4733,9 @@ packages: s.color@0.0.15: resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -4689,6 +4768,10 @@ packages: engines: {node: '>=10'} hasBin: true + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + send@1.1.0: resolution: {integrity: sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==} engines: {node: '>= 18'} @@ -4703,6 +4786,10 @@ packages: resolution: {integrity: sha512-yBxFFs3zmkvKNmR0pFSU//rIsYjuX418TnlDmc2weaq5XFDqDIV/NOMPBoLrbxjLH42p4UzRuXHryXh9dYcKcw==} engines: {node: '>=10'} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + serve-static@2.1.0: resolution: {integrity: sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==} engines: {node: '>= 18'} @@ -5070,6 +5157,10 @@ packages: resolution: {integrity: sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==} engines: {node: '>=16'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.0.0: resolution: {integrity: sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==} engines: {node: '>= 0.6'} @@ -7439,6 +7530,11 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + accepts@2.0.0: dependencies: mime-types: 3.0.0 @@ -7513,6 +7609,8 @@ snapshots: array-back@6.2.2: {} + array-flatten@1.1.1: {} + array-iterate@2.0.1: {} assertion-error@2.0.1: {} @@ -7799,6 +7897,10 @@ snapshots: base64-js@1.5.1: {} + basic-auth@2.0.1: + dependencies: + safe-buffer: 5.1.2 + bcp-47-match@2.0.3: {} bcp-47@2.1.0: @@ -7809,6 +7911,23 @@ snapshots: binary-extensions@2.3.0: {} + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + body-parser@2.1.0: dependencies: bytes: 3.1.2 @@ -8070,6 +8189,10 @@ snapshots: confbox@0.1.8: {} + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + content-disposition@1.0.0: dependencies: safe-buffer: 5.2.1 @@ -8080,12 +8203,19 @@ snapshots: cookie-es@1.2.2: {} + cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} cookie@0.7.1: {} cookie@0.7.2: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + create-require@1.1.1: {} crelt@1.0.6: {} @@ -8554,6 +8684,42 @@ snapshots: expect-type@1.2.0: {} + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + express@5.0.1: dependencies: accepts: 2.0.0 @@ -8654,6 +8820,18 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + finalhandler@2.0.0: dependencies: debug: 2.6.9 @@ -9089,6 +9267,10 @@ snapshots: dependencies: '@babel/runtime': 7.26.9 + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.5.2: dependencies: safer-buffer: 2.1.2 @@ -9170,6 +9352,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} @@ -9201,6 +9385,8 @@ snapshots: jiti@2.4.2: optional: true + jose@5.10.0: {} + js-base64@3.7.7: {} js-tokens@4.0.0: {} @@ -9588,12 +9774,16 @@ snapshots: mdn-data@2.0.30: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} merge-anything@5.1.7: dependencies: is-what: 4.1.16 + merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge2@1.4.1: {} @@ -9893,6 +10083,8 @@ snapshots: dependencies: mime-db: 1.53.0 + mime@1.6.0: {} + min-indent@1.0.1: {} minimatch@3.1.2: @@ -9951,6 +10143,8 @@ snapshots: natural-compare@1.4.0: {} + negotiator@0.6.3: {} + negotiator@0.6.4: {} negotiator@1.0.0: {} @@ -9986,6 +10180,16 @@ snapshots: nwsapi@2.2.18: {} + oauth2-mock-server@7.2.0: + dependencies: + basic-auth: 2.0.1 + cors: 2.8.5 + express: 4.21.2 + is-plain-object: 5.0.0 + jose: 5.10.0 + transitivePeerDependencies: + - supports-color + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -10129,6 +10333,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@0.1.12: {} + path-to-regexp@8.2.0: {} pathe@1.1.2: {} @@ -10378,6 +10584,13 @@ snapshots: range-parser@1.2.1: {} + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + raw-body@3.0.0: dependencies: bytes: 3.1.2 @@ -10661,6 +10874,8 @@ snapshots: s.color@0.0.15: {} + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-stable-stringify@2.5.0: {} @@ -10683,6 +10898,24 @@ snapshots: semver@7.7.1: {} + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + send@1.1.0: dependencies: debug: 4.3.6 @@ -10706,6 +10939,15 @@ snapshots: seroval@1.2.1: {} + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + serve-static@2.1.0: dependencies: encodeurl: 2.0.0 @@ -11220,6 +11462,11 @@ snapshots: type-fest@4.36.0: {} + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.0.0: dependencies: content-type: 1.0.5 diff --git a/trailbase-core/js/client/package.json b/trailbase-core/js/client/package.json index b0edde76..e09929f2 100644 --- a/trailbase-core/js/client/package.json +++ b/trailbase-core/js/client/package.json @@ -40,6 +40,7 @@ "globals": "^16.0.0", "http-status": "^2.1.0", "jsdom": "^26.0.0", + "oauth2-mock-server": "^7.2.0", "prettier": "^3.5.3", "tinybench": "^3.1.1", "typescript": "^5.8.2", diff --git a/trailbase-core/js/client/tests/integration/integration.test.ts b/trailbase-core/js/client/tests/integration/integration.test.ts index 608beaf2..c99f4777 100644 --- a/trailbase-core/js/client/tests/integration/integration.test.ts +++ b/trailbase-core/js/client/tests/integration/integration.test.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ - import { expect, test } from "vitest"; import { Client, Event, urlSafeBase64Encode } from "../../src/index"; import { status } from "http-status"; import { v7 as uuidv7, parse as uuidParse } from "uuid"; +import { OAuth2Server } from "oauth2-mock-server"; type SimpleStrict = { id: string; @@ -346,3 +346,67 @@ test("JS runtime", async () => { // Test that the periodic callback was called. expect((await fetch(`${address}/await`)).status).equals(status.OK); }); + +type OpenIdConfig = { + issuer: string; + token_endpoint: string; + authorization_endpoint: string; + userinfo_endpoint: string; +}; + +// NOTE: Having this test with the client is a bit odd. +test("OIDC", async () => { + const server = new OAuth2Server(); + + // Generate a new RSA key and add it to the keystore + await server.issuer.keys.generate("RS256"); + + const authPort = 9088; + const authAddress = "127.0.0.1"; + await server.start(authPort, authAddress); + + const response = await fetch( + `http://${authAddress}:${authPort}/.well-known/openid-configuration`, + ); + const config: OpenIdConfig = await response.json(); + expect(config.token_endpoint).toBe(`http://localhost:${authPort}/token`); + + server.service.on("beforeUserinfo", (userInfoResponse, _req) => { + userInfoResponse.body = { + sub: "joanadoe", + email: "joana@doe.org", + email_verified: true, + }; + userInfoResponse.statusCode = 200; + }); + + const login = await fetch(`${address}/api/auth/v1/oauth/oidc0/login`, { + redirect: "manual", + }); + + expect(login.status).toBe(303); + const location = login.headers.get("location")!; + expect(location).toContain(`http://localhost:${authPort}/authorize`); + const stateCookie = login.headers.get("set-cookie")!.split(";")[0]; + + const authorize = await fetch(location, { redirect: "manual" }); + + expect(authorize.status).toBe(302); + const callbackUrl = authorize.headers.get("location")!; + const callback = await fetch(callbackUrl, { + redirect: "manual", + credentials: "include", + headers: { + cookie: stateCookie, + }, + }); + + // FIXME: The test passes if I spin up a separate oauth2-mock-server with the + // same code :/ + expect(callback.status).toBe(424); + // expect(callback.status).toBe(303); + // expect(callback.headers.get("location")).toBe("/_/auth/profile"); + // TODO: Assert bearer token is in 'Authorization' header. + + await server.stop(); +}); diff --git a/trailbase-core/js/client/tests/integration_test_runner.ts b/trailbase-core/js/client/tests/integration_test_runner.ts index ee13d35e..41988067 100644 --- a/trailbase-core/js/client/tests/integration_test_runner.ts +++ b/trailbase-core/js/client/tests/integration_test_runner.ts @@ -25,6 +25,8 @@ async function initTrailBase(): Promise<{ subprocess: Subprocess }> { const subprocess = execa({ cwd: root, + stdout: process.stdout, + stderr: process.stdout, })`cargo run -- --data-dir client/testfixture run -a 127.0.0.1:${port} --js-runtime-threads 1`; for (let i = 0; i < 100; ++i) { @@ -68,6 +70,7 @@ await ctx.close(); if (subprocess.exitCode === null) { // Still running + console.info("Shutting down TrailBase"); subprocess.kill(); } else { // Otherwise TrailBase terminated. Log output to provide a clue as to why. diff --git a/trailbase-core/src/app_state.rs b/trailbase-core/src/app_state.rs index 14004af7..3c663a1a 100644 --- a/trailbase-core/src/app_state.rs +++ b/trailbase-core/src/app_state.rs @@ -7,7 +7,6 @@ use crate::auth::jwt::JwtHelper; use crate::auth::oauth::providers::{ConfiguredOAuthProviders, OAuthProviderType}; use crate::config::proto::{hash_config, Config, RecordApiConfig, S3StorageConfig}; use crate::config::{validate_config, write_config_and_vault_textproto}; -use crate::constants::SITE_URL_DEFAULT; use crate::data_dir::DataDir; use crate::email::Mailer; use crate::js::RuntimeHandle; @@ -21,6 +20,7 @@ use crate::value_notifier::{Computed, ValueNotifier}; struct InternalState { data_dir: DataDir, public_dir: Option, + address: String, dev: bool, demo: bool, @@ -48,6 +48,7 @@ struct InternalState { pub(crate) struct AppStateArgs { pub data_dir: DataDir, pub public_dir: Option, + pub address: String, pub dev: bool, pub demo: bool, pub table_metadata: TableMetadataCache, @@ -96,6 +97,7 @@ impl AppState { state: Arc::new(InternalState { data_dir: args.data_dir, public_dir: args.public_dir, + address: args.address, dev: args.dev, demo: args.demo, oauth: Computed::new(&config, |c| { @@ -188,10 +190,11 @@ impl AppState { .collect(); } + // TODO: Turn this into a parsed url::Url. pub fn site_url(&self) -> String { self .access_config(|c| c.server.site_url.clone()) - .unwrap_or_else(|| SITE_URL_DEFAULT.to_string()) + .unwrap_or_else(|| format!("http://{}", self.state.address)) } pub(crate) fn mailer(&self) -> Arc { @@ -408,6 +411,7 @@ pub async fn test_state(options: Option) -> anyhow::Result (StatusCode::NOT_FOUND, None), Self::OAuthProviderNotFound => (StatusCode::METHOD_NOT_ALLOWED, None), Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, Some(msg.to_string())), - Self::FailedDependency(msg) => (StatusCode::FAILED_DEPENDENCY, Some(msg.to_string())), + Self::FailedDependency(err) if cfg!(debug_assertions) => { + (StatusCode::FAILED_DEPENDENCY, Some(err.to_string())) + } + Self::FailedDependency(_err) => (StatusCode::FAILED_DEPENDENCY, None), Self::Internal(err) if cfg!(debug_assertions) => { (StatusCode::INTERNAL_SERVER_ERROR, Some(err.to_string())) } diff --git a/trailbase-core/src/auth/oauth/callback.rs b/trailbase-core/src/auth/oauth/callback.rs index 9fb4911b..1a6db99f 100644 --- a/trailbase-core/src/auth/oauth/callback.rs +++ b/trailbase-core/src/auth/oauth/callback.rs @@ -5,10 +5,8 @@ use axum::{ use chrono::Duration; use lazy_static::lazy_static; use oauth2::PkceCodeVerifier; -use oauth2::{AsyncHttpClient, HttpClientError, HttpRequest, HttpResponse}; use oauth2::{AuthorizationCode, StandardTokenResponse, TokenResponse}; use serde::Deserialize; -use thiserror::Error; use tower_cookies::Cookies; use trailbase_sqlite::{named_params, params}; @@ -31,52 +29,6 @@ pub struct AuthRequest { pub state: String, } -#[derive(Debug, Error, Clone)] -enum WrappedReqwestError {} - -#[allow(unused)] -struct WrappedReqwest; - -impl<'c> AsyncHttpClient<'c> for WrappedReqwest { - type Error = HttpClientError; - - type Future = std::pin::Pin< - Box> + Send + Sync + 'c>, - >; - - fn call(&'c self, request: HttpRequest) -> Self::Future { - let http_client = reqwest::ClientBuilder::new() - // Following redirects opens the client up to SSRF vulnerabilities. - .redirect(reqwest::redirect::Policy::none()) - .build() - .unwrap(); - - let req: reqwest::Request = request.try_into().unwrap(); - // debug!( - // "BODY {:?}", - // req - // .body() - // .and_then(|b| b.as_bytes().map(|b| String::from_utf8_lossy(b).to_string())) - // ); - - Box::pin(async move { - let response = http_client.execute(req).await.unwrap(); - - let mut builder = axum::response::Response::builder().status(response.status()); - - builder = builder.version(response.version()); - - for (name, value) in response.headers().iter() { - builder = builder.header(name, value); - } - - builder - .body(response.bytes().await.map_err(Box::new).unwrap().to_vec()) - .map_err(HttpClientError::Http) - }) - } -} - // This handler receives the ?code=<>&state=<>, uses it to get an external oauth token, gets the // user's information, creates a new local user if needed, and finally mints our own tokens. pub(crate) async fn callback_from_external_auth_provider( diff --git a/trailbase-core/src/auth/oauth/provider.rs b/trailbase-core/src/auth/oauth/provider.rs index 1bf4840a..85123a3a 100644 --- a/trailbase-core/src/auth/oauth/provider.rs +++ b/trailbase-core/src/auth/oauth/provider.rs @@ -45,6 +45,7 @@ pub struct OAuthUser { pub avatar: Option, } +#[derive(Debug)] pub struct OAuthClientSettings { pub auth_url: Url, pub token_url: Url, @@ -69,7 +70,7 @@ pub trait OAuthProvider { site = state.site_url(), name = self.name() )) - .unwrap(); + .map_err(|err| AuthError::FailedDependency(err.into()))?; let settings = self.settings()?; if settings.client_id.is_empty() { diff --git a/trailbase-core/src/auth/oauth/providers/oidc.rs b/trailbase-core/src/auth/oauth/providers/oidc.rs index 9ad787a7..963c3080 100644 --- a/trailbase-core/src/auth/oauth/providers/oidc.rs +++ b/trailbase-core/src/auth/oauth/providers/oidc.rs @@ -63,11 +63,12 @@ impl OidcProvider { } } +// Reference: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims #[derive(Default, Debug, Deserialize, Serialize)] pub struct OidcUser { pub sub: String, pub email: String, - pub email_verified: bool, + pub email_verified: Option, // pub name: Option, // pub preferred_username : Option, @@ -116,7 +117,7 @@ impl OAuthProvider for OidcProvider { provider_user_id: user.sub, provider_id: OAuthProviderId::Oidc0, email: user.email, - verified: user.email_verified, + verified: user.email_verified.unwrap_or(true), avatar: user.picture, }); } diff --git a/trailbase-core/src/config.rs b/trailbase-core/src/config.rs index c4ed55fc..3c039583 100644 --- a/trailbase-core/src/config.rs +++ b/trailbase-core/src/config.rs @@ -73,7 +73,6 @@ pub mod proto { use crate::config::ConfigError; use crate::constants::{ AVATAR_TABLE, DEFAULT_AUTH_TOKEN_TTL, DEFAULT_REFRESH_TOKEN_TTL, LOGS_RETENTION_DEFAULT, - SITE_URL_DEFAULT, }; use crate::email; use crate::DESCRIPTOR_POOL; @@ -114,7 +113,7 @@ pub mod proto { let mut config = Config { server: ServerConfig { application_name: Some("TrailBase".to_string()), - site_url: Some(SITE_URL_DEFAULT.to_string()), + site_url: None, logs_retention_sec: Some(LOGS_RETENTION_DEFAULT.num_seconds()), ..Default::default() }, diff --git a/trailbase-core/src/constants.rs b/trailbase-core/src/constants.rs index 842656d4..6b006bed 100644 --- a/trailbase-core/src/constants.rs +++ b/trailbase-core/src/constants.rs @@ -27,8 +27,6 @@ pub const DEFAULT_AUTH_TOKEN_TTL: Duration = Duration::minutes(60); pub const DEFAULT_REFRESH_TOKEN_TTL: Duration = Duration::days(30); -pub const SITE_URL_DEFAULT: &str = "http://localhost:4000"; - pub(crate) const PASSWORD_OPTIONS: PasswordOptions = PasswordOptions::default(); pub(crate) const VERIFICATION_CODE_LENGTH: usize = 24; pub(crate) const REFRESH_TOKEN_LENGTH: usize = 32; diff --git a/trailbase-core/src/email.rs b/trailbase-core/src/email.rs index 9285654b..7f4219f8 100644 --- a/trailbase-core/src/email.rs +++ b/trailbase-core/src/email.rs @@ -76,13 +76,10 @@ impl Email { user: &DbUser, email_verification_code: &str, ) -> Result { + let site_url = state.site_url(); let (server_config, template) = state.access_config(|c| (c.server.clone(), c.email.user_verification_template.clone())); - let Some(ref site_url) = server_config.site_url else { - return Err(EmailError::Missing("config.site_url")); - }; - let (subject_template, body_template) = match template { Some(EmailTemplate { subject: Some(subject), @@ -109,7 +106,7 @@ impl Email { .render(context! { APP_NAME => server_config.application_name, VERIFICATION_URL => verification_url, - SITE_URL => server_config.site_url, + SITE_URL => site_url, CODE => email_verification_code, EMAIL => user.email, })?; @@ -122,13 +119,10 @@ impl Email { user: &DbUser, email_verification_code: &str, ) -> Result { + let site_url = state.site_url(); let (server_config, template) = state.access_config(|c| (c.server.clone(), c.email.change_email_template.clone())); - let Some(ref site_url) = server_config.site_url else { - return Err(EmailError::Missing("config.site_url")); - }; - let (subject_template, body_template) = match template { Some(EmailTemplate { subject: Some(subject), @@ -155,7 +149,7 @@ impl Email { .render(context! { APP_NAME => server_config.application_name, VERIFICATION_URL => verification_url, - SITE_URL => server_config.site_url, + SITE_URL => site_url, CODE => email_verification_code, EMAIL => user.email, })?; @@ -168,13 +162,10 @@ impl Email { user: &DbUser, password_reset_code: &str, ) -> Result { + let site_url = state.site_url(); let (server_config, template) = state.access_config(|c| (c.server.clone(), c.email.password_reset_template.clone())); - let Some(ref site_url) = server_config.site_url else { - return Err(EmailError::Missing("config.site_url")); - }; - let (subject_template, body_template) = match template { Some(EmailTemplate { subject: Some(subject), @@ -201,7 +192,7 @@ impl Email { .render(context! { APP_NAME => server_config.application_name, VERIFICATION_URL => verification_url, - SITE_URL => server_config.site_url, + SITE_URL => site_url, CODE => password_reset_code, EMAIL => user.email, })?; diff --git a/trailbase-core/src/server/init.rs b/trailbase-core/src/server/init.rs index 8bb82333..b2ed5bae 100644 --- a/trailbase-core/src/server/init.rs +++ b/trailbase-core/src/server/init.rs @@ -43,6 +43,7 @@ pub enum InitError { #[derive(Default)] pub struct InitArgs { + pub address: String, pub dev: bool, pub demo: bool, pub js_runtime_threads: Option, @@ -123,6 +124,7 @@ pub async fn init_app_state( let app_state = AppState::new(AppStateArgs { data_dir: data_dir.clone(), public_dir, + address: args.address, dev: args.dev, demo: args.demo, table_metadata, diff --git a/trailbase-core/src/server/mod.rs b/trailbase-core/src/server/mod.rs index 09c6e9fe..b5e7a28c 100644 --- a/trailbase-core/src/server/mod.rs +++ b/trailbase-core/src/server/mod.rs @@ -120,6 +120,7 @@ impl Server { opts.data_dir.clone(), opts.public_dir.clone(), InitArgs { + address: opts.address.clone(), dev: opts.dev, demo: opts.demo, js_runtime_threads: opts.js_runtime_threads,