mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Merge branch 'develop' into feature/lightbox-sidebar-metadata
This commit is contained in:
commit
974d9e0e0f
15 changed files with 814 additions and 420 deletions
16
NOTICE
16
NOTICE
|
|
@ -9,7 +9,7 @@ The following 3rd-party software packages may be used by or distributed with
|
|||
PhotoPrism. Any information relevant to third-party vendors listed below are
|
||||
collected using common, reasonable means.
|
||||
|
||||
Date generated: 2026-04-15
|
||||
Date generated: 2026-04-27
|
||||
|
||||
================================================================================
|
||||
|
||||
|
|
@ -1150,8 +1150,8 @@ THE SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/go-co-op/gocron/v2
|
||||
Version: v2.21.0
|
||||
License: MIT (https://github.com/go-co-op/gocron/blob/v2.21.0/LICENSE)
|
||||
Version: v2.21.1
|
||||
License: MIT (https://github.com/go-co-op/gocron/blob/v2.21.1/LICENSE)
|
||||
|
||||
MIT License
|
||||
|
||||
|
|
@ -6627,8 +6627,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/yalue/onnxruntime_go
|
||||
Version: v1.27.0
|
||||
License: MIT (https://github.com/yalue/onnxruntime_go/blob/v1.27.0/LICENSE)
|
||||
Version: v1.28.0
|
||||
License: MIT (https://github.com/yalue/onnxruntime_go/blob/v1.28.0/LICENSE)
|
||||
|
||||
Copyright (c) 2023 Nathan Otterness
|
||||
|
||||
|
|
@ -6893,8 +6893,8 @@ License: Apache-2.0 (https://github.com/zitadel/logging/blob/v0.7.0/LICENSE)
|
|||
--------------------------------------------------------------------------------
|
||||
|
||||
Package: github.com/zitadel/oidc/v3
|
||||
Version: v3.47.3
|
||||
License: Apache-2.0 (https://github.com/zitadel/oidc/blob/v3.47.3/LICENSE)
|
||||
Version: v3.47.5
|
||||
License: Apache-2.0 (https://github.com/zitadel/oidc/blob/v3.47.5/LICENSE)
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
|
|
@ -9323,7 +9323,7 @@ node-storage-shim ISC Michael Nahkies
|
|||
passive-events-support MIT Ignas Damunskis <ignas3run@gmail.com>
|
||||
photoswipe MIT Dmytro Semenov (https://dimsemenov.com)
|
||||
playwright Apache-2.0 Microsoft Corporation
|
||||
postcss MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
postcss MIT Andrey Sitnik <andrey@sitnik.es>
|
||||
postcss-import MIT Maxime Thirouin
|
||||
postcss-loader MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
postcss-preset-env MIT-0 n/a
|
||||
|
|
|
|||
|
|
@ -380,7 +380,7 @@ services:
|
|||
|
||||
## Dummy OIDC Identity Provider
|
||||
dummy-oidc:
|
||||
image: photoprism/dummy-oidc:251210
|
||||
image: photoprism/dummy-oidc:260427
|
||||
stop_grace_period: 5s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
|
|
|
|||
|
|
@ -1,23 +1,31 @@
|
|||
module caos-test-op
|
||||
|
||||
go 1.24.0
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/zitadel/oidc v1.13.5
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-jose/go-jose/v4 v4.1.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/zitadel/oidc/v3 v3.47.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/muhlemmer/gu v0.3.1 // indirect
|
||||
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
|
||||
github.com/rs/cors v1.11.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/zitadel/logging v0.6.2 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/zitadel/logging v0.7.0 // indirect
|
||||
github.com/zitadel/schema v1.3.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,46 +1,69 @@
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
|
||||
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
|
||||
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
|
||||
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
|
||||
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
|
||||
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
|
||||
github.com/zitadel/oidc v1.13.5 h1:7jhh68NGZitLqwLiVU9Dtwa4IraJPFF1vS+4UupO93U=
|
||||
github.com/zitadel/oidc v1.13.5/go.mod h1:rHs1DhU3Sv3tnI6bQRVlFa3u0lCwtR7S21WHY+yXgPA=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ=
|
||||
github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ=
|
||||
github.com/zitadel/oidc/v3 v3.47.5 h1:cR2z0oqa5XZkwpXQiPCUGqKtndrjHgEXb81y3oXocK4=
|
||||
github.com/zitadel/oidc/v3 v3.47.5/go.mod h1:XxFh0666HRXycyrKmono+3gY0RACpYJLgy4r/+kliKY=
|
||||
github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI=
|
||||
github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
|
|
@ -1,104 +1,164 @@
|
|||
package mock
|
||||
|
||||
// revive:disable
|
||||
// Dummy storage implementation for the test OIDC provider; lint strictness is relaxed intentionally.
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/pkg/oidc"
|
||||
"github.com/zitadel/oidc/pkg/op"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
// ConfClient represents a fixed client configuration used by the dummy provider.
|
||||
// defaultRedirectURIs lists the redirect URIs accepted by the dummy provider.
|
||||
// These match the development origins used by PhotoPrism's local compose stack.
|
||||
var defaultRedirectURIs = []string{
|
||||
"https://registered.com/callback",
|
||||
"http://localhost:9999/callback",
|
||||
"http://localhost:5556/auth/callback",
|
||||
"custom://callback",
|
||||
"https://localhost:8443/test/a/instructions-example/callback",
|
||||
"https://op.certification.openid.net:62064/authz_cb",
|
||||
"https://op.certification.openid.net:62064/authz_post",
|
||||
"http://localhost:2342/api/v1/oidc/redirect",
|
||||
"https://app.localssl.dev/api/v1/oidc/redirect",
|
||||
}
|
||||
|
||||
// ConfClient is the dummy provider's op.Client implementation. It is permissive
|
||||
// by design: any client_id is accepted and treated as a public web client so
|
||||
// that local development flows can authenticate without prior registration.
|
||||
type ConfClient struct {
|
||||
id string
|
||||
applicationType op.ApplicationType
|
||||
authMethod oidc.AuthMethod
|
||||
accessTokenType op.AccessTokenType
|
||||
responseTypes []oidc.ResponseType
|
||||
grantTypes []oidc.GrantType
|
||||
ID string
|
||||
accessTokenType op.AccessTokenType
|
||||
devMode bool
|
||||
}
|
||||
|
||||
func (c *ConfClient) GetID() string {
|
||||
return c.ID
|
||||
// NewClient returns a ConfClient configured for the given client_id.
|
||||
// The web/native variants mirror the original v1 dummy behavior; everything
|
||||
// else falls back to a permissive user-agent client.
|
||||
func NewClient(id string) *ConfClient {
|
||||
c := &ConfClient{
|
||||
id: id,
|
||||
devMode: true,
|
||||
grantTypes: []oidc.GrantType{
|
||||
oidc.GrantTypeCode,
|
||||
oidc.GrantTypeRefreshToken,
|
||||
},
|
||||
}
|
||||
switch id {
|
||||
case "native":
|
||||
c.applicationType = op.ApplicationTypeNative
|
||||
c.authMethod = oidc.AuthMethodBasic
|
||||
c.accessTokenType = op.AccessTokenTypeBearer
|
||||
c.responseTypes = []oidc.ResponseType{
|
||||
oidc.ResponseTypeCode,
|
||||
oidc.ResponseTypeIDToken,
|
||||
oidc.ResponseTypeIDTokenOnly,
|
||||
}
|
||||
case "user_agent":
|
||||
c.applicationType = op.ApplicationTypeUserAgent
|
||||
c.authMethod = oidc.AuthMethodNone
|
||||
c.accessTokenType = op.AccessTokenTypeJWT
|
||||
c.responseTypes = []oidc.ResponseType{
|
||||
oidc.ResponseTypeIDToken,
|
||||
oidc.ResponseTypeIDTokenOnly,
|
||||
}
|
||||
default:
|
||||
// Default to a permissive confidential web client so that any unknown
|
||||
// client_id (such as the ones PhotoPrism generates for its backend)
|
||||
// can complete the authorization-code flow with client_secret_basic
|
||||
// authentication and without PKCE, matching the v1 dummy's behavior.
|
||||
c.applicationType = op.ApplicationTypeWeb
|
||||
c.authMethod = oidc.AuthMethodBasic
|
||||
c.accessTokenType = op.AccessTokenTypeBearer
|
||||
c.responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ConfClient) RedirectURIs() []string {
|
||||
return []string{
|
||||
"https://registered.com/callback",
|
||||
"http://localhost:9999/callback",
|
||||
"http://localhost:5556/auth/callback",
|
||||
"custom://callback",
|
||||
"https://localhost:8443/test/a/instructions-example/callback",
|
||||
"https://op.certification.openid.net:62064/authz_cb",
|
||||
"https://op.certification.openid.net:62064/authz_post",
|
||||
"http://localhost:2342/api/v1/oidc/redirect",
|
||||
"https://app.localssl.dev/api/v1/oidc/redirect",
|
||||
}
|
||||
// GetID returns the client_id.
|
||||
func (c *ConfClient) GetID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
// RedirectURIs returns the registered redirect URIs.
|
||||
func (c *ConfClient) RedirectURIs() []string {
|
||||
return defaultRedirectURIs
|
||||
}
|
||||
|
||||
// PostLogoutRedirectURIs returns the registered post-logout redirect URIs.
|
||||
func (c *ConfClient) PostLogoutRedirectURIs() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// LoginURL points the OpenID provider at the dummy's login endpoint.
|
||||
func (c *ConfClient) LoginURL(id string) string {
|
||||
// return "authorize/callback?id=" + id
|
||||
return "login?id=" + id
|
||||
return "/login?id=" + id
|
||||
}
|
||||
|
||||
// ApplicationType returns the OAuth application type.
|
||||
func (c *ConfClient) ApplicationType() op.ApplicationType {
|
||||
return c.applicationType
|
||||
}
|
||||
|
||||
// AuthMethod returns the client's auth method.
|
||||
func (c *ConfClient) AuthMethod() oidc.AuthMethod {
|
||||
return c.authMethod
|
||||
}
|
||||
|
||||
// IDTokenLifetime is the issued id_token lifetime.
|
||||
func (c *ConfClient) IDTokenLifetime() time.Duration {
|
||||
return 60 * time.Minute
|
||||
}
|
||||
|
||||
// AccessTokenType returns the access token format.
|
||||
func (c *ConfClient) AccessTokenType() op.AccessTokenType {
|
||||
return c.accessTokenType
|
||||
}
|
||||
|
||||
// ResponseTypes returns the supported response_type values.
|
||||
func (c *ConfClient) ResponseTypes() []oidc.ResponseType {
|
||||
return c.responseTypes
|
||||
}
|
||||
|
||||
// GrantTypes returns the supported grant_type values.
|
||||
func (c *ConfClient) GrantTypes() []oidc.GrantType {
|
||||
return c.grantTypes
|
||||
}
|
||||
|
||||
// DevMode permits non-compliant configurations so http callbacks are accepted.
|
||||
func (c *ConfClient) DevMode() bool {
|
||||
return c.devMode
|
||||
}
|
||||
|
||||
// AllowedScopes returns nil to indicate that no scope restriction applies.
|
||||
func (c *ConfClient) AllowedScopes() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RestrictAdditionalIdTokenScopes returns the input scopes unchanged.
|
||||
func (c *ConfClient) RestrictAdditionalIdTokenScopes() func(scopes []string) []string {
|
||||
return func(scopes []string) []string {
|
||||
return scopes
|
||||
}
|
||||
return func(scopes []string) []string { return scopes }
|
||||
}
|
||||
|
||||
// RestrictAdditionalAccessTokenScopes returns the input scopes unchanged.
|
||||
func (c *ConfClient) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string {
|
||||
return func(scopes []string) []string {
|
||||
return scopes
|
||||
}
|
||||
return func(scopes []string) []string { return scopes }
|
||||
}
|
||||
|
||||
// IsScopeAllowed permits any scope so callers can extend the dummy easily.
|
||||
func (c *ConfClient) IsScopeAllowed(scope string) bool {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// IDTokenUserinfoClaimsAssertion forces userinfo claims into the id_token to
|
||||
// match the original v1 dummy's behavior.
|
||||
func (c *ConfClient) IDTokenUserinfoClaimsAssertion() bool {
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
// ClockSkew returns no clock skew tolerance.
|
||||
func (c *ConfClient) ClockSkew() time.Duration {
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,110 +1,172 @@
|
|||
// Package mock provides an in-memory OIDC storage used by the dummy provider in development.
|
||||
package mock
|
||||
|
||||
// revive:disable
|
||||
// Dummy storage implementation for the test OIDC provider; lint strictness is relaxed intentionally.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/zitadel/oidc/pkg/oidc"
|
||||
"github.com/zitadel/oidc/pkg/op"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
)
|
||||
|
||||
var (
|
||||
a = &AuthRequest{}
|
||||
t bool
|
||||
c string
|
||||
state string
|
||||
)
|
||||
// Compile-time assertions that AuthStorage satisfies the v3 op.Storage interface.
|
||||
var _ op.Storage = (*AuthStorage)(nil)
|
||||
|
||||
// AuthStorage keeps ephemeral keys and auth state for the dummy provider.
|
||||
// AuthStorage is the in-memory storage backing the dummy OIDC provider.
|
||||
// It implements the op.Storage interface required by zitadel/oidc/v3.
|
||||
type AuthStorage struct {
|
||||
key *rsa.PrivateKey
|
||||
kid string
|
||||
lock sync.Mutex
|
||||
signingKey signingKey
|
||||
authRequests map[string]*AuthRequest
|
||||
codes map[string]string
|
||||
tokens map[string]*tokenEntry
|
||||
refreshTokens map[string]*refreshTokenEntry
|
||||
}
|
||||
|
||||
// NewAuthStorage constructs the dummy storage with a fresh RSA key.
|
||||
func NewAuthStorage() op.Storage {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
// AuthRequest captures the subset of an OIDC authorization request that the
|
||||
// dummy provider needs to satisfy the op.AuthRequest interface.
|
||||
type AuthRequest struct {
|
||||
ID string
|
||||
ClientID string
|
||||
Subject string
|
||||
Scopes []string
|
||||
RedirectURI string
|
||||
ResponseType oidc.ResponseType
|
||||
ResponseMode oidc.ResponseMode
|
||||
Nonce string
|
||||
State string
|
||||
CodeChallenge *oidc.CodeChallenge
|
||||
|
||||
authTime time.Time
|
||||
done bool
|
||||
}
|
||||
|
||||
type signingKey struct {
|
||||
id string
|
||||
algorithm jose.SignatureAlgorithm
|
||||
key *rsa.PrivateKey
|
||||
}
|
||||
|
||||
// SignatureAlgorithm returns the signing algorithm used by the key.
|
||||
func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { return s.algorithm }
|
||||
|
||||
// Key returns the private key used for signing.
|
||||
func (s *signingKey) Key() any { return s.key }
|
||||
|
||||
// ID returns the key identifier.
|
||||
func (s *signingKey) ID() string { return s.id }
|
||||
|
||||
type publicKey struct{ signingKey }
|
||||
|
||||
// ID returns the key identifier.
|
||||
func (p *publicKey) ID() string { return p.id }
|
||||
|
||||
// Algorithm returns the JOSE algorithm of the key.
|
||||
func (p *publicKey) Algorithm() jose.SignatureAlgorithm { return p.algorithm }
|
||||
|
||||
// Use returns the key usage; the dummy only signs.
|
||||
func (p *publicKey) Use() string { return "sig" }
|
||||
|
||||
// Key returns the public part of the signing key.
|
||||
func (p *publicKey) Key() any { return &p.key.PublicKey }
|
||||
|
||||
type tokenEntry struct {
|
||||
id string
|
||||
applicationID string
|
||||
subject string
|
||||
audience []string
|
||||
scopes []string
|
||||
expiration time.Time
|
||||
refreshTokens string
|
||||
}
|
||||
|
||||
type refreshTokenEntry struct {
|
||||
id string
|
||||
applicationID string
|
||||
subject string
|
||||
audience []string
|
||||
scopes []string
|
||||
authTime time.Time
|
||||
amr []string
|
||||
accessToken string
|
||||
expiration time.Time
|
||||
}
|
||||
|
||||
// NewAuthStorage creates an AuthStorage with a fresh RSA signing key.
|
||||
func NewAuthStorage() *AuthStorage {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b := make([]byte, 16)
|
||||
|
||||
if _, err = rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &AuthStorage{
|
||||
key: key,
|
||||
kid: string(b),
|
||||
signingKey: signingKey{
|
||||
id: uuid.NewString(),
|
||||
algorithm: jose.RS256,
|
||||
key: key,
|
||||
},
|
||||
authRequests: make(map[string]*AuthRequest),
|
||||
codes: make(map[string]string),
|
||||
tokens: make(map[string]*tokenEntry),
|
||||
refreshTokens: make(map[string]*refreshTokenEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRequest is a lightweight auth request implementation for tests.
|
||||
type AuthRequest struct {
|
||||
ID string
|
||||
ResponseType oidc.ResponseType
|
||||
ResponseMode oidc.ResponseMode
|
||||
RedirectURI string
|
||||
Nonce string
|
||||
ClientID string
|
||||
CodeChallenge *oidc.CodeChallenge
|
||||
// MarkRequestDone flags an auth request as authenticated so the OP can issue a code.
|
||||
func (s *AuthStorage) MarkRequestDone(id string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
req, ok := s.authRequests[id]
|
||||
if !ok {
|
||||
return errors.New("auth request not found")
|
||||
}
|
||||
req.done = true
|
||||
req.authTime = time.Now().UTC()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetACR() string {
|
||||
return ""
|
||||
}
|
||||
// GetID returns the auth request id.
|
||||
func (a *AuthRequest) GetID() string { return a.ID }
|
||||
|
||||
// GetACR returns the (unset) authentication context class reference.
|
||||
func (a *AuthRequest) GetACR() string { return "" }
|
||||
|
||||
// GetAMR returns the authentication methods that were used.
|
||||
func (a *AuthRequest) GetAMR() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetAudience() []string {
|
||||
return []string{
|
||||
a.ClientID,
|
||||
if a.done {
|
||||
return []string{"pwd"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetAuthTime() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
// GetAudience returns the audience for issued tokens.
|
||||
func (a *AuthRequest) GetAudience() []string { return []string{a.ClientID} }
|
||||
|
||||
func (a *AuthRequest) GetClientID() string {
|
||||
return a.ClientID
|
||||
}
|
||||
// GetAuthTime returns the time the user authenticated.
|
||||
func (a *AuthRequest) GetAuthTime() time.Time { return a.authTime }
|
||||
|
||||
func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
|
||||
fmt.Println("GetCodeChallenge: ", a.CodeChallenge.Challenge, a.CodeChallenge.Method)
|
||||
return a.CodeChallenge
|
||||
}
|
||||
// GetClientID returns the client identifier.
|
||||
func (a *AuthRequest) GetClientID() string { return a.ClientID }
|
||||
|
||||
func (a *AuthRequest) GetID() string {
|
||||
return a.ID
|
||||
}
|
||||
// GetCodeChallenge returns the PKCE code challenge if any.
|
||||
func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { return a.CodeChallenge }
|
||||
|
||||
func (a *AuthRequest) GetNonce() string {
|
||||
return a.Nonce
|
||||
}
|
||||
// GetNonce returns the request nonce.
|
||||
func (a *AuthRequest) GetNonce() string { return a.Nonce }
|
||||
|
||||
func (a *AuthRequest) GetRedirectURI() string {
|
||||
return a.RedirectURI
|
||||
}
|
||||
// GetRedirectURI returns the client redirect URI.
|
||||
func (a *AuthRequest) GetRedirectURI() string { return a.RedirectURI }
|
||||
|
||||
func (a *AuthRequest) GetResponseType() oidc.ResponseType {
|
||||
return a.ResponseType
|
||||
}
|
||||
// GetResponseType returns the requested response_type.
|
||||
func (a *AuthRequest) GetResponseType() oidc.ResponseType { return a.ResponseType }
|
||||
|
||||
// GetResponseMode returns the response_mode (defaulting to query).
|
||||
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
||||
if a.ResponseMode != "" {
|
||||
return a.ResponseMode
|
||||
|
|
@ -112,229 +174,372 @@ func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
|||
return oidc.ResponseModeQuery
|
||||
}
|
||||
|
||||
// GetScopes returns the scopes granted to the request.
|
||||
func (a *AuthRequest) GetScopes() []string {
|
||||
return []string{
|
||||
"openid",
|
||||
"profile",
|
||||
"email",
|
||||
if len(a.Scopes) > 0 {
|
||||
return a.Scopes
|
||||
}
|
||||
return []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail}
|
||||
}
|
||||
|
||||
func (a *AuthRequest) SetCurrentScopes(scopes []string) {}
|
||||
// GetState returns the state parameter that the client provided.
|
||||
func (a *AuthRequest) GetState() string { return a.State }
|
||||
|
||||
func (a *AuthRequest) GetState() string {
|
||||
return state
|
||||
}
|
||||
// GetSubject returns the authenticated user's subject identifier.
|
||||
func (a *AuthRequest) GetSubject() string { return a.Subject }
|
||||
|
||||
func (a *AuthRequest) GetSubject() string {
|
||||
return "sub00000001"
|
||||
}
|
||||
// Done reports whether the request has completed authentication.
|
||||
func (a *AuthRequest) Done() bool { return a.done }
|
||||
|
||||
func (a *AuthRequest) Done() bool {
|
||||
return true
|
||||
}
|
||||
// Health implements op.Storage.
|
||||
func (s *AuthStorage) Health(_ context.Context) error { return nil }
|
||||
|
||||
func (s *AuthStorage) Health(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
// CreateAuthRequest stores a new request and returns it as op.AuthRequest.
|
||||
func (s *AuthStorage) CreateAuthRequest(_ context.Context, authReq *oidc.AuthRequest, _ string) (op.AuthRequest, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// CreateAuthRequest stores the incoming request in memory and returns a stub AuthRequest.
|
||||
func (s *AuthStorage) CreateAuthRequest(_ context.Context, authReq *oidc.AuthRequest, userId string) (op.AuthRequest, error) {
|
||||
fmt.Println("Userid: ", userId)
|
||||
fmt.Println("CreateAuthRequest CodeChallenge: ", authReq.CodeChallenge)
|
||||
fmt.Println("CreateAuthRequest CodeChallengeMethod: ", authReq.CodeChallengeMethod)
|
||||
fmt.Println("CreateAuthRequest State: ", authReq.State)
|
||||
fmt.Println("CreateAuthRequest ClientID: ", authReq.ClientID)
|
||||
fmt.Println("CreateAuthRequest ResponseType: ", authReq.ResponseType)
|
||||
fmt.Println("CreateAuthRequest Nonce: ", authReq.Nonce)
|
||||
fmt.Println("CreateAuthRequest Scopes: ", authReq.Scopes)
|
||||
fmt.Println("CreateAuthRequest Display: ", authReq.Display)
|
||||
fmt.Println("CreateAuthRequest LoginHint: ", authReq.LoginHint)
|
||||
fmt.Println("CreateAuthRequest IDTokenHint: ", authReq.IDTokenHint)
|
||||
a = &AuthRequest{
|
||||
ID: "authReqUserAgentId",
|
||||
id := uuid.NewString()
|
||||
req := &AuthRequest{
|
||||
ID: id,
|
||||
ClientID: authReq.ClientID,
|
||||
Subject: "sub00000001",
|
||||
Scopes: authReq.Scopes,
|
||||
RedirectURI: authReq.RedirectURI,
|
||||
ResponseType: authReq.ResponseType,
|
||||
ResponseMode: authReq.ResponseMode,
|
||||
Nonce: authReq.Nonce,
|
||||
RedirectURI: authReq.RedirectURI,
|
||||
State: authReq.State,
|
||||
}
|
||||
if authReq.CodeChallenge != "" {
|
||||
a.CodeChallenge = &oidc.CodeChallenge{
|
||||
req.CodeChallenge = &oidc.CodeChallenge{
|
||||
Challenge: authReq.CodeChallenge,
|
||||
Method: authReq.CodeChallengeMethod,
|
||||
}
|
||||
}
|
||||
state = authReq.State
|
||||
t = false
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) AuthRequestByCode(_ context.Context, code string) (op.AuthRequest, error) {
|
||||
if code != c {
|
||||
return nil, errors.New("invalid code")
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) SaveAuthCode(_ context.Context, id, code string) error {
|
||||
if a.ID != id {
|
||||
return errors.New("SaveAuthCode: not found")
|
||||
}
|
||||
c = code
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) DeleteAuthRequest(context.Context, string) error {
|
||||
t = true
|
||||
return nil
|
||||
s.authRequests[id] = req
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// AuthRequestByID looks up an auth request by id.
|
||||
func (s *AuthStorage) AuthRequestByID(_ context.Context, id string) (op.AuthRequest, error) {
|
||||
fmt.Println("AuthRequestByID: ", id)
|
||||
if id != "authReqUserAgentId:usertoken" || t {
|
||||
return nil, errors.New("AuthRequestByID: not found")
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
req, ok := s.authRequests[id]
|
||||
if !ok {
|
||||
return nil, errors.New("auth request not found")
|
||||
}
|
||||
return a, nil
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) {
|
||||
return "loginId", time.Now().UTC().Add(5 * time.Minute), nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) {
|
||||
return "loginId", "refreshToken", time.Now().UTC().Add(5 * time.Minute), nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
||||
if refreshToken != c {
|
||||
return nil, errors.New("invalid token")
|
||||
// AuthRequestByCode looks up an auth request by an issued authorization code.
|
||||
func (s *AuthStorage) AuthRequestByCode(_ context.Context, code string) (op.AuthRequest, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
id, ok := s.codes[code]
|
||||
if !ok {
|
||||
return nil, errors.New("code not found")
|
||||
}
|
||||
return a, nil
|
||||
req, ok := s.authRequests[id]
|
||||
if !ok {
|
||||
return nil, errors.New("auth request not found")
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// SaveAuthCode stores the code-to-request mapping.
|
||||
func (s *AuthStorage) SaveAuthCode(_ context.Context, id, code string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.codes[code] = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteAuthRequest removes an auth request and any associated code.
|
||||
func (s *AuthStorage) DeleteAuthRequest(_ context.Context, id string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
delete(s.authRequests, id)
|
||||
for code, reqID := range s.codes {
|
||||
if reqID == id {
|
||||
delete(s.codes, code)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAccessToken issues an opaque access token id.
|
||||
func (s *AuthStorage) CreateAccessToken(_ context.Context, request op.TokenRequest) (string, time.Time, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
tok := s.newAccessTokenLocked(request, "")
|
||||
return tok.id, tok.expiration, nil
|
||||
}
|
||||
|
||||
// CreateAccessAndRefreshTokens issues an access token alongside a refresh token.
|
||||
func (s *AuthStorage) CreateAccessAndRefreshTokens(_ context.Context, request op.TokenRequest, currentRefreshToken string) (string, string, time.Time, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
if currentRefreshToken != "" {
|
||||
if _, ok := s.refreshTokens[currentRefreshToken]; !ok {
|
||||
return "", "", time.Time{}, errors.New("invalid refresh token")
|
||||
}
|
||||
delete(s.refreshTokens, currentRefreshToken)
|
||||
}
|
||||
|
||||
refreshID := uuid.NewString()
|
||||
tok := s.newAccessTokenLocked(request, refreshID)
|
||||
|
||||
authTime := time.Now().UTC()
|
||||
var amr []string
|
||||
if authReq, ok := request.(*AuthRequest); ok {
|
||||
authTime = authReq.authTime
|
||||
amr = authReq.GetAMR()
|
||||
}
|
||||
|
||||
s.refreshTokens[refreshID] = &refreshTokenEntry{
|
||||
id: refreshID,
|
||||
applicationID: tok.applicationID,
|
||||
subject: tok.subject,
|
||||
audience: tok.audience,
|
||||
scopes: tok.scopes,
|
||||
authTime: authTime,
|
||||
amr: amr,
|
||||
accessToken: tok.id,
|
||||
expiration: time.Now().UTC().Add(5 * time.Hour),
|
||||
}
|
||||
return tok.id, refreshID, tok.expiration, nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) newAccessTokenLocked(request op.TokenRequest, refreshID string) *tokenEntry {
|
||||
id := uuid.NewString()
|
||||
tok := &tokenEntry{
|
||||
id: id,
|
||||
applicationID: clientIDFromRequest(request),
|
||||
subject: request.GetSubject(),
|
||||
audience: request.GetAudience(),
|
||||
scopes: request.GetScopes(),
|
||||
expiration: time.Now().UTC().Add(5 * time.Minute),
|
||||
refreshTokens: refreshID,
|
||||
}
|
||||
s.tokens[id] = tok
|
||||
return tok
|
||||
}
|
||||
|
||||
func clientIDFromRequest(request op.TokenRequest) string {
|
||||
switch req := request.(type) {
|
||||
case *AuthRequest:
|
||||
return req.ClientID
|
||||
case *refreshTokenRequest:
|
||||
return req.applicationID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TokenRequestByRefreshToken returns a refresh-token-backed token request.
|
||||
func (s *AuthStorage) TokenRequestByRefreshToken(_ context.Context, refreshToken string) (op.RefreshTokenRequest, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
rt, ok := s.refreshTokens[refreshToken]
|
||||
if !ok {
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
return &refreshTokenRequest{
|
||||
id: rt.id,
|
||||
applicationID: rt.applicationID,
|
||||
subject: rt.subject,
|
||||
audience: rt.audience,
|
||||
scopes: rt.scopes,
|
||||
authTime: rt.authTime,
|
||||
amr: rt.amr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TerminateSession removes any tokens for a given user/client.
|
||||
func (s *AuthStorage) TerminateSession(_ context.Context, userID, clientID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeToken is a no-op for the dummy implementation.
|
||||
func (s *AuthStorage) RevokeToken(_ context.Context, tokenOrTokenID string, userID string, clientID string) *oidc.Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey) {
|
||||
// keyCh <- jose.SigningKey{Algorithm: jose.RS256, Key: s.key}
|
||||
keyCh <- jose.SigningKey{Algorithm: jose.RS256, Key: &jose.JSONWebKey{Key: s.key, KeyID: s.kid}}
|
||||
}
|
||||
|
||||
func (s *AuthStorage) GetKey(_ context.Context) (*rsa.PrivateKey, error) {
|
||||
return s.key, nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error) {
|
||||
pubkey := s.key.Public()
|
||||
|
||||
wrongkey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
for id, tok := range s.tokens {
|
||||
if tok.subject == userID && tok.applicationID == clientID {
|
||||
delete(s.tokens, id)
|
||||
}
|
||||
}
|
||||
|
||||
return &jose.JSONWebKeySet{
|
||||
Keys: []jose.JSONWebKey{
|
||||
{
|
||||
Key: wrongkey.Public(),
|
||||
Use: "sig",
|
||||
Algorithm: "RS256",
|
||||
KeyID: "wrongkey0002",
|
||||
}, {
|
||||
Key: pubkey,
|
||||
Use: "sig",
|
||||
Algorithm: "RS256",
|
||||
KeyID: s.kid,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
for id, rt := range s.refreshTokens {
|
||||
if rt.subject == userID && rt.applicationID == clientID {
|
||||
delete(s.refreshTokens, id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) GetKeyByIDAndUserID(_ context.Context, _, _ string) (*jose.JSONWebKey, error) {
|
||||
pubkey := s.key.Public()
|
||||
|
||||
return &jose.JSONWebKey{
|
||||
Key: pubkey,
|
||||
Use: "sig",
|
||||
Algorithm: "RS256",
|
||||
KeyID: s.kid,
|
||||
}, nil
|
||||
// GetRefreshTokenInfo returns the user and token id for a refresh token.
|
||||
func (s *AuthStorage) GetRefreshTokenInfo(_ context.Context, _, token string) (string, string, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
rt, ok := s.refreshTokens[token]
|
||||
if !ok {
|
||||
return "", "", op.ErrInvalidRefreshToken
|
||||
}
|
||||
return rt.subject, rt.id, nil
|
||||
}
|
||||
|
||||
// RevokeToken removes an access or refresh token.
|
||||
func (s *AuthStorage) RevokeToken(_ context.Context, tokenOrID, _, clientID string) *oidc.Error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if tok, ok := s.tokens[tokenOrID]; ok {
|
||||
if clientID != "" && tok.applicationID != clientID {
|
||||
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
||||
}
|
||||
delete(s.tokens, tokenOrID)
|
||||
return nil
|
||||
}
|
||||
if rt, ok := s.refreshTokens[tokenOrID]; ok {
|
||||
if clientID != "" && rt.applicationID != clientID {
|
||||
return oidc.ErrInvalidClient().WithDescription("token was not issued for this client")
|
||||
}
|
||||
delete(s.refreshTokens, tokenOrID)
|
||||
delete(s.tokens, rt.accessToken)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SigningKey returns the static dummy signing key.
|
||||
func (s *AuthStorage) SigningKey(_ context.Context) (op.SigningKey, error) {
|
||||
return &s.signingKey, nil
|
||||
}
|
||||
|
||||
// SignatureAlgorithms reports the algorithms this dummy supports.
|
||||
func (s *AuthStorage) SignatureAlgorithms(_ context.Context) ([]jose.SignatureAlgorithm, error) {
|
||||
return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil
|
||||
}
|
||||
|
||||
// KeySet returns the dummy's public JWKS.
|
||||
func (s *AuthStorage) KeySet(_ context.Context) ([]op.Key, error) {
|
||||
return []op.Key{&publicKey{s.signingKey}}, nil
|
||||
}
|
||||
|
||||
// GetClientByClientID returns a permissive client for any id.
|
||||
func (s *AuthStorage) GetClientByClientID(_ context.Context, id string) (op.Client, error) {
|
||||
if id == "none" {
|
||||
return nil, errors.New("GetClientByClientID: not found")
|
||||
return nil, errors.New("client not found")
|
||||
}
|
||||
|
||||
var appType op.ApplicationType
|
||||
var authMethod oidc.AuthMethod
|
||||
var accessTokenType op.AccessTokenType
|
||||
var responseTypes []oidc.ResponseType
|
||||
var grantTypes = []oidc.GrantType{
|
||||
oidc.GrantTypeCode,
|
||||
}
|
||||
|
||||
switch id {
|
||||
case "web":
|
||||
appType = op.ApplicationTypeWeb
|
||||
authMethod = oidc.AuthMethodBasic
|
||||
accessTokenType = op.AccessTokenTypeBearer
|
||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode}
|
||||
case "native":
|
||||
appType = op.ApplicationTypeNative
|
||||
authMethod = oidc.AuthMethodBasic
|
||||
accessTokenType = op.AccessTokenTypeBearer
|
||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeCode, oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly}
|
||||
default:
|
||||
appType = op.ApplicationTypeUserAgent
|
||||
authMethod = oidc.AuthMethodNone
|
||||
accessTokenType = op.AccessTokenTypeJWT
|
||||
responseTypes = []oidc.ResponseType{oidc.ResponseTypeIDToken, oidc.ResponseTypeIDTokenOnly}
|
||||
}
|
||||
|
||||
return &ConfClient{ID: id, applicationType: appType, authMethod: authMethod, accessTokenType: accessTokenType, responseTypes: responseTypes, grantTypes: grantTypes, devMode: true}, nil
|
||||
return NewClient(id), nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, id string, _ string) error {
|
||||
// AuthorizeClientIDSecret accepts any client/secret pair.
|
||||
func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error {
|
||||
return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{})
|
||||
}
|
||||
|
||||
func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, _, _ string, _ []string) error {
|
||||
userinfo.SetSubject(a.GetSubject())
|
||||
// userinfo.SetAddress(oidc.NewUserInfoAddress("Test 789\nPostfach 2", "", "", "", "", ""))
|
||||
userinfo.SetEmail("test@example.com", true)
|
||||
userinfo.SetPhone("0791234567", true)
|
||||
userinfo.SetName("Test")
|
||||
userinfo.AppendClaims("private_claim", "test")
|
||||
userinfo.SetNickname("testnick")
|
||||
userinfo.SetPreferredUsername("prefname")
|
||||
// SetUserinfoFromScopes is required by op.Storage but the dummy populates claims via SetUserinfoFromToken.
|
||||
func (s *AuthStorage) SetUserinfoFromScopes(_ context.Context, info *oidc.UserInfo, _, _ string, _ []string) error {
|
||||
fillUserInfo(info)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{"private_claim": "test"}, nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, tokenID, subject, clientID string) error {
|
||||
if err := s.SetUserinfoFromScopes(ctx, introspect, "", "", []string{}); err != nil {
|
||||
return err
|
||||
// SetUserinfoFromToken populates the userinfo response for a given token.
|
||||
func (s *AuthStorage) SetUserinfoFromToken(_ context.Context, info *oidc.UserInfo, tokenID, _, _ string) error {
|
||||
s.lock.Lock()
|
||||
tok, ok := s.tokens[tokenID]
|
||||
s.lock.Unlock()
|
||||
if !ok {
|
||||
return errors.New("token not found")
|
||||
}
|
||||
|
||||
introspect.SetClientID(a.ClientID)
|
||||
|
||||
if tok.expiration.Before(time.Now().UTC()) {
|
||||
return errors.New("token expired")
|
||||
}
|
||||
fillUserInfo(info)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID string, scope []string) ([]string, error) {
|
||||
return scope, nil
|
||||
// SetIntrospectionFromToken populates the introspection response for a given token.
|
||||
func (s *AuthStorage) SetIntrospectionFromToken(_ context.Context, response *oidc.IntrospectionResponse, tokenID, _, clientID string) error {
|
||||
s.lock.Lock()
|
||||
tok, ok := s.tokens[tokenID]
|
||||
s.lock.Unlock()
|
||||
if !ok {
|
||||
return errors.New("token not found")
|
||||
}
|
||||
if tok.expiration.Before(time.Now().UTC()) {
|
||||
return errors.New("token expired")
|
||||
}
|
||||
for _, aud := range tok.audience {
|
||||
if aud == clientID {
|
||||
response.Expiration = oidc.FromTime(tok.expiration)
|
||||
response.Scope = tok.scopes
|
||||
response.ClientID = tok.applicationID
|
||||
info := new(oidc.UserInfo)
|
||||
fillUserInfo(info)
|
||||
response.SetUserInfo(info)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("token is not valid for this client")
|
||||
}
|
||||
|
||||
// GetPrivateClaimsFromScopes returns custom claims for a token. The dummy always returns one.
|
||||
func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]any, error) {
|
||||
return map[string]any{"private_claim": "test"}, nil
|
||||
}
|
||||
|
||||
// GetKeyByIDAndClientID returns the dummy's public key regardless of clientID.
|
||||
func (s *AuthStorage) GetKeyByIDAndClientID(_ context.Context, _, _ string) (*jose.JSONWebKey, error) {
|
||||
return &jose.JSONWebKey{
|
||||
KeyID: s.signingKey.id,
|
||||
Algorithm: string(s.signingKey.algorithm),
|
||||
Use: "sig",
|
||||
Key: s.signingKey.key.Public(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateJWTProfileScopes accepts the requested scopes verbatim.
|
||||
func (s *AuthStorage) ValidateJWTProfileScopes(_ context.Context, _ string, scopes []string) ([]string, error) {
|
||||
return scopes, nil
|
||||
}
|
||||
|
||||
// fillUserInfo populates the userinfo response with the dummy user identity.
|
||||
func fillUserInfo(info *oidc.UserInfo) {
|
||||
info.Subject = "sub00000001"
|
||||
info.Email = "test@example.com"
|
||||
info.EmailVerified = oidc.Bool(true)
|
||||
info.Name = "Test"
|
||||
info.Nickname = "testnick"
|
||||
info.PreferredUsername = "prefname"
|
||||
info.PhoneNumber = "0791234567"
|
||||
info.PhoneNumberVerified = oidc.Bool(true)
|
||||
info.AppendClaims("private_claim", "test")
|
||||
}
|
||||
|
||||
// refreshTokenRequest implements op.RefreshTokenRequest for the dummy.
|
||||
type refreshTokenRequest struct {
|
||||
id string
|
||||
applicationID string
|
||||
subject string
|
||||
audience []string
|
||||
scopes []string
|
||||
authTime time.Time
|
||||
amr []string
|
||||
}
|
||||
|
||||
// GetAMR returns the authentication methods recorded with the refresh token.
|
||||
func (r *refreshTokenRequest) GetAMR() []string { return r.amr }
|
||||
|
||||
// GetAudience returns the token audience.
|
||||
func (r *refreshTokenRequest) GetAudience() []string { return r.audience }
|
||||
|
||||
// GetAuthTime returns the original auth time.
|
||||
func (r *refreshTokenRequest) GetAuthTime() time.Time { return r.authTime }
|
||||
|
||||
// GetClientID returns the client_id the token was issued to.
|
||||
func (r *refreshTokenRequest) GetClientID() string { return r.applicationID }
|
||||
|
||||
// GetScopes returns the scopes that the refresh token grants.
|
||||
func (r *refreshTokenRequest) GetScopes() []string { return r.scopes }
|
||||
|
||||
// GetSubject returns the subject identifier.
|
||||
func (r *refreshTokenRequest) GetSubject() string { return r.subject }
|
||||
|
||||
// SetCurrentScopes mutates the active scopes when the OP narrows them.
|
||||
func (r *refreshTokenRequest) SetCurrentScopes(scopes []string) { r.scopes = scopes }
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/oidc/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
)
|
||||
|
||||
func TestAuthRequestResponseModeDefault(t *testing.T) {
|
||||
|
|
@ -15,13 +15,49 @@ func TestAuthRequestResponseModeDefault(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRevokeTokenNoError(t *testing.T) {
|
||||
s := &AuthStorage{}
|
||||
if err := s.RevokeToken(
|
||||
context.TODO(),
|
||||
"token",
|
||||
"user",
|
||||
"client",
|
||||
); err != nil {
|
||||
s := NewAuthStorage()
|
||||
if err := s.RevokeToken(context.Background(), "unknown-token", "user", "client"); err != nil {
|
||||
t.Fatalf("expected nil error from RevokeToken, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAndFetchAuthRequest(t *testing.T) {
|
||||
s := NewAuthStorage()
|
||||
req, err := s.CreateAuthRequest(context.Background(), &oidc.AuthRequest{
|
||||
ClientID: "web",
|
||||
RedirectURI: "https://app.localssl.dev/api/v1/oidc/redirect",
|
||||
ResponseType: oidc.ResponseTypeCode,
|
||||
Scopes: []string{"openid", "email"},
|
||||
}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateAuthRequest: %v", err)
|
||||
}
|
||||
if req.GetID() == "" {
|
||||
t.Fatalf("expected non-empty auth request id")
|
||||
}
|
||||
|
||||
if err := s.MarkRequestDone(req.GetID()); err != nil {
|
||||
t.Fatalf("MarkRequestDone: %v", err)
|
||||
}
|
||||
|
||||
got, err := s.AuthRequestByID(context.Background(), req.GetID())
|
||||
if err != nil {
|
||||
t.Fatalf("AuthRequestByID: %v", err)
|
||||
}
|
||||
if !got.Done() {
|
||||
t.Fatalf("expected auth request to be marked done")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientDefaults(t *testing.T) {
|
||||
c := NewClient("anything")
|
||||
if c.GetID() != "anything" {
|
||||
t.Fatalf("unexpected id: %q", c.GetID())
|
||||
}
|
||||
if !c.DevMode() {
|
||||
t.Fatalf("expected dev mode for permissive dummy clients")
|
||||
}
|
||||
if uris := c.RedirectURIs(); len(uris) == 0 {
|
||||
t.Fatalf("expected default redirect URIs")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,40 +12,36 @@ import (
|
|||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zitadel/oidc/pkg/op"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"caos-test-op/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultIssuer = "http://dummy-oidc:9998"
|
||||
defaultPort = "9998"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
b := make([]byte, 32)
|
||||
_, _ = rand.Read(b)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Printf("failed to seed crypto key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
port := "9998"
|
||||
config := &op.Config{
|
||||
Issuer: "http://dummy-oidc:9998",
|
||||
CryptoKey: sha256.Sum256(b),
|
||||
CodeMethodS256: true,
|
||||
}
|
||||
storage := mock.NewAuthStorage()
|
||||
|
||||
handler, err := op.NewOpenIDProvider(ctx, config, storage)
|
||||
provider, err := newProvider(defaultIssuer, storage)
|
||||
if err != nil {
|
||||
log.Printf("failed to create OIDC provider: %v", err)
|
||||
return
|
||||
}
|
||||
router := handler.HttpHandler().(*mux.Router)
|
||||
router.Methods("GET").Path("/login").HandlerFunc(HandleLogin)
|
||||
|
||||
router := chi.NewRouter()
|
||||
loginHandler := newLoginHandler(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest))
|
||||
router.Get("/login", loginHandler)
|
||||
router.Mount("/", provider)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Addr: ":" + defaultPort,
|
||||
Handler: router,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
|
|
@ -62,16 +58,49 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// HandleLogin mocks a login page and immediately redirects with a user token.
|
||||
// newProvider builds an OpenID provider with the dummy's permissive defaults.
|
||||
func newProvider(issuer string, storage op.Storage) (*op.Provider, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := &op.Config{
|
||||
CryptoKey: sha256.Sum256(b),
|
||||
CodeMethodS256: true,
|
||||
AuthMethodPost: true,
|
||||
AuthMethodPrivateKeyJWT: true,
|
||||
GrantTypeRefreshToken: true,
|
||||
RequestObjectSupported: true,
|
||||
}
|
||||
return op.NewOpenIDProvider(issuer, cfg, storage,
|
||||
op.WithAllowInsecure(),
|
||||
)
|
||||
}
|
||||
|
||||
// newLoginHandler returns the dummy /login handler. It marks the auth request as
|
||||
// authenticated and redirects back to the OP's auth callback so the code flow
|
||||
// completes without any user interaction.
|
||||
func newLoginHandler(storage *mock.AuthStorage, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) http.HandlerFunc {
|
||||
return issuerInterceptor.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
HandleLogin(w, r, storage, callback)
|
||||
})
|
||||
}
|
||||
|
||||
// HandleLogin marks an in-flight auth request as authenticated and redirects to the OP callback.
|
||||
// It is exported to keep the existing test surface stable.
|
||||
func HandleLogin(w http.ResponseWriter, r *http.Request, storage *mock.AuthStorage, callback func(context.Context, string) string) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "invalid login request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
requestId := r.Form.Get("id")
|
||||
// simulate user login and retrieve a token that indicates a successfully logged-in user
|
||||
userToken := requestId + ":usertoken"
|
||||
|
||||
http.Redirect(w, r, "/authorize/callback?id="+userToken, http.StatusFound)
|
||||
id := r.Form.Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "missing id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := storage.MarkRequestDone(id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, callback(r.Context(), id), http.StatusFound)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,91 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"caos-test-op/mock"
|
||||
)
|
||||
|
||||
func newTestStorage(t *testing.T) (*mock.AuthStorage, string) {
|
||||
t.Helper()
|
||||
storage := mock.NewAuthStorage()
|
||||
req, err := storage.CreateAuthRequest(context.Background(), &oidc.AuthRequest{
|
||||
ClientID: "csg6yqvykh0780f9",
|
||||
RedirectURI: "https://app.localssl.dev/api/v1/oidc/redirect",
|
||||
ResponseType: oidc.ResponseTypeCode,
|
||||
Scopes: []string{"openid", "email", "profile"},
|
||||
}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("create auth request: %v", err)
|
||||
}
|
||||
return storage, req.GetID()
|
||||
}
|
||||
|
||||
func TestHandleLoginRedirects(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/login?id=abc123", nil)
|
||||
storage, id := newTestStorage(t)
|
||||
|
||||
callback := func(_ context.Context, requestID string) string {
|
||||
return "/callback?id=" + requestID
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/login?id="+id, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleLogin(w, req)
|
||||
HandleLogin(w, req, storage, callback)
|
||||
|
||||
resp := w.Result()
|
||||
if resp.StatusCode != http.StatusFound {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusFound, resp.StatusCode)
|
||||
}
|
||||
location := resp.Header.Get("Location")
|
||||
if location != "/authorize/callback?id=abc123:usertoken" {
|
||||
if location := resp.Header.Get("Location"); location != "/callback?id="+id {
|
||||
t.Fatalf("unexpected redirect location: %s", location)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLoginBadRequest(t *testing.T) {
|
||||
func TestHandleLoginUnknownID(t *testing.T) {
|
||||
storage := mock.NewAuthStorage()
|
||||
|
||||
callback := func(_ context.Context, requestID string) string {
|
||||
return "/callback?id=" + requestID
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/login?id=not-a-real-id", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleLogin(w, req, storage, callback)
|
||||
|
||||
if w.Result().StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected bad request for unknown id, got %d", w.Result().StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLoginMissingID(t *testing.T) {
|
||||
storage := mock.NewAuthStorage()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/login", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleLogin(w, req, storage, func(context.Context, string) string { return "/callback" })
|
||||
|
||||
if w.Result().StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected bad request for missing id, got %d", w.Result().StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleLoginParseError(t *testing.T) {
|
||||
storage := mock.NewAuthStorage()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader("%zz"))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
HandleLogin(w, req)
|
||||
HandleLogin(w, req, storage, func(context.Context, string) string { return "/callback" })
|
||||
|
||||
if w.Result().StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected bad request, got %d", w.Result().StatusCode)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ node-storage-shim ISC Michael Nahkies
|
|||
passive-events-support MIT Ignas Damunskis <ignas3run@gmail.com>
|
||||
photoswipe MIT Dmytro Semenov (https://dimsemenov.com)
|
||||
playwright Apache-2.0 Microsoft Corporation
|
||||
postcss MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
postcss MIT Andrey Sitnik <andrey@sitnik.es>
|
||||
postcss-import MIT Maxime Thirouin
|
||||
postcss-loader MIT Andrey Sitnik <andrey@sitnik.ru>
|
||||
postcss-preset-env MIT-0 n/a
|
||||
|
|
|
|||
|
|
@ -139,9 +139,7 @@
|
|||
"yarn": "please use npm"
|
||||
},
|
||||
"overrides": {
|
||||
"@ctrl/tinycolor": "^4.1.3",
|
||||
"axios": "1.14.0",
|
||||
"color-convert": "2.0.1",
|
||||
"color-name": "1.1.4"
|
||||
"minimatch@~3.0": "^3.1.3",
|
||||
"serialize-javascript": "^7.0.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -96,7 +96,7 @@ require (
|
|||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
|
|
@ -194,3 +194,5 @@ require (
|
|||
)
|
||||
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.26.2
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -20,8 +20,8 @@ filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q
|
|||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0=
|
||||
|
|
|
|||
67
package-lock.json
generated
67
package-lock.json
generated
|
|
@ -13329,18 +13329,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-url/node_modules/minimatch": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz",
|
||||
"integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-value-parser": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
|
|
@ -13496,15 +13484,6 @@
|
|||
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
||||
|
|
@ -13903,26 +13882,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-push-apply": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
||||
|
|
@ -14183,12 +14142,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
|
||||
"integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
|
|
@ -17709,6 +17668,22 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"extraneous": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"name": "photoprism",
|
||||
"private": true,
|
||||
"overrides": {
|
||||
"axios": "1.14.0"
|
||||
"minimatch@~3.0": "^3.1.3",
|
||||
"serialize-javascript": "^7.0.5"
|
||||
},
|
||||
"workspaces": [
|
||||
"frontend"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue