From c49d727e02af4237427675c899460dae42981df9 Mon Sep 17 00:00:00 2001 From: Aaishwarya Thoke Date: Fri, 27 Mar 2026 14:41:28 +0100 Subject: [PATCH 1/2] zitadel library implementation --- .github/workflows/helm-tests.yaml | 2 +- Makefile | 2 +- go.mod | 8 +- go.sum | 30 +- internal/session/helper_test.go | 66 ++- internal/session/manager.go | 204 ++++------ internal/session/manager_test.go | 652 +++++++++++++++++++++++++++--- internal/session/openid.go | 38 +- 8 files changed, 805 insertions(+), 197 deletions(-) diff --git a/.github/workflows/helm-tests.yaml b/.github/workflows/helm-tests.yaml index 6d91063..883a730 100644 --- a/.github/workflows/helm-tests.yaml +++ b/.github/workflows/helm-tests.yaml @@ -27,4 +27,4 @@ jobs: - name: Import the Docker image run: k3d image import localhost/session-manager:latest -c k3scluster - name: Run the tests - run: cd ./helm-tests && go test -v -count=1 -race ./... + run: cd ./helm-tests && go test -v -count=1 -race -timeout 20m ./... diff --git a/Makefile b/Makefile index a05add0..310546a 100644 --- a/Makefile +++ b/Makefile @@ -166,7 +166,7 @@ k3d-setup: .PHONY: helm-integration-test-run helm-integration-test-run: kubectl config current-context - cd ./helm-tests/integration && go test -v -count=1 -race . + cd ./helm-tests/integration && go test -v -count=1 -race -timeout 20m . .PHONY: k3d-teardown k3d-teardown: diff --git a/go.mod b/go.mod index 69ad421..b4435b6 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/testcontainers/testcontainers-go/modules/valkey v0.41.0 github.com/valkey-io/valkey-go v1.0.73 github.com/veqryn/slog-context v0.9.0 + github.com/zitadel/oidc/v3 v3.45.5 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/metric v1.42.0 go.opentelemetry.io/otel/trace v1.42.0 @@ -80,6 +81,7 @@ require ( github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/cel-go v0.26.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -107,6 +109,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/morikuni/aec v1.1.0 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect @@ -140,7 +143,7 @@ require ( github.com/samber/slog-multi v1.7.1 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shirou/gopsutil/v4 v4.26.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -159,6 +162,8 @@ require ( github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/yusufpapurcu/wmi v1.2.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/collector/featuregate v1.53.0 // indirect go.opentelemetry.io/collector/pdata v1.53.0 // indirect @@ -188,6 +193,7 @@ require ( golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index b095b71..a40b803 100644 --- a/go.sum +++ b/go.sum @@ -23,7 +23,10 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= @@ -90,6 +93,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +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.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -133,11 +138,15 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= @@ -158,6 +167,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA= +github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -214,6 +225,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= +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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= @@ -297,6 +312,8 @@ github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= @@ -316,8 +333,8 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= @@ -378,6 +395,12 @@ github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1: github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +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.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrDQ= +github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE= +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/collector/featuregate v1.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw= @@ -474,6 +497,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -496,7 +521,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= diff --git a/internal/session/helper_test.go b/internal/session/helper_test.go index 7193d1c..749a1f3 100644 --- a/internal/session/helper_test.go +++ b/internal/session/helper_test.go @@ -1,18 +1,41 @@ package session_test import ( + "crypto/rand" + "crypto/rsa" "encoding/json" "net/http" "net/http/httptest" "testing" + "time" + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" "github.com/openkcm/common-sdk/pkg/oidc" + "github.com/stretchr/testify/require" "github.com/openkcm/session-manager/internal/session" ) +// StartOIDCServer creates a test OIDC server with signed ID tokens for testing. func StartOIDCServer(t *testing.T, fail bool, algs ...string) *httptest.Server { t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "generating RSA key for OIDC test server") + + const keyID = "test-kid" + signingKey := jose.SigningKey{ + Algorithm: jose.RS256, + Key: jose.JSONWebKey{ + Key: privateKey, + KeyID: keyID, + Algorithm: string(jose.RS256), + }, + } + signer, err := jose.NewSigner(signingKey, (&jose.SignerOptions{}).WithType("JWT")) + require.NoError(t, err, "creating JWT signer") + var server *httptest.Server server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if fail { @@ -21,7 +44,6 @@ func StartOIDCServer(t *testing.T, fail bool, algs ...string) *httptest.Server { _, _ = w.Write([]byte(`{"error": "invalid_request", "error_description": "Token exchange failed"}`)) return } - // Determine supported algorithms by passed arguments or set to default value var algList []string if len(algs) == 0 { algList = []string{"RS256"} @@ -38,15 +60,47 @@ func StartOIDCServer(t *testing.T, fail bool, algs ...string) *httptest.Server { JwksURI: server.URL + "/.well-known/jwks.json", IDTokenSigningAlgValuesSupported: algList, }) + case "/.well-known/jwks.json": - _, _ = w.Write([]byte(`{"keys":[{"kty": "RSA", "e": "AQAB", "use": "sig", "kid": "MwK4iAYDIILiA_ymyjAwMGAaLlW84jOAqR0V-oojuIk", "alg": "RS256", "n": "nD89GVZMXuv_MSbH_SqDnU5oQgLlcH6yGe5LkXSdP_UzBXt49wPRoVHE-W981oylw9vhzfNBE8JY0PSkxVvYCWwYP86YWVtJix23iONYpXeAH9M1ep4Gzo1y0XnjAKURi-sN5T5nUBZ-fkODvyr6ALIUG3AXzaRow1RMmhUOx1spKGS34DJPv0D3E6aVcGkwgUwZcBhObYxGQdMAYi-OYDDS3uAkFciO3G1Bpz4nyW_JaV7i4zkMOH6-2wYFt1fjMsyc0lt1eRqdUVdANy0kDtmIXnjgjKN0Isr16flzfRDXfOQmaBPp14hQPiAgVFaqvTIvXucXOkiWcAQWhas2Aw"}]}`)) + publicJWKS := jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{{ + Key: &privateKey.PublicKey, + KeyID: keyID, + Algorithm: string(jose.RS256), + Use: "sig", + }}, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(publicJWKS) + case "/oauth2/token": w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) + + now := time.Now() + claims := map[string]any{ + "iss": server.URL, + "sub": "jwt-test", + "aud": []string{""}, + "exp": now.Add(time.Hour).Unix(), + "iat": now.Unix(), + "sid": "test-sid", + "user_uuid": "test-uuid", + "given_name": "John", + "family_name": "Doe", + "email": "john.doe@example.com", + "groups": []string{"admin"}, + } + + idToken, signErr := jwt.Signed(signer).Claims(claims).Serialize() + if signErr != nil { + http.Error(w, "id_token sign error", http.StatusInternalServerError) + return + } + tokenResponse := session.TokenResponse{ - AccessToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.m3RtvePjRxhO0-O2FOzJvYWUNZqxdQ5p4wI5YTjC8BmFzCH-2gMjk1RSG00-_q0PkcKqoD_hZFVub88298nhF7WpEj2pEWvDhWeG4g3H4JxcFw-a2Pwam80qdrOOA8NkmDtTewC90yshYcGktGMHk5jjfh4sKRaZz9FmkBpc2G9I1NyxcCyj9yatMu64yFDNa0-CbaSsWyCFgsKvNxM944nJkT7q3OLFz_Tgn4HSXExEDE_Xkwhz6zykg12tcU9-5Fk40yEfOEaBCTmuv3AMguBOlEBD1X2IsPcz03My5bpECEFIuRbqu-xvny1vEhzjNB565uk4Es9PdLwi_6frWA", - RefreshToken: "refresh-token", - IDToken: "eyJhbGciOiJSUzI1NiIsImtpZCI6Ik13SzRpQVlESUlMaUFfeW15akF3TUdBYUxsVzg0ak9BcVIwVi1vb2p1SWsiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJqd3QtdGVzdCIsImp0aSI6IjIzNDE0MzUiLCJhdF9oYXNoIjoicVJiNThaanpTZFByYnpGQ3hielJFZyIsIm5iZiI6MTc2NDExNDM3MSwiZXhwIjoxNzY0MTI1MTcxLCJpYXQiOjE3NjQxMTQzNzEsImlzcyI6InNhcCIsImF1ZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20ifQ.JhC2oGYRHTL4NVaz1CZKWop_Iq54fxQOQL2pJap1LIMFKRz9RqgZr_WMulBLjNxppS3v5KFaMMp28YirzhzJQVbIlrEuUQZQCeODmLYSVkyeQKGb9WTSirzZInZbICjfocgppSzZ_Z8_P0GSS_h4IEFgcK0jnfb-2O_Xef1dYSoxA-sOFCxvn48jnjBLNjRQh2uYY61unJRzAbchXTBCtTSKNL1SEM4rCvV9b9dfYKBSlaQ11DKzzC1Zd5xG4JNkrbDXYu6MAxYLz_getXsQh6rVqOnMjUOMQLjUcuMuSva1Fh9gCeJNWsy34bh6lfScBb67L3i5D1s8pciLYTNMDQ", + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + IDToken: idToken, TokenType: "Bearer", ExpiresIn: 3600, } diff --git a/internal/session/manager.go b/internal/session/manager.go index 03c1b82..7dc6d9c 100644 --- a/internal/session/manager.go +++ b/internal/session/manager.go @@ -2,12 +2,8 @@ package session import ( "context" - "crypto/sha256" - "crypto/sha512" - "encoding/base64" "encoding/json" "fmt" - "hash" "net/http" "net/url" "strings" @@ -17,11 +13,12 @@ import ( "github.com/go-jose/go-jose/v4/jwt" "github.com/google/uuid" "github.com/openkcm/common-sdk/pkg/csrf" - "github.com/openkcm/common-sdk/pkg/oidc" "github.com/patrickmn/go-cache" + "github.com/zitadel/oidc/v3/pkg/client/rp" otlpaudit "github.com/openkcm/common-sdk/pkg/otlp/audit" slogctx "github.com/veqryn/slog-context" + zitadeloidc "github.com/zitadel/oidc/v3/pkg/oidc" "github.com/openkcm/session-manager/internal/config" "github.com/openkcm/session-manager/internal/credentials" @@ -34,6 +31,38 @@ const ( LoginCSRFCookieName = "LoginCSRF" ) +// AppIDTokenClaims extends IDTokenClaims with application-specific claims (user_uuid and groups). +type AppIDTokenClaims struct { + zitadeloidc.IDTokenClaims + + UserUUID string `json:"user_uuid,omitempty"` + Groups []string `json:"groups,omitempty"` +} + +// UnmarshalJSON delegates to IDTokenClaims and extracts user_uuid and groups from the claims map. +func (c *AppIDTokenClaims) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &c.IDTokenClaims); err != nil { + return err + } + if v, ok := c.Claims["user_uuid"]; ok { + if s, ok := v.(string); ok { + c.UserUUID = s + } + } + if v, ok := c.Claims["groups"]; ok { + if arr, ok := v.([]any); ok { + groups := make([]string, 0, len(arr)) + for _, item := range arr { + if s, ok := item.(string); ok { + groups = append(groups, s) + } + } + c.Groups = groups + } + } + return nil +} + type Manager struct { trustRepo trust.OIDCMappingRepository sessions Repository @@ -147,7 +176,7 @@ func (m *Manager) LoadState(ctx context.Context, stateID string) (State, error) return m.sessions.LoadState(ctx, stateID) } -func (m *Manager) authURI(openidConf *oidc.Configuration, state State, pkce pkce.PKCE, mapping trust.OIDCMapping) (string, error) { +func (m *Manager) authURI(openidConf *zitadeloidc.DiscoveryConfiguration, state State, pkce pkce.PKCE, mapping trust.OIDCMapping) (string, error) { u, err := url.Parse(openidConf.AuthorizationEndpoint) if err != nil { return "", fmt.Errorf("parsing authorisation endpoint url: %w", err) @@ -163,9 +192,10 @@ func (m *Manager) authURI(openidConf *oidc.Configuration, state State, pkce pkce q.Set("redirect_uri", m.callbackURL.String()) for _, parameter := range m.queryParametersAuth { value, ok := mapping.Properties[parameter] - if ok { - q.Set(parameter, value) + if !ok { + return "", fmt.Errorf("missing auth parameter: %s", parameter) } + q.Set(parameter, value) } u.RawQuery = q.Encode() @@ -173,25 +203,12 @@ func (m *Manager) authURI(openidConf *oidc.Configuration, state State, pkce pkce return u.String(), nil } -func (m *Manager) getProviderKeySet(ctx context.Context, oidcConf *oidc.Configuration) (*jose.JSONWebKeySet, error) { - var keySet jose.JSONWebKeySet - uri := oidcConf.JwksURI - req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) - if err != nil { - return nil, fmt.Errorf("creating a new HTTP request: %w", err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("executing an http request: %w", err) +// newRemoteKeySet creates a remote key set for the given JWKS URI. +func (m *Manager) newRemoteKeySet(oidcConf *zitadeloidc.DiscoveryConfiguration) zitadeloidc.KeySet { + httpClient := &http.Client{ + Transport: m.newCreds("").Transport(), } - - err = json.NewDecoder(resp.Body).Decode(&keySet) - if err != nil { - return nil, fmt.Errorf("decoding keyset response: %w", err) - } - - return &keySet, nil + return rp.NewRemoteKeySet(httpClient, oidcConf.JwksURI) } func (m *Manager) FinaliseOIDCLogin(ctx context.Context, stateID, code, fingerprint string) (OIDCSessionData, error) { @@ -200,7 +217,6 @@ func (m *Manager) FinaliseOIDCLogin(ctx context.Context, stateID, code, fingerpr return OIDCSessionData{}, fmt.Errorf("loading state from the storage: %w", err) } - // audit log metadata correlationId := uuid.NewString() metadata, err := otlpaudit.NewEventMetadata("session manager", state.TenantID, correlationId) if err != nil { @@ -241,77 +257,47 @@ func (m *Manager) FinaliseOIDCLogin(ctx context.Context, stateID, code, fingerpr sessionID := m.pkce.SessionID() csrfToken := csrf.NewToken(sessionID, m.csrfSecret) - algs := make([]jose.SignatureAlgorithm, 0, len(openidConf.IDTokenSigningAlgValuesSupported)) - for _, alg := range openidConf.IDTokenSigningAlgValuesSupported { - algs = append(algs, jose.SignatureAlgorithm(alg)) - } - token, err := jwt.ParseSigned(tokens.IDToken, algs) - if err != nil { - m.sendUserLoginFailureAudit(ctx, metadata, state.TenantID, "failed to parse id token") - return OIDCSessionData{}, fmt.Errorf("parsing id token: %w, %s", err, algs) - } - keyset, err := m.getProviderKeySet(ctx, openidConf) - if err != nil { - m.sendUserLoginFailureAudit(ctx, metadata, state.TenantID, "failed to get jwks for provider") - return OIDCSessionData{}, fmt.Errorf("getting jwks for a provider: %w", err) - } - - type CustomClaims struct { - SID string `json:"sid"` - UserUUID string `json:"user_uuid"` - GivenName string `json:"given_name"` - FamilyName string `json:"family_name"` - Email string `json:"email"` - Groups []string `json:"groups"` - } - - type ExtraClaims struct { - AtHash string `json:"at_hash,omitempty"` - } + keySet := m.newRemoteKeySet(openidConf) + verifier := rp.NewIDTokenVerifier( + mapping.IssuerURL, + m.getClientID(mapping), + keySet, + rp.WithSupportedSigningAlgorithms(openidConf.IDTokenSigningAlgValuesSupported...), + ) - var standardClaims jwt.Claims - var customClaims CustomClaims - var extraClaims ExtraClaims - err = token.Claims(keyset, &standardClaims, &customClaims, &extraClaims) + idTokenClaims, err := rp.VerifyTokens[*AppIDTokenClaims](ctx, tokens.AccessToken, tokens.IDToken, verifier) if err != nil { - m.sendUserLoginFailureAudit(ctx, metadata, state.TenantID, "failed to get JWT claims") - return OIDCSessionData{}, fmt.Errorf("getting JWT claims: %w", err) + m.sendUserLoginFailureAudit(ctx, metadata, state.TenantID, "id token verification failed") + return OIDCSessionData{}, fmt.Errorf("verifying id token: %w", err) } - if extraClaims.AtHash != "" { - err := m.verifyAccessToken(tokens.AccessToken, extraClaims.AtHash, token) - if err != nil { - return OIDCSessionData{}, err - } - } - - // prepare the auth context used by ExtAuthZ authContext := map[string]string{ "issuer": mapping.IssuerURL, "client_id": m.getClientID(mapping), } for _, parameter := range m.authContextKeys { value, ok := mapping.Properties[parameter] - if ok { - authContext[parameter] = value + if !ok { + return OIDCSessionData{}, fmt.Errorf("missing auth context parameter: %s", parameter) } + authContext[parameter] = value } session := Session{ ID: sessionID, TenantID: state.TenantID, - ProviderID: customClaims.SID, + ProviderID: idTokenClaims.SessionID, Fingerprint: fingerprint, CSRFToken: csrfToken, Issuer: mapping.IssuerURL, Claims: Claims{ - Subject: standardClaims.Subject, - UserUUID: customClaims.UserUUID, - GivenName: customClaims.GivenName, - FamilyName: customClaims.FamilyName, - Email: customClaims.Email, - Groups: customClaims.Groups, + Subject: idTokenClaims.GetSubject(), + UserUUID: idTokenClaims.UserUUID, + GivenName: idTokenClaims.GivenName, + FamilyName: idTokenClaims.FamilyName, + Email: idTokenClaims.Email, + Groups: idTokenClaims.Groups, }, AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, @@ -336,7 +322,6 @@ func (m *Manager) FinaliseOIDCLogin(ctx context.Context, stateID, code, fingerpr return OIDCSessionData{}, fmt.Errorf("deleting state: %w", err) } - // audit userLoginSuccess event, err := otlpaudit.NewUserLoginSuccessEvent(metadata, state.TenantID, otlpaudit.LOGINMETHOD_OPENIDCONNECT, otlpaudit.MFATYPE_NONE, otlpaudit.USERTYPE_BUSINESS, state.TenantID) if err != nil { return OIDCSessionData{}, fmt.Errorf("creating audit log: %w", err) @@ -387,7 +372,6 @@ func (m *Manager) Logout(ctx context.Context, sessionID string) (string, error) if oidcConf.EndSessionEndpoint == "" { slogctx.Warn(ctx, "the provider does not support RP-Initiated Logout") - // Redirect to the landing page if possible if m.postLogoutRedirectURL != "" { return m.postLogoutRedirectURL, nil } @@ -407,11 +391,13 @@ func (m *Manager) Logout(ctx context.Context, sessionID string) (string, error) vals.Set("post_logout_redirect_uri", m.postLogoutRedirectURL) } - for _, parameter := range m.queryParametersLogout { - value, ok := mapping.Properties[parameter] - if ok { - vals.Set(parameter, value) + for _, p := range m.queryParametersLogout { + v, ok := mapping.Properties[p] + if !ok { + return "", fmt.Errorf("missing auth parameter: %s", p) } + + vals.Set(p, v) } redirectURL.RawQuery = vals.Encode() @@ -439,11 +425,9 @@ func (m *Manager) BCLogout(ctx context.Context, logoutJWT string) error { return fmt.Errorf("parsing jwt: %w", err) } - // Logout token must contain either a sub or a sid Claim, and may contain both. type logoutTokenClaims struct { jwt.Claims - // Events is always "http://schemas.openid.net/event/backchannel-logout": {} Events map[string]json.RawMessage `json:"events,omitempty"` SessionID string `json:"sid,omitempty"` } @@ -480,14 +464,13 @@ func (m *Manager) BCLogout(ctx context.Context, logoutJWT string) error { return fmt.Errorf("getting oidc config: %w", err) } - keyset, err := m.getProviderKeySet(ctx, oidcConf) - if err != nil { - return fmt.Errorf("getting jwks for a provider: %w", err) - } - - var claims logoutTokenClaims - if err := token.Claims(keyset, &claims); err != nil { - return fmt.Errorf("parsing claims: %w", err) + keyset := m.newRemoteKeySet(oidcConf) + if _, err := rp.VerifyIDToken[*zitadeloidc.IDTokenClaims](ctx, logoutJWT, rp.NewIDTokenVerifier( + mapping.IssuerURL, + m.getClientID(mapping), + keyset, + )); err != nil { + return fmt.Errorf("verifying logout token: %w", err) } if err := m.sessions.DeleteSession(ctx, session); err != nil { @@ -523,7 +506,6 @@ func (m *Manager) MakeSessionCookie(ctx context.Context, tenantID, value string) func (m *Manager) MakeCSRFCookie(ctx context.Context, tenantID, value string) (*http.Cookie, error) { csrfCookie := m.csrfCookieTemplate.ToCookie(value) - if tenantID != "" { csrfCookie.Name = csrfCookie.Name + "-" + tenantID } @@ -563,9 +545,7 @@ func checkCookie(ctx context.Context, csrfCookie *http.Cookie) { } } -// sendUserLoginFailureAudit creates the user-login-failure audit event and sends it. -// The function logs any errors encountered while creating or sending the event but -// does not propagate them to the caller. +// sendUserLoginFailureAudit sends a user-login-failure audit event, logging any errors without propagating them. func (m *Manager) sendUserLoginFailureAudit(ctx context.Context, metadata otlpaudit.EventMetadata, objectID, reason string) { if m.audit == nil { slogctx.Warn(ctx, "audit logger is nil; skipping user login failure event") @@ -585,29 +565,6 @@ func (m *Manager) sendUserLoginFailureAudit(ctx context.Context, metadata otlpau slogctx.Debug(ctx, "sent audit log for user login failure") } -func (m *Manager) verifyAccessToken(accessToken, atHash string, idToken *jwt.JSONWebToken) error { - var h hash.Hash - switch alg := idToken.Headers[0].Algorithm; alg { - case "RS256", "ES256", "PS256": - h = sha256.New() - case "RS384", "ES384", "PS384": - h = sha512.New384() - case "RS512", "ES512", "PS512", "EdDSA": - h = sha512.New() - default: - return fmt.Errorf("oidc: unsupported signing algorithm %q", alg) - } - - h.Write([]byte(accessToken)) // NOSONAR - sum := h.Sum(nil)[:h.Size()/2] - actual := base64.RawURLEncoding.EncodeToString(sum) - if actual != atHash { - return serviceerr.ErrInvalidAtHash - } - - return nil -} - func (m *Manager) httpClient(mapping trust.OIDCMapping) *http.Client { creds := m.newCreds(m.getClientID(mapping)) return &http.Client{ @@ -623,7 +580,7 @@ func (m *Manager) getClientID(mapping trust.OIDCMapping) string { return m.clientID } -func (m *Manager) exchangeCode(ctx context.Context, openidConf *oidc.Configuration, code, codeVerifier string, mapping trust.OIDCMapping) (tokenResponse, error) { +func (m *Manager) exchangeCode(ctx context.Context, openidConf *zitadeloidc.DiscoveryConfiguration, code, codeVerifier string, mapping trust.OIDCMapping) (tokenResponse, error) { data := url.Values{} data.Set("grant_type", "authorization_code") data.Set("code", code) @@ -632,9 +589,10 @@ func (m *Manager) exchangeCode(ctx context.Context, openidConf *oidc.Configurati data.Set("client_id", m.getClientID(mapping)) for _, parameter := range m.queryParametersToken { value, ok := mapping.Properties[parameter] - if ok { - data.Set(parameter, value) + if !ok { + return tokenResponse{}, fmt.Errorf("missing token parameter: %s", parameter) } + data.Set(parameter, value) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, openidConf.TokenEndpoint, strings.NewReader(data.Encode())) diff --git a/internal/session/manager_test.go b/internal/session/manager_test.go index 3b219fa..f73a57f 100644 --- a/internal/session/manager_test.go +++ b/internal/session/manager_test.go @@ -154,10 +154,8 @@ func TestManager_Auth(t *testing.T) { return } - // Validate that the data has been inserted into the repository assert.Equal(t, oidcMapping, tt.oidc.TGet(tt.tenantID), "Trust mapping has not been inserted") - // Check the returned URL u, err := url.Parse(got) require.NoError(t, err, "parsing location") @@ -178,13 +176,9 @@ func TestManager_Auth(t *testing.T) { assert.Equal(t, wantQ.Get(kRedirectURI), q.Get(kRedirectURI), "Unexpected redirect URI") assert.Equal(t, wantQ.Get(kParamAuth1), q.Get(kParamAuth1), "Unexpected auth url") - // Check the scopes on the URL string to ensure we don't have - // something like scope=openid&scope=profile... - // but rather scope=openid profile email groups scopeValues := url.Values{kScope: {"openid profile email groups"}} assert.Contains(t, got, scopeValues.Encode()) - // These values are generated randomly. So check if they aren't empty assert.NotEmpty(t, q.Get(kState), "State is zero") assert.NotEmpty(t, q.Get(kCodeChallenge), "Code challenge is zero") }) @@ -421,15 +415,16 @@ func TestManager_BCLogout(t *testing.T) { panic(err) } + const keyID = "kid1" jwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{ Key: key, - KeyID: "kid1", + KeyID: keyID, Algorithm: string(jose.RS256), }}} signer, err := jose.NewSigner(jose.SigningKey{ Algorithm: jose.RS256, Key: jwks.Keys[0], - }, nil) + }, (&jose.SignerOptions{}).WithType("JWT")) if err != nil { panic(err) } @@ -443,21 +438,29 @@ func TestManager_BCLogout(t *testing.T) { return token } - jwksSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - publicJwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{ - Key: &key.PublicKey, - KeyID: "kid1", - Algorithm: string(jose.RS256), - }}} - b, err := json.Marshal(publicJwks) - if err != nil { - panic(err) - } + publicJwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{ + Key: &key.PublicKey, + KeyID: keyID, + Algorithm: string(jose.RS256), + Use: "sig", + }}} - if _, err := w.Write(b); err != nil { - panic(err) + var jwksSrv *httptest.Server + jwksSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/.well-known/openid-configuration": + _ = json.NewEncoder(w).Encode(oidc.Configuration{ + Issuer: jwksSrv.URL, + JwksURI: jwksSrv.URL + "/jwks", + }) + case "/jwks": + _ = json.NewEncoder(w).Encode(publicJwks) + default: + http.NotFound(w, r) } })) + defer jwksSrv.Close() tests := []struct { name string @@ -469,19 +472,15 @@ func TestManager_BCLogout(t *testing.T) { { name: "Success", cfg: &config.SessionManager{}, - jwt: newJwt(struct { - Events map[string]struct{} `json:"events"` - SessionID string `json:"sid"` - KeyID string `json:"kid"` - }{ - Events: map[string]struct{}{"http://schemas.openid.net/event/backchannel-logout": {}}, - SessionID: "sid-1", - }), setupMock: func(oidcs *trustmock.Repository, sessions *sessionmock.Repository) { _ = oidcs.Create(context.Background(), "tid-1", trust.OIDCMapping{ IssuerURL: jwksSrv.URL, }) - _ = sessions.StoreSession(context.Background(), session.Session{ID: "sid-1", TenantID: "tid-1"}) + _ = sessions.StoreSession(context.Background(), session.Session{ + ID: "session-1", + ProviderID: "sid-1", + TenantID: "tid-1", + }) }, errAssert: assert.NoError, }, @@ -489,8 +488,6 @@ func TestManager_BCLogout(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := t.Context() - oidcServer := StartOIDCServer(t, false) - defer oidcServer.Close() auditServer := StartAuditServer(t) defer auditServer.Close() @@ -501,30 +498,29 @@ func TestManager_BCLogout(t *testing.T) { oidcMock := trustmock.NewInMemRepository() sessionMock := sessionmock.NewInMemRepository() - rt := localRoundTripper{ - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - b, err := json.Marshal(oidc.Configuration{ - JwksURI: jwksSrv.URL, - Issuer: jwksSrv.URL, - }) - if err != nil { - panic(err) - } + tt.setupMock(oidcMock, sessionMock) - if _, err := w.Write(b); err != nil { - panic(err) - } - }), + now := time.Now() + logoutJWT := newJwt(map[string]any{ + "iss": jwksSrv.URL, + "sub": "user-1", + "aud": []string{""}, + "exp": now.Add(time.Hour).Unix(), + "iat": now.Unix(), + "sid": "sid-1", + "events": map[string]any{"http://schemas.openid.net/event/backchannel-logout": map[string]any{}}, + }) + + if tt.jwt == "" { + tt.jwt = logoutJWT } - tt.setupMock(oidcMock, sessionMock) - m, err := session.NewManager( tt.cfg, oidcMock, sessionMock, auditLogger, - session.WithTransportCredentials(newTCBuilder(rt)), + session.WithAllowHttpScheme(true), ) require.NoError(t, err) @@ -532,6 +528,9 @@ func TestManager_BCLogout(t *testing.T) { if !tt.errAssert(t, err, fmt.Sprintf("Manager.BCLogout() error = %v", err)) { return } + + _, loadErr := sessionMock.LoadSession(ctx, "session-1") + assert.Error(t, loadErr, "session should have been deleted after BCLogout") }) } } @@ -603,6 +602,171 @@ func TestManager_LogoutEdgeCases(t *testing.T) { } } +func TestManager_Logout_RedirectURL(t *testing.T) { + const ( + tenantID = "tenant-id" + sessionID = "session-id" + ) + + newOIDCDiscoveryServer := func(endSessionEndpoint string) *httptest.Server { + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(oidc.Configuration{ + Issuer: srv.URL, + JwksURI: srv.URL + "/jwks", + EndSessionEndpoint: endSessionEndpoint, + }) + })) + return srv + } + + tests := []struct { + name string + endSessionURL string + postLogoutURL string + queryParamsLogout []string + mappingProps map[string]string + deleteSessionErr error + wantURL string + wantContains []string + errAssert assert.ErrorAssertionFunc + errContains string + }{ + { + name: "Success with end session endpoint and post logout redirect", + endSessionURL: "https://idp.example.com/logout", + postLogoutURL: "https://app.example.com/landing", + wantContains: []string{ + "https://idp.example.com/logout", + "client_id=" + testClientID, + "post_logout_redirect_uri=" + url.QueryEscape("https://app.example.com/landing"), + }, + errAssert: assert.NoError, + }, + { + name: "Success with end session endpoint without post logout redirect", + endSessionURL: "https://idp.example.com/logout", + postLogoutURL: "", + wantContains: []string{ + "https://idp.example.com/logout", + "client_id=" + testClientID, + }, + errAssert: assert.NoError, + }, + { + name: "Success with additional logout query parameters", + endSessionURL: "https://idp.example.com/logout", + postLogoutURL: "", + queryParamsLogout: []string{"logoutParam1"}, + mappingProps: map[string]string{"logoutParam1": "logoutValue1"}, + wantContains: []string{ + "client_id=" + testClientID, + "logoutParam1=logoutValue1", + }, + errAssert: assert.NoError, + }, + { + name: "No end session endpoint with post logout redirect URL", + endSessionURL: "", + postLogoutURL: "https://app.example.com/landing", + wantURL: "https://app.example.com/landing", + errAssert: assert.NoError, + }, + { + name: "No end session endpoint and no post logout redirect URL", + endSessionURL: "", + postLogoutURL: "", + errAssert: assert.Error, + errContains: "end_session_not_supported", + }, + { + name: "Missing logout query parameter in mapping properties", + endSessionURL: "https://idp.example.com/logout", + postLogoutURL: "", + queryParamsLogout: []string{"missingParam"}, + mappingProps: map[string]string{}, + errAssert: assert.Error, + errContains: "missing auth parameter: missingParam", + }, + { + name: "Delete session error", + endSessionURL: "https://idp.example.com/logout", + deleteSessionErr: errors.New("storage failure"), + errAssert: assert.Error, + errContains: "deleting session", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + oidcSrv := newOIDCDiscoveryServer(tt.endSessionURL) + defer oidcSrv.Close() + + auditServer := StartAuditServer(t) + defer auditServer.Close() + + auditLogger, err := otlpaudit.NewLogger(&commoncfg.Audit{Endpoint: auditServer.URL}) + require.NoError(t, err) + + props := tt.mappingProps + if props == nil { + props = map[string]string{} + } + + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust(tenantID, trust.OIDCMapping{ + IssuerURL: oidcSrv.URL, + Properties: props, + })) + + var sessionOpts []sessionmock.RepositoryOption + sessionOpts = append(sessionOpts, sessionmock.WithSession(session.Session{ + ID: sessionID, + TenantID: tenantID, + })) + if tt.deleteSessionErr != nil { + sessionOpts = append(sessionOpts, sessionmock.WithDeleteSessionError(tt.deleteSessionErr)) + } + sessMock := sessionmock.NewInMemRepository(sessionOpts...) + + cfg := &config.SessionManager{ + CSRFSecretParsed: []byte(testCSRFSecret), + PostLogoutRedirectURL: tt.postLogoutURL, + AdditionalQueryParametersLogout: tt.queryParamsLogout, + ClientAuth: config.ClientAuth{ + ClientID: testClientID, + }, + } + + m, err := session.NewManager(cfg, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + got, err := m.Logout(ctx, sessionID) + + if !tt.errAssert(t, err, fmt.Sprintf("Manager.Logout() error = %v", err)) { + return + } + + if err != nil { + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + return + } + + if tt.wantURL != "" { + assert.Equal(t, tt.wantURL, got) + } + + for _, substr := range tt.wantContains { + assert.Contains(t, got, substr) + } + }) + } +} + func TestManager_BCLogout_ErrorCases(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) @@ -747,8 +911,398 @@ func TestManager_NewManager_Error(t *testing.T) { assert.Contains(t, err.Error(), "parsing callback URL") } -// localRoundTripper is an http.RoundTripper that executes HTTP transactions by -// using handler directly, instead of going over an HTTP connection. +func TestManager_LoadState(t *testing.T) { + const stateID = "test-state-id" + + state := session.State{ + ID: stateID, + TenantID: "tenant-id", + Expiry: time.Now().Add(time.Hour), + } + + t.Run("Success", func(t *testing.T) { + repo := sessionmock.NewInMemRepository(sessionmock.WithState(state)) + m, err := session.NewManager(&config.SessionManager{CSRFSecretParsed: []byte(testCSRFSecret)}, nil, repo, nil) + require.NoError(t, err) + + got, err := m.LoadState(t.Context(), stateID) + require.NoError(t, err) + assert.Equal(t, stateID, got.ID) + }) + + t.Run("Not found", func(t *testing.T) { + repo := sessionmock.NewInMemRepository() + m, err := session.NewManager(&config.SessionManager{CSRFSecretParsed: []byte(testCSRFSecret)}, nil, repo, nil) + require.NoError(t, err) + + _, err = m.LoadState(t.Context(), "non-existent") + assert.Error(t, err) + }) +} + +func TestManager_FinaliseOIDCLogin_StoreSessionError(t *testing.T) { + const ( + callbackURL = "http://sm.example.com/sm/callback" + tenantID = "tenant-id" + stateID = "test-state-id" + code = "auth-code" + fingerprint = "test-fingerprint" + pkceVerifier = "test-verifier" + ) + + validState := session.State{ + ID: stateID, + TenantID: tenantID, + Fingerprint: fingerprint, + PKCEVerifier: pkceVerifier, + RequestURI: "http://app.example.com/ui", + Expiry: time.Now().Add(time.Hour), + } + + oidcServer := StartOIDCServer(t, false) + defer oidcServer.Close() + + auditServer := StartAuditServer(t) + defer auditServer.Close() + + auditLogger, err := otlpaudit.NewLogger(&commoncfg.Audit{Endpoint: auditServer.URL}) + require.NoError(t, err) + + jwksURI, err := url.JoinPath(oidcServer.URL, "/.well-known/jwks.json") + require.NoError(t, err) + + mapping := trust.OIDCMapping{ + IssuerURL: oidcServer.URL, + JWKSURI: jwksURI, + Properties: map[string]string{}, + } + + cfg := &config.SessionManager{ + SessionDuration: time.Hour, + CallbackURL: callbackURL, + CSRFSecretParsed: []byte(testCSRFSecret), + } + + t.Run("Store session error", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust(tenantID, mapping)) + sessMock := sessionmock.NewInMemRepository( + sessionmock.WithState(validState), + sessionmock.WithStoreSessionError(errors.New("store failed")), + ) + + m, err := session.NewManager(cfg, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + _, err = m.FinaliseOIDCLogin(context.Background(), stateID, code, fingerprint) + assert.Error(t, err) + assert.Contains(t, err.Error(), "storing session") + }) + + t.Run("Bump active error", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust(tenantID, mapping)) + sessMock := sessionmock.NewInMemRepository( + sessionmock.WithState(validState), + sessionmock.WithBumpActiveError(errors.New("bump failed")), + ) + + m, err := session.NewManager(cfg, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + _, err = m.FinaliseOIDCLogin(context.Background(), stateID, code, fingerprint) + assert.Error(t, err) + assert.Contains(t, err.Error(), "bumping session active status") + }) + + t.Run("Delete state error", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust(tenantID, mapping)) + sessMock := sessionmock.NewInMemRepository( + sessionmock.WithState(validState), + sessionmock.WithDeleteStateError(errors.New("delete state failed")), + ) + + m, err := session.NewManager(cfg, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + _, err = m.FinaliseOIDCLogin(context.Background(), stateID, code, fingerprint) + assert.Error(t, err) + assert.Contains(t, err.Error(), "deleting state") + }) + + t.Run("Missing auth context parameter", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust(tenantID, mapping)) + sessMock := sessionmock.NewInMemRepository(sessionmock.WithState(validState)) + + cfgWithAuthCtx := &config.SessionManager{ + SessionDuration: time.Hour, + CallbackURL: callbackURL, + CSRFSecretParsed: []byte(testCSRFSecret), + AdditionalAuthContextKeys: []string{"nonExistentKey"}, + } + + m, err := session.NewManager(cfgWithAuthCtx, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + _, err = m.FinaliseOIDCLogin(context.Background(), stateID, code, fingerprint) + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing auth context parameter") + }) +} + +func TestManager_BCLogout_TrustAndVerifyErrors(t *testing.T) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + const keyID = "kid1" + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.RS256, + Key: jose.JSONWebKey{ + Key: key, + KeyID: keyID, + Algorithm: string(jose.RS256), + }, + }, (&jose.SignerOptions{}).WithType("JWT")) + require.NoError(t, err) + + newJwt := func(claims any) string { + token, err := jwt.Signed(signer).Claims(claims).Serialize() + require.NoError(t, err) + return token + } + + publicJwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{ + Key: &key.PublicKey, + KeyID: keyID, + Algorithm: string(jose.RS256), + Use: "sig", + }}} + + var jwksSrv *httptest.Server + jwksSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/.well-known/openid-configuration": + _ = json.NewEncoder(w).Encode(oidc.Configuration{ + Issuer: jwksSrv.URL, + JwksURI: jwksSrv.URL + "/jwks", + }) + case "/jwks": + _ = json.NewEncoder(w).Encode(publicJwks) + default: + http.NotFound(w, r) + } + })) + defer jwksSrv.Close() + + now := time.Now() + validLogoutClaims := map[string]any{ + "iss": jwksSrv.URL, + "sub": "user-1", + "aud": []string{""}, + "exp": now.Add(time.Hour).Unix(), + "iat": now.Unix(), + "sid": "sid-1", + "events": map[string]any{"http://schemas.openid.net/event/backchannel-logout": map[string]any{}}, + } + + auditServer := StartAuditServer(t) + defer auditServer.Close() + + auditLogger, err := otlpaudit.NewLogger(&commoncfg.Audit{Endpoint: auditServer.URL}) + require.NoError(t, err) + + t.Run("Trust mapping get error", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository(trustmock.WithGetError(errors.New("trust error"))) + sessMock := sessionmock.NewInMemRepository() + _ = sessMock.StoreSession(context.Background(), session.Session{ + ID: "s1", ProviderID: "sid-1", TenantID: "tid-1", + }) + + m, err := session.NewManager(&config.SessionManager{}, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + err = m.BCLogout(t.Context(), newJwt(validLogoutClaims)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "getting trust mapping") + }) + + t.Run("Verify logout token error - wrong issuer", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository() + _ = oidcMock.Create(context.Background(), "tid-1", trust.OIDCMapping{IssuerURL: jwksSrv.URL}) + sessMock := sessionmock.NewInMemRepository() + _ = sessMock.StoreSession(context.Background(), session.Session{ + ID: "s1", ProviderID: "sid-1", TenantID: "tid-1", + }) + + wrongIssClaims := map[string]any{ + "iss": "https://wrong-issuer.example.com", + "sub": "user-1", + "aud": []string{""}, + "exp": now.Add(time.Hour).Unix(), + "iat": now.Unix(), + "sid": "sid-1", + "events": map[string]any{"http://schemas.openid.net/event/backchannel-logout": map[string]any{}}, + } + + m, err := session.NewManager(&config.SessionManager{}, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + err = m.BCLogout(t.Context(), newJwt(wrongIssClaims)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "verifying logout token") + }) + + t.Run("Delete session error after verify", func(t *testing.T) { + oidcMock := trustmock.NewInMemRepository() + _ = oidcMock.Create(context.Background(), "tid-1", trust.OIDCMapping{IssuerURL: jwksSrv.URL}) + sessMock := sessionmock.NewInMemRepository( + sessionmock.WithDeleteSessionError(errors.New("delete failed")), + ) + _ = sessMock.StoreSession(context.Background(), session.Session{ + ID: "s1", ProviderID: "sid-1", TenantID: "tid-1", + }) + + m, err := session.NewManager(&config.SessionManager{}, oidcMock, sessMock, auditLogger, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + err = m.BCLogout(t.Context(), newJwt(validLogoutClaims)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "deleting session") + }) +} + +func TestManager_MakeAuthURI_MissingAuthParameter(t *testing.T) { + oidcServer := StartOIDCServer(t, false) + defer oidcServer.Close() + + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust("tid", trust.OIDCMapping{ + IssuerURL: oidcServer.URL, + Properties: map[string]string{}, + })) + + cfg := &config.SessionManager{ + SessionDuration: time.Hour, + CallbackURL: "http://localhost/callback", + CSRFSecretParsed: []byte(testCSRFSecret), + AdditionalQueryParametersAuthorize: []string{"missingParam"}, + } + + m, err := session.NewManager(cfg, oidcMock, sessionmock.NewInMemRepository(), nil, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + _, _, err = m.MakeAuthURI(t.Context(), "tid", "fp", "http://app/ui") + assert.Error(t, err) + assert.Contains(t, err.Error(), "missing auth parameter") +} + +func TestManager_GetClientID_FromMapping(t *testing.T) { + oidcServer := StartOIDCServer(t, false) + defer oidcServer.Close() + + mappingClientID := "mapping-specific-client-id" + oidcMock := trustmock.NewInMemRepository(trustmock.WithTrust("tid", trust.OIDCMapping{ + IssuerURL: oidcServer.URL, + ClientID: mappingClientID, + Properties: map[string]string{}, + })) + + cfg := &config.SessionManager{ + SessionDuration: time.Hour, + CallbackURL: "http://localhost/callback", + CSRFSecretParsed: []byte(testCSRFSecret), + ClientAuth: config.ClientAuth{ClientID: "global-client-id"}, + } + + m, err := session.NewManager(cfg, oidcMock, sessionmock.NewInMemRepository(), nil, session.WithAllowHttpScheme(true)) + require.NoError(t, err) + + got, _, err := m.MakeAuthURI(t.Context(), "tid", "fp", "http://app/ui") + require.NoError(t, err) + + u, err := url.Parse(got) + require.NoError(t, err) + assert.Equal(t, mappingClientID, u.Query().Get("client_id")) +} + +func TestAppIDTokenClaims_UnmarshalJSON_Error(t *testing.T) { + var claims session.AppIDTokenClaims + err := claims.UnmarshalJSON([]byte(`{invalid json`)) + assert.Error(t, err) +} + +func TestAppIDTokenClaims_UnmarshalJSON_NoCustomClaims(t *testing.T) { + payload := `{"iss":"https://example.com","sub":"user1","aud":["client"],"exp":9999999999,"iat":1700000000}` + var claims session.AppIDTokenClaims + err := claims.UnmarshalJSON([]byte(payload)) + require.NoError(t, err) + assert.Empty(t, claims.UserUUID) + assert.Empty(t, claims.Groups) +} + +func TestManager_FinaliseOIDCLogin_NilAuditLogger(t *testing.T) { + const ( + stateID = "test-state-id" + tenantID = "tenant-id" + fingerprint = "test-fingerprint" + ) + + expiredState := session.State{ + ID: stateID, + TenantID: tenantID, + Fingerprint: fingerprint, + Expiry: time.Now().Add(-time.Hour), + } + + sessMock := sessionmock.NewInMemRepository(sessionmock.WithState(expiredState)) + oidcMock := trustmock.NewInMemRepository() + + cfg := &config.SessionManager{ + CSRFSecretParsed: []byte(testCSRFSecret), + } + + m, err := session.NewManager(cfg, oidcMock, sessMock, nil) + require.NoError(t, err) + + _, err = m.FinaliseOIDCLogin(context.Background(), stateID, "code", fingerprint) + assert.Error(t, err) +} + +func TestManager_FinaliseOIDCLogin_AuditSendSuccess(t *testing.T) { + const ( + stateID = "test-state-id" + tenantID = "tenant-id" + fingerprint = "test-fingerprint" + pkceVerifier = "test-verifier" + callbackURL = "http://sm.example.com/sm/callback" + ) + + validState := session.State{ + ID: stateID, + TenantID: tenantID, + Fingerprint: "wrong-fingerprint", + PKCEVerifier: pkceVerifier, + Expiry: time.Now().Add(time.Hour), + } + + auditServer := StartAuditServer(t) + defer auditServer.Close() + + auditLogger, err := otlpaudit.NewLogger(&commoncfg.Audit{Endpoint: auditServer.URL}) + require.NoError(t, err) + + sessMock := sessionmock.NewInMemRepository(sessionmock.WithState(validState)) + oidcMock := trustmock.NewInMemRepository() + + cfg := &config.SessionManager{ + CSRFSecretParsed: []byte(testCSRFSecret), + } + + m, err := session.NewManager(cfg, oidcMock, sessMock, auditLogger) + require.NoError(t, err) + + _, err = m.FinaliseOIDCLogin(context.Background(), stateID, "code", fingerprint) + assert.Error(t, err) +} + type localRoundTripper struct { handler http.Handler } diff --git a/internal/session/openid.go b/internal/session/openid.go index 32b0fbc..4c6029e 100644 --- a/internal/session/openid.go +++ b/internal/session/openid.go @@ -4,36 +4,48 @@ import ( "context" "crypto/sha256" "encoding/base64" + "errors" + "fmt" + "net/http" + "net/url" + "time" - "github.com/openkcm/common-sdk/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/client" + "github.com/zitadel/oidc/v3/pkg/oidc" ) -func (m *Manager) getOpenIDConfig(ctx context.Context, issuerURL string) (*oidc.Configuration, error) { +// getOpenIDConfig fetches and caches the OIDC discovery configuration for the given issuer. +func (m *Manager) getOpenIDConfig(ctx context.Context, issuerURL string) (*oidc.DiscoveryConfiguration, error) { const wkocPrefix = "wkoc_" - // first check the cache for a recent WKOC configuration for this issuer + issuer, err := url.Parse(issuerURL) + if err != nil { + return nil, fmt.Errorf("parsing issuer url: %w", err) + } + if issuer.Scheme == "http" && !m.allowHttpScheme { + return nil, errors.New("insecure http issuer url is not allowed") + } + hashedSuffix := sha256.Sum256([]byte(issuerURL)) cacheKey := wkocPrefix + base64.RawURLEncoding.EncodeToString(hashedSuffix[:]) - cache, ok := m.cache.Get(cacheKey) + cached, ok := m.cache.Get(cacheKey) if ok { - value, ok := cache.(*oidc.Configuration) + value, ok := cached.(*oidc.DiscoveryConfiguration) if ok { return value, nil } m.cache.Delete(cacheKey) } - // otherwise, fetch the configuration and cache it - provider, err := oidc.NewProvider(issuerURL, []string{}, - oidc.WithAllowHttpScheme(m.allowHttpScheme), - ) - if err != nil { - return nil, err + httpClient := &http.Client{ + Transport: m.newCreds("").Transport(), + Timeout: 30 * time.Second, } - cfg, err := provider.GetConfiguration(ctx) + + cfg, err := client.Discover(ctx, issuerURL, httpClient) if err != nil { - return nil, err + return nil, fmt.Errorf("discovering openid configuration: %w", err) } m.cache.Set(cacheKey, cfg, 0) From af6f24e1a553c18149f33ce40865d5d196de9c36 Mon Sep 17 00:00:00 2001 From: Aaishwarya Thoke Date: Fri, 27 Mar 2026 15:03:25 +0100 Subject: [PATCH 2/2] code cleanup --- internal/session/manager_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/session/manager_test.go b/internal/session/manager_test.go index f73a57f..779a6ae 100644 --- a/internal/session/manager_test.go +++ b/internal/session/manager_test.go @@ -1268,18 +1268,16 @@ func TestManager_FinaliseOIDCLogin_NilAuditLogger(t *testing.T) { func TestManager_FinaliseOIDCLogin_AuditSendSuccess(t *testing.T) { const ( - stateID = "test-state-id" - tenantID = "tenant-id" - fingerprint = "test-fingerprint" - pkceVerifier = "test-verifier" - callbackURL = "http://sm.example.com/sm/callback" + stateID = "test-state-id" + tenantID = "tenant-id" + fingerprint = "test-fingerprint" ) validState := session.State{ ID: stateID, TenantID: tenantID, Fingerprint: "wrong-fingerprint", - PKCEVerifier: pkceVerifier, + PKCEVerifier: "test-verifier", Expiry: time.Now().Add(time.Hour), }