diff --git a/go.mod b/go.mod index f6da4c17..03afe259 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/emicklei/dot v1.6.2 github.com/felixge/httpsnoop v1.0.4 github.com/gertd/go-pluralize v0.2.1 + github.com/go-jose/go-jose/v4 v4.0.2 github.com/go-logr/zapr v1.3.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/google/go-cmp v0.6.0 @@ -79,7 +80,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 github.com/zitadel/logging v0.6.0 - github.com/zitadel/oidc v1.13.5 + github.com/zitadel/oidc/v3 v3.24.0 go.etcd.io/bbolt v1.3.10 go.etcd.io/etcd/client/pkg/v3 v3.5.14 go.etcd.io/etcd/client/v3 v3.5.14 @@ -89,13 +90,11 @@ require ( golang.org/x/crypto v0.23.0 golang.org/x/net v0.25.0 golang.org/x/sync v0.7.0 - golang.org/x/text v0.15.0 golang.org/x/tools v0.21.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 google.golang.org/grpc v1.64.0 google.golang.org/protobuf v1.34.1 - gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v3 v3.0.3 k8s.io/api v0.30.1 k8s.io/apimachinery v0.30.1 @@ -124,6 +123,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.28.10 // indirect github.com/beevik/etree v1.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect @@ -145,7 +145,7 @@ require ( github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/fatih/color v1.17.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-chi/chi/v5 v5.0.12 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -156,8 +156,6 @@ require ( github.com/google/btree v1.1.2 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/schema v1.2.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/gosuri/uilive v0.0.4 // indirect @@ -192,6 +190,8 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/muhlemmer/httpforwarded v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect @@ -203,7 +203,7 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.53.0 // indirect github.com/prometheus/procfs v0.14.0 // indirect - github.com/rs/cors v1.10.1 // indirect + github.com/rs/cors v1.11.0 // indirect github.com/russellhaering/goxmldsig v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect @@ -222,29 +222,31 @@ require ( github.com/vbatts/tar-split v0.11.5 // indirect github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect github.com/xlab/treeprint v1.2.0 // indirect + github.com/zitadel/schema v1.3.0 // indirect go.etcd.io/etcd/api/v3 v3.5.14 // indirect go.etcd.io/etcd/client/v2 v2.305.14 // indirect go.etcd.io/etcd/pkg/v3 v3.5.14 // indirect go.etcd.io/etcd/raft/v3 v3.5.14 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/sdk v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index 5ecc339f..3f6db2f5 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/brianvoe/gofakeit/v6 v6.24.0 h1:74yq7RRz/noddscZHRS2T84oHZisW9muwbb8sRnU52A= github.com/brianvoe/gofakeit/v6 v6.24.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -162,8 +164,10 @@ github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlL github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -217,10 +221,6 @@ github.com/google/pprof v0.0.0-20240402174815-29b9bb013b0f/go.mod h1:kf6iHlnVGwg github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/schema v1.2.1 h1:tjDxcmdb+siIqkTNoV+qRH2mjYdr2hHe5MKXbp61ziM= -github.com/gorilla/schema v1.2.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -340,6 +340,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= @@ -378,8 +382,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= -github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -484,8 +488,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank= github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow= -github.com/zitadel/oidc v1.13.5 h1:7jhh68NGZitLqwLiVU9Dtwa4IraJPFF1vS+4UupO93U= -github.com/zitadel/oidc v1.13.5/go.mod h1:rHs1DhU3Sv3tnI6bQRVlFa3u0lCwtR7S21WHY+yXgPA= +github.com/zitadel/oidc/v3 v3.24.0 h1:TK2qUpVoX0A8Rd0Z9/1jxf+/nm5gstRKReIEG808xCI= +github.com/zitadel/oidc/v3 v3.24.0/go.mod h1:A6rYWOlTb/FtvZvUP8tl2wRCJ+wXMovfwcX80yXjMZQ= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0= go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ= @@ -505,20 +511,20 @@ go.etcd.io/etcd/server/v3 v3.5.14 h1:l/3gdiSSoGU6MyKAYiL+8WSOMq9ySG+NqQ04euLtZfY go.etcd.io/etcd/server/v3 v3.5.14/go.mod h1:SPh0rUtGNDgOZd/aTbkAUYZV+5FFHw5sdbGnO2/byw0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0 h1:R9DE4kQ4k+YtfLI2ULwX82VtNQ2J8yZmA7ZIF/D+7Mc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.27.0/go.mod h1:OQFyQVrDlbe+R7xrEyDr/2Wr67Ol0hRUgsfA+V5A95s= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -668,10 +674,10 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7 h1:ImUcDPHjTrAqNhlOkSocDLfG9rrNHH7w7uoKWPaWZ8s= google.golang.org/genproto v0.0.0-20240311173647-c811ad7063a7/go.mod h1:/3XmxOjePkvmKrHuBy4zNFw7IzxJXtAgdpXi8Ll990U= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8 h1:W5Xj/70xIA4x60O/IFyXivR5MGqblAb8R3w26pnD6No= -google.golang.org/genproto/googleapis/api v0.0.0-20240513163218-0867130af1f8/go.mod h1:vPrPUTsDCYxXWjP7clS81mZ6/803D8K4iM9Ma27VKas= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8 h1:mxSlqyb8ZAHsYDCfiXN1EDdNTdvjUJSLY+OnAUtYNYA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240513163218-0867130af1f8/go.mod h1:I7Y+G38R2bu5j1aLzfFmQfTcU/WnFuqDwLZAbvKTKpM= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5 h1:P8OJ/WCl/Xo4E4zoe4/bifHpSmmKwARqyqE4nW6J2GQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -695,8 +701,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -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= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/backend/grpc/management.go b/internal/backend/grpc/management.go index dbadd36f..11139ea4 100644 --- a/internal/backend/grpc/management.go +++ b/internal/backend/grpc/management.go @@ -27,13 +27,13 @@ import ( "github.com/siderolabs/go-kubernetes/kubernetes/manifests" "github.com/siderolabs/go-kubernetes/kubernetes/upgrade" "github.com/siderolabs/talos/pkg/machinery/api/common" + "github.com/zitadel/oidc/v3/pkg/op" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" - "gopkg.in/square/go-jose.v2" "gopkg.in/yaml.v3" "k8s.io/client-go/rest" @@ -65,7 +65,7 @@ import ( // JWTSigningKeyProvider is an interface for a JWT signing key provider. type JWTSigningKeyProvider interface { - GetCurrentSigningKey() (*jose.JSONWebKey, error) + SigningKey(ctx context.Context) (op.SigningKey, error) } // managementServer implements omni management service. diff --git a/internal/backend/grpc/oidc.go b/internal/backend/grpc/oidc.go index be33dd46..410bf159 100644 --- a/internal/backend/grpc/oidc.go +++ b/internal/backend/grpc/oidc.go @@ -11,7 +11,7 @@ import ( "io" gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -78,8 +78,10 @@ func (s *oidcServer) Authenticate(ctx context.Context, req *oidc.AuthenticateReq }, nil } + issuer := s.provider.IssuerFromRequest(nil) // this is safe because we use op.StaticIssuer + return &oidc.AuthenticateResponse{ - RedirectUrl: op.AuthCallbackURL(s.provider)(req.AuthRequestId), + RedirectUrl: op.AuthCallbackURL(s.provider)(op.ContextWithIssuer(ctx, issuer), req.AuthRequestId), }, nil } diff --git a/internal/backend/grpc/serviceaccount.go b/internal/backend/grpc/serviceaccount.go index 5599bb0d..b0cae41a 100644 --- a/internal/backend/grpc/serviceaccount.go +++ b/internal/backend/grpc/serviceaccount.go @@ -42,7 +42,7 @@ func (s *managementServer) serviceAccountKubeconfig(ctx context.Context, req *ma return nil, fmt.Errorf("failed to get cluster UUID: %w", err) } - signedToken, err := s.generateServiceAccountJWT(req, cluster, clusterUUID.TypedSpec().Value.GetUuid()) + signedToken, err := s.generateServiceAccountJWT(ctx, req, cluster, clusterUUID.TypedSpec().Value.GetUuid()) if err != nil { return nil, err } @@ -83,13 +83,13 @@ func (s *managementServer) validateServiceAccountRequest(cluster string, req *ma return nil } -func (s *managementServer) generateServiceAccountJWT(req *management.KubeconfigRequest, clusterName, clusterUUID string) (string, error) { - signingKey, err := s.jwtSigningKeyProvider.GetCurrentSigningKey() +func (s *managementServer) generateServiceAccountJWT(ctx context.Context, req *management.KubeconfigRequest, clusterName, clusterUUID string) (string, error) { + signingKey, err := s.jwtSigningKeyProvider.SigningKey(ctx) if err != nil { return "", err } - signingMethod := jwt.GetSigningMethod(signingKey.Algorithm) + signingMethod := jwt.GetSigningMethod(string(signingKey.SignatureAlgorithm())) now := time.Now() token := jwt.NewWithClaims(signingMethod, jwt.MapClaims{ @@ -102,9 +102,9 @@ func (s *managementServer) generateServiceAccountJWT(req *management.KubeconfigR "cluster_uuid": clusterUUID, }) - token.Header["kid"] = signingKey.KeyID + token.Header["kid"] = signingKey.ID() - return token.SignedString(signingKey.Key) + return token.SignedString(signingKey.Key()) } func (s *managementServer) buildServiceAccountKubeconfig(cluster, user, token string) ([]byte, error) { diff --git a/internal/backend/oidc/internal/client/client.go b/internal/backend/oidc/internal/client/client.go index d4d38e46..7651a3aa 100644 --- a/internal/backend/oidc/internal/client/client.go +++ b/internal/backend/oidc/internal/client/client.go @@ -12,8 +12,8 @@ import ( "strings" "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" "github.com/siderolabs/omni/client/pkg/constants" "github.com/siderolabs/omni/internal/backend/oidc/external" diff --git a/internal/backend/oidc/internal/models/auth_request.go b/internal/backend/oidc/internal/models/auth_request.go index 868a1884..c0b3289e 100644 --- a/internal/backend/oidc/internal/models/auth_request.go +++ b/internal/backend/oidc/internal/models/auth_request.go @@ -9,7 +9,7 @@ import ( "time" "github.com/siderolabs/go-pointer" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" ) // AuthRequest is a request for authentication. diff --git a/internal/backend/oidc/internal/models/challenge.go b/internal/backend/oidc/internal/models/challenge.go index 0549e67a..d2bbaa75 100644 --- a/internal/backend/oidc/internal/models/challenge.go +++ b/internal/backend/oidc/internal/models/challenge.go @@ -5,7 +5,7 @@ package models -import "github.com/zitadel/oidc/pkg/oidc" +import "github.com/zitadel/oidc/v3/pkg/oidc" // OIDCCodeChallenge is a code challenge as defined in https://tools.ietf.org/html/rfc7636. type OIDCCodeChallenge struct { diff --git a/internal/backend/oidc/internal/storage/authrequest/storage.go b/internal/backend/oidc/internal/storage/authrequest/storage.go index 16ab3d9f..06c42a0c 100644 --- a/internal/backend/oidc/internal/storage/authrequest/storage.go +++ b/internal/backend/oidc/internal/storage/authrequest/storage.go @@ -12,8 +12,8 @@ import ( "time" "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" "github.com/siderolabs/omni/internal/backend/oidc/internal/models" ) diff --git a/internal/backend/oidc/internal/storage/authrequest/storage_test.go b/internal/backend/oidc/internal/storage/authrequest/storage_test.go index 28c58f53..c744c5b2 100644 --- a/internal/backend/oidc/internal/storage/authrequest/storage_test.go +++ b/internal/backend/oidc/internal/storage/authrequest/storage_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/siderolabs/omni/internal/backend/oidc/internal/storage/authrequest" ) diff --git a/internal/backend/oidc/internal/storage/keys/storage.go b/internal/backend/oidc/internal/storage/keys/storage.go index 0810aec6..e7fa42c2 100644 --- a/internal/backend/oidc/internal/storage/keys/storage.go +++ b/internal/backend/oidc/internal/storage/keys/storage.go @@ -12,69 +12,56 @@ import ( "crypto/x509" "errors" "fmt" + "slices" "sync" "time" "github.com/benbjohnson/clock" "github.com/cosi-project/runtime/pkg/safe" "github.com/cosi-project/runtime/pkg/state" + "github.com/go-jose/go-jose/v4" "github.com/google/uuid" "github.com/siderolabs/gen/xslices" - oidczitadel "github.com/zitadel/oidc/pkg/oidc" + oidczitadel "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" - "gopkg.in/square/go-jose.v2" "github.com/siderolabs/omni/client/pkg/omni/resources/oidc" "github.com/siderolabs/omni/internal/backend/oidc/external" + "github.com/siderolabs/omni/internal/pkg/auth/actor" ) // Storage implements JWT key signing storage around resource state. +// +//nolint:govet type Storage struct { - currentKey *jose.JSONWebKey - activeKeySet *jose.JSONWebKeySet - logger *zap.Logger - st state.State - clock clock.Clock - lock sync.Mutex + st state.State + clock clock.Clock + logger *zap.Logger + + mu sync.Mutex + currentKey op.SigningKey + activeKeySet []op.Key } // NewStorage creates a new Storage. -func NewStorage(logger *zap.Logger, st state.State, clock clock.Clock) *Storage { - return &Storage{ - logger: logger, +func NewStorage(st state.State, clock clock.Clock, logger *zap.Logger) *Storage { + result := &Storage{ st: st, clock: clock, + logger: logger, } -} -// GetSigningKey implements the op.Storage interface. -// -// It will be called when creating the OpenID Provider. -func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { - for ctx.Err() == nil { - err := s.keyRefresher(ctx, keyCh) - if err == nil { - return - } - - s.logger.Error("key refresher failed", zap.Error(err)) - - // wait some time before restarting - select { - case <-ctx.Done(): - return - case <-s.clock.After(10 * time.Second): - } - } + return result } -// GetKeySet implements the op.Storage interface. +// KeySet implements the op.Storage interface. // // It will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... -func (s *Storage) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) { - s.lock.Lock() - defer s.lock.Unlock() +func (s *Storage) KeySet() ([]op.Key, error) { + s.mu.Lock() + defer s.mu.Unlock() if s.activeKeySet != nil { return s.activeKeySet, nil @@ -85,39 +72,82 @@ func (s *Storage) GetKeySet(context.Context) (*jose.JSONWebKeySet, error) { // GetPublicKeyByID looks up the public key with the given ID. func (s *Storage) GetPublicKeyByID(keyID string) (any, error) { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() if s.activeKeySet == nil { return nil, errors.New("no active key set") } - for _, key := range s.activeKeySet.Keys { - if key.KeyID == keyID { - return key.Key, nil - } + idx := slices.IndexFunc(s.activeKeySet, func(key op.Key) bool { return key.ID() == keyID }) + if idx == -1 { + return nil, fmt.Errorf("key not found, ID %q", keyID) } - return nil, fmt.Errorf("key not found, ID %q", keyID) + return s.activeKeySet[idx].Key(), nil } // GetCurrentSigningKey returns the active and currently used signing key. -func (s *Storage) GetCurrentSigningKey() (*jose.JSONWebKey, error) { - s.lock.Lock() - defer s.lock.Unlock() +func (s *Storage) GetCurrentSigningKey() (op.SigningKey, error) { + s.mu.Lock() + defer s.mu.Unlock() - if s.currentKey != nil { - return s.currentKey, nil + if s.currentKey == nil { + return nil, errors.New("no current key") } - return nil, errors.New("no current key") + return s.currentKey, nil +} + +// Options is a set of options for the key refresher. +type Options struct { + keyCh chan<- op.SigningKey +} + +// Opts is a functional option for the key refresher. +type Opts func(*Options) + +// WithKeyCh sets the channel to send the generated key to. +func WithKeyCh(keyCh chan<- op.SigningKey) Opts { + return func(o *Options) { + o.keyCh = keyCh + } } -func (s *Storage) keyRefresher(ctx context.Context, keyCh chan<- jose.SigningKey) error { +// RunRefreshKey runs the key refresher in a loop. +func (s *Storage) RunRefreshKey(ctx context.Context, opts ...Opts) error { + ctx = actor.MarkContextAsInternalActor(ctx) + + var options Options + + for _, opt := range opts { + opt(&options) + } + + for ctx.Err() == nil { + err := s.runRefreshKey(ctx, options.keyCh) + if err == nil { + return nil + } + + s.logger.Error("key refresher failed", zap.Error(err)) + + // wait some time before restarting + select { + case <-ctx.Done(): + return nil + case <-s.clock.After(10 * time.Second): + } + } + + return nil +} + +func (s *Storage) runRefreshKey(ctx context.Context, ch chan<- op.SigningKey) error { ticker := s.clock.Ticker(external.KeyRotationInterval) defer ticker.Stop() - for { + for ctx.Err() == nil { // renew the key privateKey, err := s.generateKey() if err != nil { @@ -134,35 +164,34 @@ func (s *Storage) keyRefresher(ctx context.Context, keyCh chan<- jose.SigningKey return fmt.Errorf("failure to cleanup old keys: %w", err) } - jsonWebKey := jose.JSONWebKey{ - KeyID: keyID, - Key: privateKey, - Algorithm: string(jose.RS256), + key := &signingKey{ + id: keyID, + key: privateKey, + algorithm: jose.RS256, } - s.lock.Lock() - s.currentKey = &jsonWebKey - s.lock.Unlock() - - // send the new signing key to be used by OIDC - select { - case <-ctx.Done(): - return nil - case keyCh <- jose.SigningKey{ - Algorithm: jose.RS256, - Key: jsonWebKey, - }: - } + s.mu.Lock() + s.currentKey = key + s.mu.Unlock() s.logger.Info("new OIDC signing key generated", zap.String("key_id", keyID)) - // wait for key rotation interval + if ch != nil { + select { + case ch <- key: + case <-ctx.Done(): + return nil + } + } + select { case <-ctx.Done(): return nil case <-ticker.C: } } + + return nil } func (s *Storage) generateKey() (*rsa.PrivateKey, error) { @@ -191,7 +220,7 @@ func (s *Storage) cleanupOldKeys(ctx context.Context) error { return err } - newKeySet := &jose.JSONWebKeySet{} + newKeySet := make([]op.Key, 0, keys.Len()) for iter := keys.Iterator(); iter.Next(); { key := iter.Value() @@ -206,32 +235,26 @@ func (s *Storage) cleanupOldKeys(ctx context.Context) error { return err } } else { - publicKey, err := x509.ParsePKCS1PublicKey(key.TypedSpec().Value.PublicKey) + pKey, err := x509.ParsePKCS1PublicKey(key.TypedSpec().Value.PublicKey) if err != nil { return err } - newKeySet.Keys = append(newKeySet.Keys, jose.JSONWebKey{ - KeyID: key.Metadata().ID(), - Algorithm: string(jose.RS256), - Use: oidczitadel.KeyUseSignature, - Key: publicKey, + newKeySet = append(newKeySet, &publicKey{ + id: key.Metadata().ID(), + algorithm: jose.RS256, + publicKey: pKey, }) } } s.logger.Info("active OIDC public signing keys", - zap.Strings("key_ids", - xslices.Map(newKeySet.Keys, func(key jose.JSONWebKey) string { - return key.KeyID - }), - ), + zap.Strings("key_ids", xslices.Map(newKeySet, func(key op.Key) string { return key.ID() })), ) - s.lock.Lock() - defer s.lock.Unlock() - + s.mu.Lock() s.activeKeySet = newKeySet + s.mu.Unlock() return nil } @@ -245,3 +268,26 @@ func (s *Storage) MaxTokenLifetime() time.Duration { return external.OIDCTokenLifetime } + +//nolint:govet +type signingKey struct { + id string + algorithm jose.SignatureAlgorithm + key *rsa.PrivateKey +} + +func (s *signingKey) ID() string { return s.id } +func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { return s.algorithm } +func (s *signingKey) Key() any { return s.key } + +//nolint:govet +type publicKey struct { + id string + algorithm jose.SignatureAlgorithm + publicKey *rsa.PublicKey +} + +func (s *publicKey) ID() string { return s.id } +func (s *publicKey) Algorithm() jose.SignatureAlgorithm { return s.algorithm } +func (s *publicKey) Use() string { return oidczitadel.KeyUseSignature } +func (s *publicKey) Key() any { return s.publicKey } diff --git a/internal/backend/oidc/internal/storage/keys/storage_test.go b/internal/backend/oidc/internal/storage/keys/storage_test.go index 4d10eca0..2637d9f5 100644 --- a/internal/backend/oidc/internal/storage/keys/storage_test.go +++ b/internal/backend/oidc/internal/storage/keys/storage_test.go @@ -17,17 +17,22 @@ import ( "github.com/cosi-project/runtime/pkg/state" "github.com/cosi-project/runtime/pkg/state/impl/inmem" "github.com/cosi-project/runtime/pkg/state/impl/namespaced" + "github.com/go-jose/go-jose/v4" "github.com/siderolabs/gen/xslices" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/op" "go.uber.org/zap/zaptest" - "gopkg.in/square/go-jose.v2" "github.com/siderolabs/omni/internal/backend/oidc/external" "github.com/siderolabs/omni/internal/backend/oidc/internal/storage/keys" ) func TestStorage(t *testing.T) { + errCh := make(chan error, 1) + + t.Cleanup(func() { require.NoError(t, <-errCh) }) + var wg sync.WaitGroup t.Cleanup(wg.Wait) @@ -36,72 +41,74 @@ func TestStorage(t *testing.T) { t.Cleanup(cancel) st := state.WrapCore(namespaced.NewState(inmem.Build)) - clock := clock.NewMock() - - storage := keys.NewStorage(zaptest.NewLogger(t), st, clock) + clck := clock.NewMock() - keyCh := make(chan jose.SigningKey, 1) + storage := keys.NewStorage(st, clck, zaptest.NewLogger(t)) - // run the key rotation loop wg.Add(1) + keyCh := make(chan op.SigningKey, 1) + go func() { defer wg.Done() - storage.GetSigningKey(ctx, keyCh) + errCh <- storage.RunRefreshKey(ctx, keys.WithKeyCh(keyCh)) }() - // the first key should be generated immediately - var key jose.SigningKey + var signingKey op.SigningKey select { - case key = <-keyCh: + case signingKey = <-keyCh: case <-time.After(10 * time.Second): t.Fatal("timeout waiting for key") } - assert.Equal(t, jose.RS256, key.Algorithm) + gotKey, err := storage.GetCurrentSigningKey() + require.NoError(t, err) + assert.Equal(t, signingKey.ID(), gotKey.ID()) + assert.EqualValues(t, jose.RS256, signingKey.SignatureAlgorithm()) - privateKey, ok := key.Key.(jose.JSONWebKey) + privateKey, ok := signingKey.Key().(*rsa.PrivateKey) require.True(t, ok) // get the active set of public keys, it should have exactly one key // public key should match a private key - keySet, err := storage.GetKeySet(ctx) + keySet, err := storage.KeySet() require.NoError(t, err) - assert.Len(t, keySet.Keys, 1) - assert.Equal(t, keySet.Keys[0].KeyID, privateKey.KeyID) - assert.Equal(t, *keySet.Keys[0].Key.(*rsa.PublicKey), privateKey.Key.(*rsa.PrivateKey).PublicKey) //nolint:forcetypeassert + assert.Len(t, keySet, 1) + assert.Equal(t, keySet[0].ID(), signingKey.ID()) + assert.Equal(t, *keySet[0].Key().(*rsa.PublicKey), privateKey.PublicKey) //nolint:forcetypeassert - expectedPublicKeyIDs := []string{privateKey.KeyID} + expectedPublicKeyIDs := []string{signingKey.ID()} // advance time and generate one more key - will trigger one timer tick - clock.Add(external.KeyRotationInterval) + clck.Add(external.KeyRotationInterval) select { - case key = <-keyCh: + case signingKey = <-keyCh: case <-time.After(10 * time.Second): t.Fatal("timeout waiting for key") } - assert.Equal(t, jose.RS256, key.Algorithm) + assert.NotEqual(t, signingKey.ID(), gotKey.ID()) + assert.EqualValues(t, jose.RS256, signingKey.SignatureAlgorithm()) - privateKey, ok = key.Key.(jose.JSONWebKey) + _, ok = signingKey.Key().(*rsa.PrivateKey) require.True(t, ok) - expectedPublicKeyIDs = append(expectedPublicKeyIDs, privateKey.KeyID) + expectedPublicKeyIDs = append(expectedPublicKeyIDs, signingKey.ID()) // get the active set of public keys, it should have two keys now - keySet, err = storage.GetKeySet(ctx) + keySet, err = storage.KeySet() require.NoError(t, err) - assert.Len(t, keySet.Keys, 2) + assert.Len(t, keySet, 2) assert.Equal(t, sorted(expectedPublicKeyIDs), getKeyIDs(keySet)) // advance the time even more so that the first key expires // this triggers at least one timer tick, but the exact number varies due to the mocked clock - clock.Add(external.KeyRotationInterval + storage.MaxTokenLifetime() + time.Second) + clck.Add(external.KeyRotationInterval + storage.MaxTokenLifetime() + time.Second) expectedKeyToExpire := expectedPublicKeyIDs[0] expectedPublicKeyIDs = expectedPublicKeyIDs[1:] @@ -110,20 +117,20 @@ func TestStorage(t *testing.T) { // which will cause the first (oldest) key to be removed due to expiration for { select { - case key = <-keyCh: + case signingKey = <-keyCh: case <-time.After(10 * time.Second): t.Fatal("timeout waiting for key") } - assert.Equal(t, jose.RS256, key.Algorithm) + assert.EqualValues(t, jose.RS256, signingKey.SignatureAlgorithm()) - privateKey, ok = key.Key.(jose.JSONWebKey) + _, ok = signingKey.Key().(*rsa.PrivateKey) require.True(t, ok) - expectedPublicKeyIDs = append(expectedPublicKeyIDs, privateKey.KeyID) + expectedPublicKeyIDs = append(expectedPublicKeyIDs, signingKey.ID()) // get the active set of public keys - keySet, err = storage.GetKeySet(ctx) + keySet, err = storage.KeySet() require.NoError(t, err) keyIDs := getKeyIDs(keySet) @@ -138,7 +145,7 @@ func TestStorage(t *testing.T) { // get the active set of public keys - it should have at least two keys: // - the second key from the previous set, because it shouldn't be expired yet. // - the newly generated keys due to the tick(s) happened above. - keySet, err = storage.GetKeySet(ctx) + keySet, err = storage.KeySet() require.NoError(t, err) keyIDs := getKeyIDs(keySet) @@ -147,8 +154,8 @@ func TestStorage(t *testing.T) { assert.Equal(t, sorted(expectedPublicKeyIDs), keyIDs) } -func getKeyIDs(keySet *jose.JSONWebKeySet) []string { - ids := xslices.Map(keySet.Keys, func(k jose.JSONWebKey) string { return k.KeyID }) +func getKeyIDs(keySet []op.Key) []string { + ids := xslices.Map(keySet, func(k op.Key) string { return k.ID() }) return sorted(ids) } diff --git a/internal/backend/oidc/internal/storage/storage.go b/internal/backend/oidc/internal/storage/storage.go index f957ad88..f1a1e04e 100644 --- a/internal/backend/oidc/internal/storage/storage.go +++ b/internal/backend/oidc/internal/storage/storage.go @@ -13,10 +13,10 @@ import ( "github.com/benbjohnson/clock" "github.com/cosi-project/runtime/pkg/state" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" + "github.com/go-jose/go-jose/v4" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "go.uber.org/zap" - "gopkg.in/square/go-jose.v2" "github.com/siderolabs/omni/internal/backend/logging" "github.com/siderolabs/omni/internal/backend/oidc/external" @@ -42,10 +42,12 @@ var _ op.Storage = &Storage{} func NewStorage(st state.State, logger *zap.Logger) *Storage { logger = logger.With(logging.Component("oidc_storage")) + clk := clock.New() + return &Storage{ authRequestStorage: authrequest.NewStorage(), - tokenStorage: token.NewStorage(clock.New(), st), - keyStorage: keys.NewStorage(logger, st, clock.New()), + tokenStorage: token.NewStorage(st, clk), + keyStorage: keys.NewStorage(st, clk, logger), } } @@ -54,18 +56,11 @@ func (s *Storage) Health(context.Context) error { return nil } -// GetSigningKey implements the op.Storage interface. -// -// It will be called when creating the OpenID Provider. -func (s *Storage) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey) { - s.keyStorage.GetSigningKey(ctx, keyCh) -} - -// GetKeySet implements the op.Storage interface. +// KeySet implements the op.Storage interface. // // It will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... -func (s *Storage) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) { - return s.keyStorage.GetKeySet(ctx) +func (s *Storage) KeySet(context.Context) ([]op.Key, error) { + return s.keyStorage.KeySet() } // GetPublicKeyByID looks up the public key with the given ID. @@ -125,11 +120,6 @@ func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error { return s.authRequestStorage.DeleteAuthRequest(ctx, id) } -// AuthenticateRequest implements the `authenticate` interface of the login. -func (s *Storage) AuthenticateRequest(requestID, identity string) error { - return s.authRequestStorage.AuthenticateRequest(requestID, identity) -} - // GetPrivateClaimsFromScopes implements the op.Storage interface. // // It will be called for the creation of a JWT access token to assert claims for custom scopes. @@ -147,28 +137,28 @@ func (s *Storage) ValidateJWTProfileScopes(ctx context.Context, userID string, s // SetUserinfoFromScopes implements the op.Storage interface. // // It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check. -func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, clientID string, scopes []string) error { +func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { return s.tokenStorage.SetUserinfoFromScopes(ctx, userinfo, userID, clientID, scopes) } // SetUserinfoFromToken implements the op.Storage interface. // // It will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function. -func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, subject, origin string) error { +func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { return s.tokenStorage.SetUserinfoFromToken(ctx, userinfo, tokenID, subject, origin) } // SetIntrospectionFromToken implements the op.Storage interface. // // It will be called for the introspection endpoint, so we read the token and pass the information from that to the private function. -func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { +func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { return s.tokenStorage.SetIntrospectionFromToken(ctx, introspection, tokenID, subject, clientID) } -// GetKeyByIDAndUserID implements the op.Storage interface. +// GetKeyByIDAndClientID implements the op.Storage interface. // // It will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication). -func (s *Storage) GetKeyByIDAndUserID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { +func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { return s.tokenStorage.GetKeyByIDAndUserID(ctx, keyID, clientID) } @@ -207,7 +197,27 @@ func (s *Storage) RevokeToken(ctx context.Context, token string, userID string, return s.tokenStorage.RevokeToken(ctx, token, userID, clientID) } -// GetCurrentSigningKey returns the active and currently used signing key. -func (s *Storage) GetCurrentSigningKey() (*jose.JSONWebKey, error) { +// SigningKey returns the active and currently used signing key. +func (s *Storage) SigningKey(context.Context) (op.SigningKey, error) { return s.keyStorage.GetCurrentSigningKey() } + +// GetRefreshTokenInfo implements the op.Storage interface. +func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) { + return s.tokenStorage.GetRefreshTokenInfo(ctx, clientID, token) +} + +// SignatureAlgorithms implements the op.Storage interface. +func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) { + return []jose.SignatureAlgorithm{jose.RS256}, nil +} + +// AuthenticateRequest implements the `authenticate` interface of the login. +func (s *Storage) AuthenticateRequest(requestID, identity string) error { + return s.authRequestStorage.AuthenticateRequest(requestID, identity) +} + +// Run runs the key refresher in a loop. +func (s *Storage) Run(ctx context.Context) error { + return s.keyStorage.RunRefreshKey(ctx) +} diff --git a/internal/backend/oidc/internal/storage/token/mock_test.go b/internal/backend/oidc/internal/storage/token/mock_test.go index ecc5eb27..ec5d0006 100644 --- a/internal/backend/oidc/internal/storage/token/mock_test.go +++ b/internal/backend/oidc/internal/storage/token/mock_test.go @@ -6,10 +6,7 @@ package token_test import ( - "time" - - "github.com/zitadel/oidc/pkg/oidc" - "golang.org/x/text/language" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/siderolabs/omni/internal/backend/oidc/external" ) @@ -30,122 +27,3 @@ func (mockTokenRequest) GetScopes() []string { func (mockTokenRequest) GetSubject() string { return "some@example.com" } - -type mockUserInfoSetter struct { - claims map[string]any - subject string -} - -func (m *mockUserInfoSetter) GetSubject() string { - return "" -} - -func (m *mockUserInfoSetter) GetName() string { - return "" -} - -func (m *mockUserInfoSetter) GetGivenName() string { - return "" -} - -func (m *mockUserInfoSetter) GetFamilyName() string { - return "" -} - -func (m *mockUserInfoSetter) GetMiddleName() string { - return "" -} - -func (m *mockUserInfoSetter) GetNickname() string { - return "" -} - -func (m *mockUserInfoSetter) GetProfile() string { - return "" -} - -func (m *mockUserInfoSetter) GetPicture() string { - return "" -} - -func (m *mockUserInfoSetter) GetWebsite() string { - return "" -} - -func (m *mockUserInfoSetter) GetGender() oidc.Gender { - return "" -} - -func (m *mockUserInfoSetter) GetBirthdate() string { - return "" -} - -func (m *mockUserInfoSetter) GetZoneinfo() string { - return "" -} - -func (m *mockUserInfoSetter) GetLocale() language.Tag { - return language.Und -} - -func (m *mockUserInfoSetter) GetPreferredUsername() string { - return "" -} - -func (m *mockUserInfoSetter) GetEmail() string { - return "" -} - -func (m *mockUserInfoSetter) IsEmailVerified() bool { - return false -} - -func (m *mockUserInfoSetter) GetPhoneNumber() string { - return "" -} - -func (m *mockUserInfoSetter) IsPhoneNumberVerified() bool { - return false -} - -func (m *mockUserInfoSetter) GetAddress() oidc.UserInfoAddress { - return nil -} - -func (m *mockUserInfoSetter) GetClaim(string) any { - return nil -} - -func (m *mockUserInfoSetter) GetClaims() map[string]any { - return nil -} - -func (m *mockUserInfoSetter) SetSubject(sub string) { - m.subject = sub -} - -func (m *mockUserInfoSetter) AppendClaims(key string, values any) { - if m.claims == nil { - m.claims = map[string]any{} - } - - m.claims[key] = values -} - -func (m *mockUserInfoSetter) SetName(string) {} -func (m *mockUserInfoSetter) SetGivenName(string) {} -func (m *mockUserInfoSetter) SetFamilyName(string) {} -func (m *mockUserInfoSetter) SetMiddleName(string) {} -func (m *mockUserInfoSetter) SetNickname(string) {} -func (m *mockUserInfoSetter) SetUpdatedAt(time.Time) {} -func (m *mockUserInfoSetter) SetProfile(string) {} -func (m *mockUserInfoSetter) SetPicture(string) {} -func (m *mockUserInfoSetter) SetWebsite(string) {} -func (m *mockUserInfoSetter) SetGender(oidc.Gender) {} -func (m *mockUserInfoSetter) SetBirthdate(string) {} -func (m *mockUserInfoSetter) SetZoneinfo(string) {} -func (m *mockUserInfoSetter) SetLocale(language.Tag) {} -func (m *mockUserInfoSetter) SetPreferredUsername(string) {} -func (m *mockUserInfoSetter) SetEmail(string, bool) {} -func (m *mockUserInfoSetter) SetPhone(string, bool) {} -func (m *mockUserInfoSetter) SetAddress(oidc.UserInfoAddress) {} diff --git a/internal/backend/oidc/internal/storage/token/storage.go b/internal/backend/oidc/internal/storage/token/storage.go index 23fe4a23..fa9d385a 100644 --- a/internal/backend/oidc/internal/storage/token/storage.go +++ b/internal/backend/oidc/internal/storage/token/storage.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "sync" "time" @@ -16,11 +17,11 @@ import ( "github.com/benbjohnson/clock" "github.com/cosi-project/runtime/pkg/safe" "github.com/cosi-project/runtime/pkg/state" + "github.com/go-jose/go-jose/v4" "github.com/google/uuid" "github.com/siderolabs/gen/maps" - "github.com/zitadel/oidc/pkg/oidc" - "github.com/zitadel/oidc/pkg/op" - "gopkg.in/square/go-jose.v2" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" "github.com/siderolabs/omni/client/pkg/constants" "github.com/siderolabs/omni/client/pkg/omni/resources" @@ -37,17 +38,20 @@ import ( const Lifetime = 5 * time.Minute // Storage implements storing and handling OIDC access tokens, handling roles and claims. +// +//nolint:govet type Storage struct { - clock clock.Clock - state state.State + state state.State + clock clock.Clock + + mu sync.Mutex tokens map[string]*models.Token - lock sync.Mutex } // NewStorage creates a new token storage. -func NewStorage(clock clock.Clock, st state.State) *Storage { +func NewStorage(st state.State, clk clock.Clock) *Storage { return &Storage{ - clock: clock, + clock: clk, tokens: map[string]*models.Token{}, state: st, } @@ -88,8 +92,8 @@ func (s *Storage) TokenRequestByRefreshToken(context.Context, string) (op.Refres // // It will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed. func (s *Storage) TerminateSession(_ context.Context, userID string, clientID string) error { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() for _, token := range s.tokens { if token.ApplicationID == clientID && token.Subject == userID { @@ -106,8 +110,8 @@ func (s *Storage) TerminateSession(_ context.Context, userID string, clientID st // // It will be called after parsing and validation of the token revocation request. func (s *Storage) RevokeToken(_ context.Context, token string, _ string, clientID string) *oidc.Error { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() accessToken, ok := s.tokens[token] if ok { @@ -127,8 +131,8 @@ func (s *Storage) RevokeToken(_ context.Context, token string, _ string, clientI // accessToken will store an access_token in-memory based on the provided information. func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) *models.Token { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() token := &models.Token{ ID: uuid.NewString(), @@ -148,17 +152,17 @@ func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, aud // SetUserinfoFromScopes implements the op.Storage interface. // // It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check. -func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, userID, _ string, scopes []string) error { - return s.setUserinfo(ctx, userinfo, userID, scopes) +func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, _ string, scopes []string) error { + return s.setUserInfo(ctx, userinfo, userID, scopes) } // SetUserinfoFromToken implements the op.Storage interface. // // It will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function. -func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, tokenID, _, _ string) error { +func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, _, _ string) error { token, ok := func() (*models.Token, bool) { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() token, ok := s.tokens[tokenID] @@ -172,16 +176,16 @@ func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserIn return errors.New("token is invalid or has expired") } - return s.setUserinfo(ctx, userinfo, token.Subject, token.Scopes) + return s.setUserInfo(ctx, userinfo, token.Subject, token.Scopes) } // SetIntrospectionFromToken implements the op.Storage interface. // // It will be called for the introspection endpoint, so we read the token and pass the information from that to the private function. -func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection oidc.IntrospectionResponse, tokenID, subject, clientID string) error { +func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { token, ok := func() (*models.Token, bool) { - s.lock.Lock() - defer s.lock.Unlock() + s.mu.Lock() + defer s.mu.Unlock() token, ok := s.tokens[tokenID] @@ -198,16 +202,16 @@ func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection o // this will automatically be done by the library if you don't return an error // you can also return further information about the user / associated token // e.g. the userinfo (equivalent to userinfo endpoint) - err := s.setUserinfo(ctx, introspection, subject, token.Scopes) + err := s.setResponse(ctx, introspection, subject, token.Scopes) if err != nil { return err } // ...and also the requested scopes... - introspection.SetScopes(token.Scopes) + introspection.Scope = slices.Clone(token.Scopes) // ...and the client the token was issued to - introspection.SetClientID(token.ApplicationID) + introspection.ClientID = token.ApplicationID return nil } @@ -223,12 +227,38 @@ func (s *Storage) GetKeyByIDAndUserID(context.Context, string, string) (*jose.JS return nil, errors.New("not implemented") } -// setUserinfo sets the info based on the user, scopes and if necessary the clientID. -func (s *Storage) setUserinfo(ctx context.Context, userInfo oidc.UserInfoSetter, userID string, scopes []string) error { +// setResponse sets the info based on the user, scopes and if necessary the clientID. +func (s *Storage) setResponse(ctx context.Context, ir *oidc.IntrospectionResponse, userID string, scopes []string) error { for _, currentScope := range scopes { switch { case currentScope == oidc.ScopeOpenID: - userInfo.SetSubject(userID) + ir.Subject = userID + case strings.HasPrefix(currentScope, external.ScopeClusterPrefix): + cluster := currentScope[len(external.ScopeClusterPrefix):] + + impersonateGroups, err := s.getImpersonateGroups(ctx, cluster, userID) + if err != nil { + return err + } + + if ir.Claims == nil { + ir.Claims = map[string]any{} + } + + ir.Claims["cluster"] = cluster + ir.Claims["groups"] = impersonateGroups + } + } + + return nil +} + +// setUserInfo sets the info based on the user, scopes and if necessary the clientID. +func (s *Storage) setUserInfo(ctx context.Context, userInfo *oidc.UserInfo, userID string, scopes []string) error { + for _, currentScope := range scopes { + switch { + case currentScope == oidc.ScopeOpenID: + userInfo.Subject = userID case strings.HasPrefix(currentScope, external.ScopeClusterPrefix): cluster := currentScope[len(external.ScopeClusterPrefix):] @@ -372,3 +402,8 @@ func (s *Storage) ValidateJWTProfileScopes(_ context.Context, _ string, scopes [ return allowedScopes, nil } + +// GetRefreshTokenInfo implements the op.Storage interface. +func (s *Storage) GetRefreshTokenInfo(context.Context, string, string) (userID string, tokenID string, err error) { + return "", "", errors.New("not implemented") +} diff --git a/internal/backend/oidc/internal/storage/token/storage_test.go b/internal/backend/oidc/internal/storage/token/storage_test.go index 4c145a3b..6c33c44e 100644 --- a/internal/backend/oidc/internal/storage/token/storage_test.go +++ b/internal/backend/oidc/internal/storage/token/storage_test.go @@ -17,7 +17,7 @@ import ( "github.com/cosi-project/runtime/pkg/state/impl/namespaced" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/zitadel/oidc/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/siderolabs/omni/client/api/omni/specs" "github.com/siderolabs/omni/client/pkg/constants" @@ -35,7 +35,7 @@ func TestValidateJWTProfileScopes(t *testing.T) { st := state.WrapCore(namespaced.NewState(inmem.Build)) - s := token.NewStorage(clock.NewMock(), st) + s := token.NewStorage(st, clock.NewMock()) scopes, err := s.ValidateJWTProfileScopes(ctx, "", []string{ oidc.ScopeOpenID, @@ -89,7 +89,7 @@ func TestGetPrivateClaimsFromScopes(t *testing.T) { require.NoError(t, st.Create(ctx, user)) require.NoError(t, st.Create(ctx, cluster)) - s := token.NewStorage(clock.NewMock(), st) + s := token.NewStorage(st, clock.NewMock()) claims, err := s.GetPrivateClaimsFromScopes(ctx, userIdentity, "", []string{ oidc.ScopeOpenID, @@ -193,24 +193,24 @@ func TestSetUserinfoFromScopes(t *testing.T) { require.NoError(t, st.Create(ctx, user)) require.NoError(t, st.Create(ctx, cluster)) - s := token.NewStorage(clock.NewMock(), st) + s := token.NewStorage(st, clock.NewMock()) - var mockUserInfo mockUserInfoSetter + ui := &oidc.UserInfo{} - err := s.SetUserinfoFromScopes(ctx, &mockUserInfo, "test@example.com", "", []string{ + err := s.SetUserinfoFromScopes(ctx, ui, "test@example.com", "", []string{ oidc.ScopeOpenID, external.ScopeClusterPrefix + "talos-default", "other-scope", }) require.NoError(t, err) - assert.Equal(t, userIdentity, mockUserInfo.subject) + assert.Equal(t, userIdentity, ui.Subject) - actualCluster, ok := mockUserInfo.claims["cluster"] + actualCluster, ok := ui.Claims["cluster"] require.True(t, ok) assert.Equal(t, clusterID, actualCluster) - actualGroups, ok := mockUserInfo.claims["groups"] + actualGroups, ok := ui.Claims["groups"] require.True(t, ok) actualGroupsSlc, ok := actualGroups.([]string) @@ -242,35 +242,35 @@ func TestTokenIntrospection(t *testing.T) { require.NoError(t, st.Create(ctx, identity)) require.NoError(t, st.Create(ctx, user)) - clock := clock.NewMock() - s := token.NewStorage(clock, st) + clck := clock.NewMock() + s := token.NewStorage(st, clck) // create token and try fetching information from it tokenID, expiration, err := s.CreateAccessToken(ctx, req) require.NoError(t, err) - assert.Equal(t, clock.Now().Add(token.Lifetime), expiration) + assert.Equal(t, clck.Now().Add(token.Lifetime), expiration) assert.NotEmpty(t, tokenID) - var mockUserInfo mockUserInfoSetter + ui := &oidc.UserInfo{} - err = s.SetUserinfoFromToken(ctx, &mockUserInfo, tokenID, userIdentity, "") + err = s.SetUserinfoFromToken(ctx, ui, tokenID, userIdentity, "") require.NoError(t, err) - assert.Equal(t, "some@example.com", mockUserInfo.subject) + assert.Equal(t, "some@example.com", ui.Subject) assert.Equal(t, map[string]any{ "cluster": "cluster1", "groups": []string{constants.DefaultAccessGroup}, - }, mockUserInfo.claims) + }, ui.Claims) // advance time so that token expires - clock.Add(token.Lifetime + time.Second) + clck.Add(token.Lifetime + time.Second) - err = s.SetUserinfoFromToken(ctx, &mockUserInfo, tokenID, "", "") + err = s.SetUserinfoFromToken(ctx, ui, tokenID, "", "") assert.Error(t, err) // invalid ID - err = s.SetUserinfoFromToken(ctx, &mockUserInfo, "invalid", "", "") + err = s.SetUserinfoFromToken(ctx, ui, "invalid", "", "") assert.Error(t, err) } @@ -281,7 +281,7 @@ func TestRevokeToken(t *testing.T) { st := state.WrapCore(namespaced.NewState(inmem.Build)) clock := clock.NewMock() - s := token.NewStorage(clock, st) + s := token.NewStorage(st, clock) tokenID, _, err := s.CreateAccessToken(ctx, mockTokenRequest{}) require.NoError(t, err) @@ -300,7 +300,7 @@ func TestTerminateSession(t *testing.T) { st := state.WrapCore(namespaced.NewState(inmem.Build)) clock := clock.NewMock() - s := token.NewStorage(clock, st) + s := token.NewStorage(st, clock) tokenID, _, err := s.CreateAccessToken(ctx, mockTokenRequest{}) require.NoError(t, err) diff --git a/internal/backend/oidc/oidc.go b/internal/backend/oidc/oidc.go index 3e427dc5..1ac909b9 100644 --- a/internal/backend/oidc/oidc.go +++ b/internal/backend/oidc/oidc.go @@ -10,12 +10,11 @@ import ( "context" "crypto/rand" "fmt" - "os" "github.com/cosi-project/runtime/pkg/state" "github.com/sirupsen/logrus" oidc_logging "github.com/zitadel/logging" - "github.com/zitadel/oidc/pkg/op" + "github.com/zitadel/oidc/v3/pkg/op" "go.uber.org/zap" "github.com/siderolabs/omni/client/pkg/constants" @@ -28,7 +27,7 @@ func init() { } // NewStorage creates OIDC internal storage. -func NewStorage(st state.State, logger *zap.Logger) *storage.Storage { +func NewStorage(st state.State, logger *zap.Logger) Storage { return storage.NewStorage(st, logger) } @@ -36,11 +35,11 @@ func NewStorage(st state.State, logger *zap.Logger) *storage.Storage { type Provider struct { op.OpenIDProvider - storage *storage.Storage + storage Storage } // NewProvider creates new OIDC provider. -func NewProvider(ctx context.Context, store *storage.Storage) (*Provider, error) { +func NewProvider(store Storage) (*Provider, error) { issuerEndpoint, err := config.Config.GetOIDCIssuerEndpoint() if err != nil { return nil, err @@ -54,8 +53,7 @@ func NewProvider(ctx context.Context, store *storage.Storage) (*Provider, error) return nil, fmt.Errorf("failed to generate crypto key: %w", err) } - config := &op.Config{ - Issuer: issuerEndpoint, + cfg := &op.Config{ CryptoKey: cryptoKey, DefaultLogoutRedirectURI: "/logout", CodeMethodS256: true, @@ -65,12 +63,14 @@ func NewProvider(ctx context.Context, store *storage.Storage) (*Provider, error) RequestObjectSupported: true, } + var opts []op.Option + if constants.IsDebugBuild { // allow HTTP in OIDC issuer endpoint - os.Setenv(op.OidcDevMode, "true") //nolint:errcheck + opts = append(opts, op.WithAllowInsecure()) } - h, err := op.NewOpenIDProvider(ctx, config, store) + h, err := op.NewProvider(cfg, store, op.StaticIssuer(issuerEndpoint), opts...) if err != nil { return nil, err } @@ -85,3 +85,11 @@ func NewProvider(ctx context.Context, store *storage.Storage) (*Provider, error) func (p *Provider) AuthenticateRequest(requestID, identity string) error { return p.storage.AuthenticateRequest(requestID, identity) } + +// Storage is the OIDC storage interface. It's here because storage.Storage is in internal package. +type Storage interface { + op.Storage + AuthenticateRequest(requestID, identity string) error + GetPublicKeyByID(keyID string) (any, error) + Run(context.Context) error +} diff --git a/internal/backend/runtime/proxy_runtime.go b/internal/backend/runtime/proxy_runtime.go index 22d47df2..ac40c371 100644 --- a/internal/backend/runtime/proxy_runtime.go +++ b/internal/backend/runtime/proxy_runtime.go @@ -128,11 +128,12 @@ func fill(r WatchResponse, field string, desc bool) error { } func changeTotal(ev WatchResponse, total *int32) int32 { - switch EventType(ev) { //nolint:exhaustive + switch EventType(ev) { case resources.EventType_CREATED: *total++ case resources.EventType_DESTROYED: *total-- + case resources.EventType_UNKNOWN, resources.EventType_UPDATED, resources.EventType_BOOTSTRAPPED: } return *total diff --git a/internal/backend/server.go b/internal/backend/server.go index 255a4e77..ee84d608 100644 --- a/internal/backend/server.go +++ b/internal/backend/server.go @@ -177,7 +177,9 @@ func (s *Server) Run(ctx context.Context) error { runtimeState := s.omniRuntime.State() oidcStorage := oidc.NewStorage(runtimeState, s.logger) - oidcProvider, err := oidc.NewProvider(ctx, oidcStorage) + eg.Go(func() error { return oidcStorage.Run(ctx) }) + + oidcProvider, err := oidc.NewProvider(oidcStorage) if err != nil { return err } @@ -204,7 +206,7 @@ func (s *Server) Run(ctx context.Context) error { } } - mux, err := makeMux(imageFactoryHandler, oidcProvider.HttpHandler(), samlHandler, s.omniRuntime, s.logger) + mux, err := makeMux(imageFactoryHandler, oidcProvider, samlHandler, s.omniRuntime, s.logger) if err != nil { return fmt.Errorf("failed to create mux: %w", err) }