Add OIDC end-to-end test.

This commit is contained in:
Sebastian Jeltsch
2025-03-08 14:48:39 +01:00
parent 093ede080d
commit c14c544db7
15 changed files with 344 additions and 78 deletions

View File

@@ -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: "<REDACTED>"
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"
}
}]
}

247
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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",

View File

@@ -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();
});

View File

@@ -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.

View File

@@ -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<PathBuf>,
address: String,
dev: bool,
demo: bool,
@@ -48,6 +48,7 @@ struct InternalState {
pub(crate) struct AppStateArgs {
pub data_dir: DataDir,
pub public_dir: Option<PathBuf>,
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<Mailer> {
@@ -408,6 +411,7 @@ pub async fn test_state(options: Option<TestStateOptions>) -> anyhow::Result<App
state: Arc::new(InternalState {
data_dir,
public_dir: None,
address: "localhost:4000".to_string(),
dev: true,
demo: false,
oauth: Computed::new(&config, |c| {

View File

@@ -75,7 +75,10 @@ impl IntoResponse for AuthError {
Self::NotFound => (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()))
}

View File

@@ -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<WrappedReqwestError>;
type Future = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<HttpResponse, Self::Error>> + 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(

View File

@@ -45,6 +45,7 @@ pub struct OAuthUser {
pub avatar: Option<String>,
}
#[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() {

View File

@@ -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<bool>,
// pub name: Option<String>,
// pub preferred_username : Option<String>,
@@ -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,
});
}

View File

@@ -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()
},

View File

@@ -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;

View File

@@ -76,13 +76,10 @@ impl Email {
user: &DbUser,
email_verification_code: &str,
) -> Result<Self, EmailError> {
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<Self, EmailError> {
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<Self, EmailError> {
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,
})?;

View File

@@ -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<usize>,
@@ -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,

View File

@@ -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,