diff --git a/README.md b/README.md index d0de784e58..949aa76f91 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +**Note:** This repository is modified from oauth2-proxy for applications that support proxy wasm. For more details, refer to [Higress OIDC Wasm Plugin](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/oidc). + ![OAuth2 Proxy](docs/static/img/logos/OAuth2_Proxy_horizontal.svg) [![Continuous Integration](https://github.com/oauth2-proxy/oauth2-proxy/actions/workflows/ci.yaml/badge.svg)](https://github.com/oauth2-proxy/oauth2-proxy/actions/workflows/ci.yaml) diff --git a/go.mod b/go.mod index 60503a0b7b..4bbeb51a2c 100644 --- a/go.mod +++ b/go.mod @@ -1,97 +1,39 @@ -module github.com/oauth2-proxy/oauth2-proxy/v7 +module github.com/higress-group/oauth2-proxy -go 1.22.0 +go 1.19 require ( - cloud.google.com/go/compute/metadata v0.3.0 - github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb - github.com/a8m/envsubst v1.4.2 - github.com/alicebob/miniredis/v2 v2.33.0 + github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240531060402-2807ddfbb79e github.com/benbjohnson/clock v1.3.5 github.com/bitly/go-simplejson v0.5.1 - github.com/bsm/redislock v0.9.4 - github.com/coreos/go-oidc/v3 v3.10.0 - github.com/fsnotify/fsnotify v1.7.0 - github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 - github.com/go-jose/go-jose/v3 v3.0.3 - github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-cmp v0.6.0 + github.com/go-jose/go-jose/v4 v4.0.1 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 + github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f github.com/justinas/alice v1.2.0 - github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa - github.com/mitchellh/mapstructure v1.5.0 - github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/oauth2-proxy/tools/reference-gen v0.0.0-20220223111546-d3b50d1a591a github.com/ohler55/ojg v1.22.0 - github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.33.1 - github.com/pierrec/lz4/v4 v4.1.21 - github.com/prometheus/client_golang v1.19.1 - github.com/redis/go-redis/v9 v9.5.3 github.com/spf13/cast v1.6.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.19.0 - github.com/stretchr/testify v1.9.0 - github.com/vmihailenco/msgpack/v5 v5.4.1 - golang.org/x/crypto v0.24.0 - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 - golang.org/x/net v0.26.0 - golang.org/x/oauth2 v0.21.0 - golang.org/x/sync v0.7.0 - google.golang.org/api v0.183.0 - gopkg.in/natefinch/lumberjack.v2 v2.2.1 - k8s.io/apimachinery v0.30.1 + github.com/tidwall/gjson v1.17.1 + github.com/wasilibs/go-re2 v1.6.0 + golang.org/x/crypto v0.23.0 + golang.org/x/oauth2 v0.20.0 ) require ( - cloud.google.com/go/auth v0.5.1 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect - github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.0.2 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.4 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/nxadm/tail v1.4.11 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/go-logr/logr v0.2.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect + github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.54.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect - github.com/sagikazarmark/locafero v0.6.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/yuin/gopher-lua v1.1.1 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect - go.opentelemetry.io/otel v1.27.0 // indirect - go.opentelemetry.io/otel/metric v1.27.0 // indirect - go.opentelemetry.io/otel/trace v1.27.0 // indirect - go.uber.org/multierr v1.11.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/tetratelabs/wazero v1.7.2 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/resp v0.1.1 // indirect golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect - google.golang.org/grpc v1.64.0 // indirect - google.golang.org/protobuf v1.34.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/gengo v0.0.0-20240404160639-a0386bf69313 // indirect - k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/gengo v0.0.0-20201113003025-83324d819ded // indirect + k8s.io/klog/v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 2caee04f99..ab01f88826 100644 --- a/go.sum +++ b/go.sum @@ -1,401 +1,107 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= -cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= -cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= -cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw= -github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= -github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= -github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= -github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= -github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= -github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE= -github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= -github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= +github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240531060402-2807ddfbb79e h1:dSKhz60LkydiLqZDVeCSqZCXA78pWHqNTU6OM7MTL/0= +github.com/alibaba/higress/plugins/wasm-go v1.3.6-0.20240531060402-2807ddfbb79e/go.mod h1:10jQXKsYFUF7djs+Oy7t82f4dbie9pISfP9FJwpPLuk= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/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/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw= -github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= -github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= -github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -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-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-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3 h1:6amM4HsNPOvMLVc2ZnyqrjeQ92YAVWn7T4WBKK87inY= -github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= -github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg= +github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo= github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo= github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvBn0G0a7QBRdBUp6Y048BtLAHRTKPo= -github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= -github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= -github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk= -github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw= +github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a h1:tdPcGgyiH0K+SbsJBBm2oPyEIOTAvLBwD9TuUwVtZho= +github.com/magefile/mage v1.15.1-0.20230912152418-9f54e0f83e2a/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/oauth2-proxy/tools/reference-gen v0.0.0-20220223111546-d3b50d1a591a h1:2RkJiJXdto2/qHaM7mTUKSR8yxImz0zei8LW0bcbav0= github.com/oauth2-proxy/tools/reference-gen v0.0.0-20220223111546-d3b50d1a591a/go.mod h1:J9TATNVXZX2MAsXx9J35weO47Fp3FQtx+f48AHLFAug= github.com/ohler55/ojg v1.22.0 h1:McZObj3cD/Zz/ojzk5Pi5VvgQcagxmT1bVKNzhE5ihI= github.com/ohler55/ojg v1.22.0/go.mod h1:gQhDVpQLqrmnd2eqGAvJtn+NfKoYJbe/A4Sj3/Vro4o= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= -github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= +github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= -github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= -github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc= +github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= +github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= +github.com/wasilibs/go-re2 v1.6.0 h1:CLlhDebt38wtl/zz4ww+hkXBMcxjrKFvTDXzFW2VOz8= +github.com/wasilibs/go-re2 v1.6.0/go.mod h1:prArCyErsypRBI/jFAFJEbzyHzjABKqkzlidF0SNA04= +github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= -github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -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/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/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= -go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vvT1btiBxyq+s0orYBqcQY= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE= -google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1:Zz7rLWqp0ApfsR/l7+zSHhY3PMiH2xqgxlfYfAfNpoU= -google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/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= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= -k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/gengo v0.0.0-20240404160639-a0386bf69313 h1:wBIDZID8ju9pwOiLlV22YYKjFGtiNSWgHf5CnKLRUuM= -k8s.io/gengo v0.0.0-20240404160639-a0386bf69313/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE= +k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/load.go b/load.go new file mode 100644 index 0000000000..818398fa35 --- /dev/null +++ b/load.go @@ -0,0 +1,77 @@ +package oidc + +import ( + "fmt" + + "github.com/higress-group/oauth2-proxy/pkg/apis/options" + "github.com/higress-group/oauth2-proxy/pkg/mapstructure" + "github.com/higress-group/oauth2-proxy/pkg/validation" + + "github.com/tidwall/gjson" +) + +func LoadOptions(json gjson.Result) (*options.Options, error) { + input := gjsonToResultMap(json) + opts, err := loadLegacyOptions(input) + if err = validation.Validate(opts); err != nil { + return opts, err + } + return opts, err +} + +// loadLegacyOptions loads the old toml options using the legacy flag set +// and legacy options struct. +func loadLegacyOptions(input map[string]interface{}) (*options.Options, error) { + + legacyOpts := options.NewLegacyOptions() + + err := mapstructure.Decode(input, &legacyOpts) + if err != nil { + return nil, fmt.Errorf("failed to decode input: %v", err) + } + + opts, err := legacyOpts.ToOptions() + if err != nil { + return nil, fmt.Errorf("failed to convert config: %v", err) + } + + return opts, nil +} + +func gjsonToResultMap(result gjson.Result) map[string]interface{} { + // 我们需要一个 map 来对应 JSON 对象 + resultMap := make(map[string]interface{}) + + // 遍历 JSON 对象的每个成员 + result.ForEach(func(key, value gjson.Result) bool { + resultMap[key.String()] = gjsonToInterface(value) + return true // 继续遍历 + }) + + return resultMap +} + +// gjsonToInterface 将 gjson.Result 转换为 interface{} +func gjsonToInterface(result gjson.Result) interface{} { + switch { + case result.IsArray(): + // Result 是一个数组,转换每个元素 + values := result.Array() + array := make([]interface{}, len(values)) + for i, value := range values { + array[i] = gjsonToInterface(value) + } + return array + case result.IsObject(): + // Result 是一个对象,转换每个成员 + objMap := make(map[string]interface{}) + result.ForEach(func(key, value gjson.Result) bool { + objMap[key.String()] = gjsonToInterface(value) + return true // 继续遍历 + }) + return objMap + default: + // 为空或为其他复杂类型 + return result.Value() + } +} diff --git a/main.go b/main.go deleted file mode 100644 index 5694e94e33..0000000000 --- a/main.go +++ /dev/null @@ -1,152 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime" - - "github.com/ghodss/yaml" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" - "github.com/spf13/pflag" -) - -func main() { - logger.SetFlags(logger.Lshortfile) - - configFlagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ContinueOnError) - - // Because we parse early to determine alpha vs legacy config, we have to - // ignore any unknown flags for now - configFlagSet.ParseErrorsWhitelist.UnknownFlags = true - - config := configFlagSet.String("config", "", "path to config file") - alphaConfig := configFlagSet.String("alpha-config", "", "path to alpha config file (use at your own risk - the structure in this config file may change between minor releases)") - convertConfig := configFlagSet.Bool("convert-config-to-alpha", false, "if true, the proxy will load configuration as normal and convert existing configuration to the alpha config structure, and print it to stdout") - showVersion := configFlagSet.Bool("version", false, "print version string") - configFlagSet.Parse(os.Args[1:]) - - if *showVersion { - fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version()) - return - } - - if *convertConfig && *alphaConfig != "" { - logger.Fatal("cannot use alpha-config and convert-config-to-alpha together") - } - - opts, err := loadConfiguration(*config, *alphaConfig, configFlagSet, os.Args[1:]) - if err != nil { - logger.Fatalf("ERROR: %v", err) - } - - if *convertConfig { - if err := printConvertedConfig(opts); err != nil { - logger.Fatalf("ERROR: could not convert config: %v", err) - } - return - } - - if err = validation.Validate(opts); err != nil { - logger.Fatalf("%s", err) - } - - validator := NewValidator(opts.EmailDomains, opts.AuthenticatedEmailsFile) - oauthproxy, err := NewOAuthProxy(opts, validator) - if err != nil { - logger.Fatalf("ERROR: Failed to initialise OAuth2 Proxy: %v", err) - } - - if err := oauthproxy.Start(); err != nil { - logger.Fatalf("ERROR: Failed to start OAuth2 Proxy: %v", err) - } -} - -// loadConfiguration will load in the user's configuration. -// It will either load the alpha configuration (if alphaConfig is given) -// or the legacy configuration. -func loadConfiguration(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { - if alphaConfig != "" { - logger.Printf("WARNING: You are using alpha configuration. The structure in this configuration file may change without notice. You MUST remove conflicting options from your existing configuration.") - return loadAlphaOptions(config, alphaConfig, extraFlags, args) - } - return loadLegacyOptions(config, extraFlags, args) -} - -// loadLegacyOptions loads the old toml options using the legacy flagset -// and legacy options struct. -func loadLegacyOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { - optionsFlagSet := options.NewLegacyFlagSet() - optionsFlagSet.AddFlagSet(extraFlags) - if err := optionsFlagSet.Parse(args); err != nil { - return nil, fmt.Errorf("failed to parse flags: %v", err) - } - - legacyOpts := options.NewLegacyOptions() - if err := options.Load(config, optionsFlagSet, legacyOpts); err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } - - opts, err := legacyOpts.ToOptions() - if err != nil { - return nil, fmt.Errorf("failed to convert config: %v", err) - } - - return opts, nil -} - -// loadAlphaOptions loads the old style config excluding options converted to -// the new alpha format, then merges the alpha options, loaded from YAML, -// into the core configuration. -func loadAlphaOptions(config, alphaConfig string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { - opts, err := loadOptions(config, extraFlags, args) - if err != nil { - return nil, fmt.Errorf("failed to load core options: %v", err) - } - - alphaOpts := &options.AlphaOptions{} - if err := options.LoadYAML(alphaConfig, alphaOpts); err != nil { - return nil, fmt.Errorf("failed to load alpha options: %v", err) - } - - alphaOpts.MergeInto(opts) - return opts, nil -} - -// loadOptions loads the configuration using the old style format into the -// core options.Options struct. -// This means that none of the options that have been converted to alpha config -// will be loaded using this method. -func loadOptions(config string, extraFlags *pflag.FlagSet, args []string) (*options.Options, error) { - optionsFlagSet := options.NewFlagSet() - optionsFlagSet.AddFlagSet(extraFlags) - if err := optionsFlagSet.Parse(args); err != nil { - return nil, fmt.Errorf("failed to parse flags: %v", err) - } - - opts := options.NewOptions() - if err := options.Load(config, optionsFlagSet, opts); err != nil { - return nil, fmt.Errorf("failed to load config: %v", err) - } - - return opts, nil -} - -// printConvertedConfig extracts alpha options from the loaded configuration -// and renders these to stdout in YAML format. -func printConvertedConfig(opts *options.Options) error { - alphaConfig := &options.AlphaOptions{} - alphaConfig.ExtractFrom(opts) - - data, err := yaml.Marshal(alphaConfig) - if err != nil { - return fmt.Errorf("unable to marshal config: %v", err) - } - - if _, err := os.Stdout.Write(data); err != nil { - return fmt.Errorf("unable to write output: %v", err) - } - - return nil -} diff --git a/main_suite_test.go b/main_suite_test.go deleted file mode 100644 index 1fafda3eae..0000000000 --- a/main_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "testing" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestMainSuite(t *testing.T) { - logger.SetOutput(GinkgoWriter) - logger.SetErrOutput(GinkgoWriter) - - RegisterFailHandler(Fail) - RunSpecs(t, "Main Suite") -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index fdbd6ebbbc..0000000000 --- a/main_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "os" - "strings" - "time" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" - . "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/testutil" - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/format" - "github.com/spf13/pflag" -) - -var _ = Describe("Configuration Loading Suite", func() { - // For comparing the full configuration differences of our structs we need to increase the gomega limits - format.MaxLength = 50000 - format.MaxDepth = 10 - - const testLegacyConfig = ` -http_address="127.0.0.1:4180" -upstreams="http://httpbin" -set_basic_auth="true" -basic_auth_password="super-secret-password" -client_id="oauth2-proxy" -client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" -` - - const testAlphaConfig = ` -upstreamConfig: - proxyrawpath: false - upstreams: - - id: / - path: / - uri: http://httpbin - flushInterval: 1s - passHostHeader: true - proxyWebSockets: true - timeout: 30s -injectRequestHeaders: -- name: Authorization - values: - - claim: user - prefix: "Basic " - basicAuthPassword: - value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk -- name: X-Forwarded-Groups - values: - - claim: groups -- name: X-Forwarded-User - values: - - claim: user -- name: X-Forwarded-Email - values: - - claim: email -- name: X-Forwarded-Preferred-Username - values: - - claim: preferred_username -injectResponseHeaders: -- name: Authorization - values: - - claim: user - prefix: "Basic " - basicAuthPassword: - value: c3VwZXItc2VjcmV0LXBhc3N3b3Jk -server: - bindAddress: "127.0.0.1:4180" -providers: -- provider: google - ID: google=oauth2-proxy - clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK - clientID: oauth2-proxy - azureConfig: - tenant: common - oidcConfig: - groupsClaim: groups - emailClaim: email - userIDClaim: email - insecureSkipNonce: true - audienceClaims: [aud] - extraAudiences: [] - loginURLParameters: - - name: approval_prompt - default: - - force -` - - const testCoreConfig = ` -cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" -email_domains="example.com" -cookie_secure="false" - -redirect_url="http://localhost:4180/oauth2/callback" -` - - boolPtr := func(b bool) *bool { - return &b - } - - durationPtr := func(d time.Duration) *options.Duration { - du := options.Duration(d) - return &du - } - - testExpectedOptions := func() *options.Options { - opts, err := options.NewLegacyOptions().ToOptions() - Expect(err).ToNot(HaveOccurred()) - - opts.Cookie.Secret = "OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" - opts.EmailDomains = []string{"example.com"} - opts.Cookie.Secure = false - opts.RawRedirectURL = "http://localhost:4180/oauth2/callback" - - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: "/", - Path: "/", - URI: "http://httpbin", - FlushInterval: durationPtr(options.DefaultUpstreamFlushInterval), - PassHostHeader: boolPtr(true), - ProxyWebSockets: boolPtr(true), - Timeout: durationPtr(options.DefaultUpstreamTimeout), - }, - }, - } - - authHeader := options.Header{ - Name: "Authorization", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - Prefix: "Basic ", - BasicAuthPassword: &options.SecretSource{ - Value: []byte("super-secret-password"), - }, - }, - }, - }, - } - - opts.InjectRequestHeaders = append([]options.Header{authHeader}, opts.InjectRequestHeaders...) - opts.InjectResponseHeaders = append(opts.InjectResponseHeaders, authHeader) - - opts.Providers = options.Providers{ - options.Provider{ - ID: "google=oauth2-proxy", - Type: "google", - ClientSecret: "b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK", - ClientID: "oauth2-proxy", - AzureConfig: options.AzureOptions{ - Tenant: "common", - }, - OIDCConfig: options.OIDCOptions{ - GroupsClaim: "groups", - EmailClaim: "email", - UserIDClaim: "email", - AudienceClaims: []string{"aud"}, - ExtraAudiences: []string{}, - InsecureSkipNonce: true, - }, - LoginURLParameters: []options.LoginURLParameter{ - {Name: "approval_prompt", Default: []string{"force"}}, - }, - }, - } - return opts - } - - type loadConfigurationTableInput struct { - configContent string - alphaConfigContent string - args []string - extraFlags func() *pflag.FlagSet - expectedOptions func() *options.Options - expectedErr error - } - - DescribeTable("LoadConfiguration", - func(in loadConfigurationTableInput) { - var configFileName, alphaConfigFileName string - - defer func() { - if configFileName != "" { - Expect(os.Remove(configFileName)).To(Succeed()) - } - if alphaConfigFileName != "" { - Expect(os.Remove(alphaConfigFileName)).To(Succeed()) - } - }() - - if in.configContent != "" { - By("Writing the config to a temporary file", func() { - file, err := os.CreateTemp("", "oauth2-proxy-test-config-XXXX.cfg") - Expect(err).ToNot(HaveOccurred()) - defer file.Close() - - configFileName = file.Name() - - _, err = file.WriteString(in.configContent) - Expect(err).ToNot(HaveOccurred()) - }) - } - - if in.alphaConfigContent != "" { - By("Writing the config to a temporary file", func() { - file, err := os.CreateTemp("", "oauth2-proxy-test-alpha-config-XXXX.yaml") - Expect(err).ToNot(HaveOccurred()) - defer file.Close() - - alphaConfigFileName = file.Name() - - _, err = file.WriteString(in.alphaConfigContent) - Expect(err).ToNot(HaveOccurred()) - }) - } - - extraFlags := pflag.NewFlagSet("test-flagset", pflag.ExitOnError) - if in.extraFlags != nil { - extraFlags = in.extraFlags() - } - - opts, err := loadConfiguration(configFileName, alphaConfigFileName, extraFlags, in.args) - if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) - } else { - Expect(err).ToNot(HaveOccurred()) - } - Expect(in.expectedOptions).ToNot(BeNil()) - Expect(opts).To(EqualOpts(in.expectedOptions())) - }, - Entry("with legacy configuration", loadConfigurationTableInput{ - configContent: testCoreConfig + testLegacyConfig, - expectedOptions: testExpectedOptions, - }), - Entry("with alpha configuration", loadConfigurationTableInput{ - configContent: testCoreConfig, - alphaConfigContent: testAlphaConfig, - expectedOptions: testExpectedOptions, - }), - Entry("with bad legacy configuration", loadConfigurationTableInput{ - configContent: testCoreConfig + "unknown_field=\"something\"", - expectedOptions: func() *options.Options { return nil }, - expectedErr: errors.New("failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"), - }), - Entry("with bad alpha configuration", loadConfigurationTableInput{ - configContent: testCoreConfig, - alphaConfigContent: testAlphaConfig + ":", - expectedOptions: func() *options.Options { return nil }, - expectedErr: fmt.Errorf("failed to load alpha options: error unmarshalling config: error converting YAML to JSON: yaml: line %d: did not find expected key", strings.Count(testAlphaConfig, "\n")), - }), - Entry("with alpha configuration and bad core configuration", loadConfigurationTableInput{ - configContent: testCoreConfig + "unknown_field=\"something\"", - alphaConfigContent: testAlphaConfig, - expectedOptions: func() *options.Options { return nil }, - expectedErr: errors.New("failed to load core options: failed to load config: error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_field"), - }), - ) -}) diff --git a/oauthproxy.go b/oauthproxy.go index df646f7261..1411c24cf8 100644 --- a/oauthproxy.go +++ b/oauthproxy.go @@ -1,58 +1,42 @@ -package main +package oidc import ( - "context" - "embed" "encoding/base64" - "encoding/json" "errors" "fmt" - "net" "net/http" "net/url" - "os" - "os/signal" - "regexp" "strings" - "syscall" "time" + "github.com/higress-group/oauth2-proxy/pkg/apis/options" + sessionsapi "github.com/higress-group/oauth2-proxy/pkg/apis/sessions" + "github.com/higress-group/oauth2-proxy/pkg/app/redirect" + "github.com/higress-group/oauth2-proxy/pkg/cookies" + "github.com/higress-group/oauth2-proxy/pkg/encryption" + "github.com/higress-group/oauth2-proxy/pkg/middleware" + requestutil "github.com/higress-group/oauth2-proxy/pkg/requests/util" + "github.com/higress-group/oauth2-proxy/pkg/sessions" + "github.com/higress-group/oauth2-proxy/pkg/util" + "github.com/higress-group/oauth2-proxy/providers" + + middlewareapi "github.com/higress-group/oauth2-proxy/pkg/apis/middleware" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" "github.com/gorilla/mux" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" "github.com/justinas/alice" - ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip" - middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" - sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/redirect" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption" - proxyhttp "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/http" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware" - requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream" - "github.com/oauth2-proxy/oauth2-proxy/v7/providers" ) const ( + SetCookieHeader = "Set-Cookie" schemeHTTP = "http" schemeHTTPS = "https" applicationJSON = "application/json" - robotsPath = "/robots.txt" - signInPath = "/sign_in" - signOutPath = "/sign_out" oauthStartPath = "/start" oauthCallbackPath = "/callback" - authOnlyPath = "/auth" - userInfoPath = "/userinfo" - staticPathPrefix = "/static/" + signOutPath = "/sign_out" ) var ( @@ -61,149 +45,69 @@ var ( // ErrAccessDenied means the user should receive a 401 Unauthorized response ErrAccessDenied = errors.New("access denied") - - //go:embed static/* - staticFiles embed.FS ) -// allowedRoute manages method + path based allowlists -type allowedRoute struct { - method string - negate bool - pathRegex *regexp.Regexp -} - -type apiRoute struct { - pathRegex *regexp.Regexp -} - // OAuthProxy is the main authentication proxy type OAuthProxy struct { CookieOptions *options.Cookie - Validator func(string) bool - - SignInPath string - - allowedRoutes []allowedRoute - apiRoutes []apiRoute - redirectURL *url.URL // the url to receive requests at - relativeRedirectURL bool - whitelistDomains []string - provider providers.Provider - sessionStore sessionsapi.SessionStore - ProxyPrefix string - basicAuthValidator basic.Validator - basicAuthGroups []string - SkipProviderButton bool - skipAuthPreflight bool - skipJwtBearerTokens bool - forceJSONErrors bool - allowQuerySemicolons bool - realClientIPParser ipapi.RealClientIPParser - trustedIPs *ip.NetSet - - sessionChain alice.Chain - headersChain alice.Chain - preAuthChain alice.Chain - pageWriter pagewriter.Writer - server proxyhttp.Server - upstreamProxy http.Handler + validator func(string) bool + ctx wrapper.HttpContext + + redirectURL *url.URL // the url to receive requests at + relativeRedirectURL bool + whitelistDomains []string + provider providers.Provider + sessionStore sessionsapi.SessionStore + ProxyPrefix string + skipAuthPreflight bool + + sessionChain alice.Chain + preAuthChain alice.Chain + serveMux *mux.Router redirectValidator redirect.Validator appDirector redirect.AppDirector - encodeState bool + passAuthorization bool + encodeState bool + + client wrapper.HttpClient } // NewOAuthProxy creates a new instance of OAuthProxy from the options provided -func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthProxy, error) { +func NewOAuthProxy(opts *options.Options) (*OAuthProxy, error) { sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie) if err != nil { return nil, fmt.Errorf("error initialising session store: %v", err) } - var basicAuthValidator basic.Validator - if opts.HtpasswdFile != "" { - logger.Printf("using htpasswd file: %s", opts.HtpasswdFile) - var err error - basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile) - if err != nil { - return nil, fmt.Errorf("could not validate htpasswd: %v", err) - } - } - provider, err := providers.NewProvider(opts.Providers[0]) if err != nil { return nil, fmt.Errorf("error initialising provider: %v", err) } - pageWriter, err := pagewriter.NewWriter(pagewriter.Opts{ - TemplatesPath: opts.Templates.Path, - CustomLogo: opts.Templates.CustomLogo, - ProxyPrefix: opts.ProxyPrefix, - Footer: opts.Templates.Footer, - Version: VERSION, - Debug: opts.Templates.Debug, - ProviderName: buildProviderName(provider, opts.Providers[0].Name), - SignInMessage: buildSignInMessage(opts), - DisplayLoginForm: basicAuthValidator != nil && opts.Templates.DisplayLoginForm, - }) - if err != nil { - return nil, fmt.Errorf("error initialising page writer: %v", err) - } - - upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), pageWriter) - if err != nil { - return nil, fmt.Errorf("error initialising upstream proxy: %v", err) - } - - if opts.SkipJwtBearerTokens { - logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.Providers[0].OIDCConfig.IssuerURL) - for _, issuer := range opts.ExtraJwtIssuers { - logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer) - } - } redirectURL := opts.GetRedirectURL() if redirectURL.Path == "" { redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) } - logger.Printf("OAuthProxy configured for %s Client ID: %s", provider.Data().ProviderName, opts.Providers[0].ClientID) + util.Logger.Infof("OAuthProxy configured for %s Client ID: %s", provider.Data().ProviderName, opts.Providers[0].ClientID) refresh := "disabled" if opts.Cookie.Refresh != time.Duration(0) { refresh = fmt.Sprintf("after %s", opts.Cookie.Refresh) } + util.Logger.Infof("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh) - logger.Printf("Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s", opts.Cookie.Name, opts.Cookie.Secure, opts.Cookie.HTTPOnly, opts.Cookie.Expire, strings.Join(opts.Cookie.Domains, ","), opts.Cookie.Path, opts.Cookie.SameSite, refresh) - - trustedIPs := ip.NewNetSet() - for _, ipStr := range opts.TrustedIPs { - if ipNet := ip.ParseIPNet(ipStr); ipNet != nil { - trustedIPs.AddIPNet(*ipNet) - } else { - return nil, fmt.Errorf("could not parse IP network (%s)", ipStr) - } - } - - allowedRoutes, err := buildRoutesAllowlist(opts) + serviceClient, err := opts.Service.NewService() if err != nil { return nil, err } - apiRoutes, err := buildAPIRoutes(opts) - if err != nil { - return nil, err - } - - preAuthChain, err := buildPreAuthChain(opts, sessionStore) + preAuthChain, err := buildPreAuthChain(opts) if err != nil { return nil, fmt.Errorf("could not build pre-auth chain: %v", err) } - sessionChain := buildSessionChain(opts, provider, sessionStore, basicAuthValidator) - headersChain, err := buildHeadersChain(opts) - if err != nil { - return nil, fmt.Errorf("could not build headers chain: %v", err) - } + sessionChain := buildSessionChain(opts, provider, sessionStore, serviceClient) redirectValidator := redirect.NewValidator(opts.WhitelistDomains) appDirector := redirect.NewAppDirector(redirect.AppDirectorOpts{ @@ -211,576 +115,128 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr Validator: redirectValidator, }) + // TODO: Support Email Validation + validator := func(string) bool { return true } + p := &OAuthProxy{ CookieOptions: &opts.Cookie, - Validator: validator, - - SignInPath: fmt.Sprintf("%s/sign_in", opts.ProxyPrefix), - - ProxyPrefix: opts.ProxyPrefix, - provider: provider, - sessionStore: sessionStore, - redirectURL: redirectURL, - relativeRedirectURL: opts.RelativeRedirectURL, - apiRoutes: apiRoutes, - allowedRoutes: allowedRoutes, - whitelistDomains: opts.WhitelistDomains, - skipAuthPreflight: opts.SkipAuthPreflight, - skipJwtBearerTokens: opts.SkipJwtBearerTokens, - realClientIPParser: opts.GetRealClientIPParser(), - SkipProviderButton: opts.SkipProviderButton, - forceJSONErrors: opts.ForceJSONErrors, - allowQuerySemicolons: opts.AllowQuerySemicolons, - trustedIPs: trustedIPs, - - basicAuthValidator: basicAuthValidator, - basicAuthGroups: opts.HtpasswdUserGroups, - sessionChain: sessionChain, - headersChain: headersChain, - preAuthChain: preAuthChain, - pageWriter: pageWriter, - upstreamProxy: upstreamProxy, - redirectValidator: redirectValidator, - appDirector: appDirector, - encodeState: opts.EncodeState, - } - p.buildServeMux(opts.ProxyPrefix) - - if err := p.setupServer(opts); err != nil { - return nil, fmt.Errorf("error setting up server: %v", err) - } - - return p, nil -} - -func (p *OAuthProxy) Start() error { - if p.server == nil { - // We have to call setupServer before Start is called. - // If this doesn't happen it's a programming error. - panic("server has not been initialised") - } + validator: validator, - ctx, cancel := context.WithCancel(context.Background()) - - // Observe signals in background goroutine. - go func() { - sigint := make(chan os.Signal, 1) - signal.Notify(sigint, os.Interrupt, syscall.SIGTERM) - <-sigint - cancel() // cancel the context - }() - - return p.server.Start(ctx) -} + ProxyPrefix: opts.ProxyPrefix, + provider: provider, + sessionStore: sessionStore, + redirectURL: redirectURL, + relativeRedirectURL: opts.RelativeRedirectURL, + whitelistDomains: opts.WhitelistDomains, + skipAuthPreflight: opts.SkipAuthPreflight, -func (p *OAuthProxy) setupServer(opts *options.Options) error { - serverOpts := proxyhttp.Opts{ - Handler: p, - BindAddress: opts.Server.BindAddress, - SecureBindAddress: opts.Server.SecureBindAddress, - TLS: opts.Server.TLS, - } + sessionChain: sessionChain, + preAuthChain: preAuthChain, - // Option: AllowQuerySemicolons - if opts.AllowQuerySemicolons { - serverOpts.Handler = http.AllowQuerySemicolons(serverOpts.Handler) - } + redirectValidator: redirectValidator, + appDirector: appDirector, + encodeState: opts.EncodeState, + passAuthorization: opts.PassAuthorization, - appServer, err := proxyhttp.NewServer(serverOpts) - if err != nil { - return fmt.Errorf("could not build app server: %v", err) + client: serviceClient, } + p.buildServeMux(opts.ProxyPrefix) - metricsServer, err := proxyhttp.NewServer(proxyhttp.Opts{ - Handler: middleware.DefaultMetricsHandler, - BindAddress: opts.MetricsServer.BindAddress, - SecureBindAddress: opts.MetricsServer.SecureBindAddress, - TLS: opts.MetricsServer.TLS, - }) - if err != nil { - return fmt.Errorf("could not build metrics server: %v", err) - } + return p, nil +} - p.server = proxyhttp.NewServerGroup(appServer, metricsServer) - return nil +func SetLogger(log wrapper.Log) { + util.Logger = &log } func (p *OAuthProxy) buildServeMux(proxyPrefix string) { - // Use the encoded path here so we can have the option to pass it on in the upstream mux. - // Otherwise something like /%2F/ would be redirected to / here already. + // Use the encoded path here, so we can have the option to pass it on in the upstream mux. + // Otherwise, something like /%2F/ would be redirected to / here already. r := mux.NewRouter().UseEncodedPath() // Everything served by the router must go through the preAuthChain first. r.Use(p.preAuthChain.Then) - // Register the robots path writer - r.Path(robotsPath).HandlerFunc(p.pageWriter.WriteRobotsTxt) - - // The authonly path should be registered separately to prevent it from getting no-cache headers. - // We do this to allow users to have a short cache (via nginx) of the response to reduce the - // likelihood of multiple requests trying to refresh sessions simultaneously. - r.Path(proxyPrefix + authOnlyPath).Handler(p.sessionChain.ThenFunc(p.AuthOnly)) - - // This will register all of the paths under the proxy prefix, except the auth only path so that no cache headers + // This will register all the paths under the proxy prefix, except the auth only path so that no cache headers // are not applied. - p.buildProxySubrouter(r.PathPrefix(proxyPrefix).Subrouter()) + p.buildProxySubRouter(r.PathPrefix(proxyPrefix).Subrouter()) - // Register serveHTTP last so it catches anything that isn't already caught earlier. + // Register serveHTTP last, so it catches anything that isn't already caught earlier. // Anything that got to this point needs to have a session loaded. r.PathPrefix("/").Handler(p.sessionChain.ThenFunc(p.Proxy)) p.serveMux = r } -func (p *OAuthProxy) buildProxySubrouter(s *mux.Router) { +func (p *OAuthProxy) buildProxySubRouter(s *mux.Router) { s.Use(prepareNoCacheMiddleware) - s.Path(signInPath).HandlerFunc(p.SignIn) s.Path(oauthStartPath).HandlerFunc(p.OAuthStart) s.Path(oauthCallbackPath).HandlerFunc(p.OAuthCallback) - // Static file paths - s.PathPrefix(staticPathPrefix).Handler(http.StripPrefix(p.ProxyPrefix, http.FileServer(http.FS(staticFiles)))) - - // The userinfo and logout endpoints needs to load sessions before handling the request - s.Path(userInfoPath).Handler(p.sessionChain.ThenFunc(p.UserInfo)) s.Path(signOutPath).Handler(p.sessionChain.ThenFunc(p.SignOut)) } // buildPreAuthChain constructs a chain that should process every request before // the OAuth2 Proxy authentication logic kicks in. // For example forcing HTTPS or health checks. -func buildPreAuthChain(opts *options.Options, sessionStore sessionsapi.SessionStore) (alice.Chain, error) { - chain := alice.New(middleware.NewScope(opts.ReverseProxy, opts.Logging.RequestIDHeader)) - - if opts.ForceHTTPS { - _, httpsPort, err := net.SplitHostPort(opts.Server.SecureBindAddress) - if err != nil { - return alice.Chain{}, fmt.Errorf("invalid HTTPS address %q: %v", opts.Server.SecureBindAddress, err) - } - chain = chain.Append(middleware.NewRedirectToHTTPS(httpsPort)) - } - - healthCheckPaths := []string{opts.PingPath} - healthCheckUserAgents := []string{opts.PingUserAgent} - if opts.GCPHealthChecks { - logger.Printf("WARNING: GCP HealthChecks are now deprecated: Reconfigure apps to use the ping path for liveness and readiness checks, set the ping user agent to \"GoogleHC/1.0\" to preserve existing behaviour") - healthCheckPaths = append(healthCheckPaths, "/liveness_check", "/readiness_check") - healthCheckUserAgents = append(healthCheckUserAgents, "GoogleHC/1.0") - } - - // To silence logging of health checks, register the health check handler before - // the logging handler - if opts.Logging.SilencePing { - chain = chain.Append( - middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), - middleware.NewReadynessCheck(opts.ReadyPath, sessionStore), - middleware.NewRequestLogger(), - ) - } else { - chain = chain.Append( - middleware.NewRequestLogger(), - middleware.NewHealthCheck(healthCheckPaths, healthCheckUserAgents), - middleware.NewReadynessCheck(opts.ReadyPath, sessionStore), - ) - } - - chain = chain.Append(middleware.NewRequestMetricsWithDefaultRegistry()) - +func buildPreAuthChain(opts *options.Options) (alice.Chain, error) { + chain := alice.New(middleware.NewScope(opts.ReverseProxy, "X-Request-Id")) return chain, nil } -func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain { +func buildSessionChain(opts *options.Options, provider providers.Provider, sessionStore sessionsapi.SessionStore, serviceClient wrapper.HttpClient) alice.Chain { chain := alice.New() - if opts.SkipJwtBearerTokens { - sessionLoaders := []middlewareapi.TokenToSessionFunc{ - provider.CreateSessionFromToken, - } - - for _, verifier := range opts.GetJWTBearerVerifiers() { - sessionLoaders = append(sessionLoaders, - middlewareapi.CreateTokenToSessionFunc(verifier.Verify)) - } - - chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders)) - } - - if validator != nil { - chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator, opts.HtpasswdUserGroups, opts.LegacyPreferEmailToUser)) - } - - chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{ - SessionStore: sessionStore, - RefreshPeriod: opts.Cookie.Refresh, - RefreshSession: provider.RefreshSession, - ValidateSession: provider.ValidateSession, - })) - - return chain -} - -func buildHeadersChain(opts *options.Options) (alice.Chain, error) { - requestInjector, err := middleware.NewRequestHeaderInjector(opts.InjectRequestHeaders) - if err != nil { - return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err) - } - - responseInjector, err := middleware.NewResponseHeaderInjector(opts.InjectResponseHeaders) - if err != nil { - return alice.Chain{}, fmt.Errorf("error constructing request header injector: %v", err) - } - - return alice.New(requestInjector, responseInjector), nil -} - -func buildSignInMessage(opts *options.Options) string { - var msg string - if len(opts.Templates.Banner) >= 1 { - if opts.Templates.Banner == "-" { - msg = "" - } else { - msg = opts.Templates.Banner - } - } else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" { - if len(opts.EmailDomains) > 1 { - msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", ")) - } else if opts.EmailDomains[0] != "*" { - msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0]) - } - } - return msg -} - -func buildProviderName(p providers.Provider, override string) string { - if override != "" { - return override - } - return p.Data().ProviderName -} - -// buildRoutesAllowlist builds an []allowedRoute list from either the legacy -// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option -// (method=path support) -func buildRoutesAllowlist(opts *options.Options) ([]allowedRoute, error) { - routes := make([]allowedRoute, 0, len(opts.SkipAuthRegex)+len(opts.SkipAuthRoutes)) - - for _, path := range opts.SkipAuthRegex { - compiledRegex, err := regexp.Compile(path) - if err != nil { - return nil, err - } - logger.Printf("Skipping auth - Method: ALL | Path: %s", path) - routes = append(routes, allowedRoute{ - method: "", - pathRegex: compiledRegex, - }) - } - - for _, methodPath := range opts.SkipAuthRoutes { - var ( - method string - path string - negate = strings.Contains(methodPath, "!=") - ) - - parts := regexp.MustCompile("!?=").Split(methodPath, 2) - if len(parts) == 1 { - method = "" - path = parts[0] - } else { - method = strings.ToUpper(parts[0]) - path = parts[1] - } - - compiledRegex, err := regexp.Compile(path) - if err != nil { - return nil, err - } - logger.Printf("Skipping auth - Method: %s | Path: %s", method, path) - routes = append(routes, allowedRoute{ - method: method, - negate: negate, - pathRegex: compiledRegex, - }) - } - - return routes, nil -} - -// buildAPIRoutes builds an []apiRoute from ApiRoutes option -func buildAPIRoutes(opts *options.Options) ([]apiRoute, error) { - routes := make([]apiRoute, 0, len(opts.APIRoutes)) - - for _, path := range opts.APIRoutes { - compiledRegex, err := regexp.Compile(path) - if err != nil { - return nil, err - } - logger.Printf("API route - Path: %s", path) - routes = append(routes, apiRoute{ - pathRegex: compiledRegex, - }) - } - - return routes, nil -} - -// ClearSessionCookie creates a cookie to unset the user's authentication cookie -// stored in the user's session -func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error { - return p.sessionStore.Clear(rw, req) -} - -// LoadCookiedSession reads the user's authentication details from the request -func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) { - return p.sessionStore.Load(req) -} - -// SaveSession creates a new session cookie value and sets this on the response -func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error { - return p.sessionStore.Save(rw, req, s) -} - -func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - p.serveMux.ServeHTTP(rw, req) -} - -// ErrorPage writes an error response -func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, req *http.Request, code int, appError string, messages ...interface{}) { - redirectURL, err := p.appDirector.GetRedirect(req) - if err != nil { - logger.Errorf("Error obtaining redirect: %v", err) - } - if redirectURL == p.SignInPath || redirectURL == "" { - redirectURL = "/" - } - - scope := middlewareapi.GetRequestScope(req) - p.pageWriter.WriteErrorPage(rw, pagewriter.ErrorPageOpts{ - Status: code, - RedirectURL: redirectURL, - RequestID: scope.RequestID, - AppError: appError, - Messages: messages, + ss, loadSession := middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{ + SessionStore: sessionStore, + RefreshPeriod: opts.Cookie.Refresh, + RefreshSession: provider.RefreshSession, + ValidateSession: provider.ValidateSession, + RefreshClient: serviceClient, + RefreshRequestTimeout: provider.Data().RedeemTimeout, }) -} - -// IsAllowedRequest is used to check if auth should be skipped for this request -func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool { - isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" - return isPreflightRequestAllowed || p.isAllowedRoute(req) || p.isTrustedIP(req) -} - -func isAllowedMethod(req *http.Request, route allowedRoute) bool { - return route.method == "" || req.Method == route.method -} - -func isAllowedPath(req *http.Request, route allowedRoute) bool { - matches := route.pathRegex.MatchString(requestutil.GetRequestURI(req)) - - if route.negate { - return !matches - } - - return matches -} - -// IsAllowedRoute is used to check if the request method & path is allowed without auth -func (p *OAuthProxy) isAllowedRoute(req *http.Request) bool { - for _, route := range p.allowedRoutes { - if isAllowedMethod(req, route) && isAllowedPath(req, route) { - return true - } - } - return false -} - -func (p *OAuthProxy) isAPIPath(req *http.Request) bool { - for _, route := range p.apiRoutes { - if route.pathRegex.MatchString(requestutil.GetRequestURI(req)) { - return true - } - } - return false -} - -// isTrustedIP is used to check if a request comes from a trusted client IP address. -func (p *OAuthProxy) isTrustedIP(req *http.Request) bool { - if p.trustedIPs == nil { - return false - } - - remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req) - if err != nil { - logger.Errorf("Error obtaining real IP for trusted IP list: %v", err) - // Possibly spoofed X-Real-IP header - return false - } - - if remoteAddr == nil { - return false - } - - return p.trustedIPs.Has(remoteAddr) -} - -// SignInPage writes the sign in template to the response -func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) { - prepareNoCache(rw) - err := p.ClearSessionCookie(rw, req) - if err != nil { - logger.Printf("Error clearing session cookie: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - rw.WriteHeader(code) - - redirectURL, err := p.appDirector.GetRedirect(req) - if err != nil { - logger.Errorf("Error obtaining redirect: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - - if redirectURL == p.SignInPath { - redirectURL = "/" - } - - p.pageWriter.WriteSignInPage(rw, req, redirectURL, code) -} - -// ManualSignIn handles basic auth logins to the proxy -func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool, int) { - if req.Method != "POST" || p.basicAuthValidator == nil { - return "", false, http.StatusOK - } - user := req.FormValue("username") - passwd := req.FormValue("password") - if user == "" { - return "", false, http.StatusBadRequest - } - // check auth - if p.basicAuthValidator.Validate(user, passwd) { - logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via HtpasswdFile") - return user, true, http.StatusOK - } - logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via HtpasswdFile") - return "", false, http.StatusUnauthorized -} - -// SignIn serves a page prompting users to sign in -func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) { - redirect, err := p.appDirector.GetRedirect(req) - if err != nil { - logger.Errorf("Error obtaining redirect: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - - user, ok, statusCode := p.ManualSignIn(req) - if ok { - session := &sessionsapi.SessionState{User: user, Groups: p.basicAuthGroups} - err = p.SaveSession(rw, req, session) - if err != nil { - logger.Printf("Error saving session: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - http.Redirect(rw, req, redirect, http.StatusFound) - } else { - if p.SkipProviderButton { - p.OAuthStart(rw, req) - } else { - // TODO - should we pass on /oauth2/sign_in query params to /oauth2/start? - p.SignInPage(rw, req, statusCode) - } - } -} - -// UserInfo endpoint outputs session email and preferred username in JSON format -func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { - session, err := p.getAuthenticatedSession(rw, req) - if err != nil { - http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(http.StatusOK) - if session == nil { - if _, err := rw.Write([]byte("{}")); err != nil { - logger.Printf("Error encoding empty user info: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - } - return - } - - userInfo := struct { - User string `json:"user"` - Email string `json:"email"` - Groups []string `json:"groups,omitempty"` - PreferredUsername string `json:"preferredUsername,omitempty"` - }{ - User: session.User, - Email: session.Email, - Groups: session.Groups, - PreferredUsername: session.PreferredUsername, - } - - if err := json.NewEncoder(rw).Encode(userInfo); err != nil { - logger.Printf("Error encoding user info: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - } + chain = chain.Append(loadSession) + provider.Data().StoredSession = ss + provider.Data().StoredSession.NeedsVerifier = provider.Data().NeedsVerifier + return chain } // SignOut sends a response to clear the authentication cookie func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) { redirect, err := p.appDirector.GetRedirect(req) if err != nil { - logger.Errorf("Error obtaining redirect: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.Logger.Errorf("Error obtaining redirect: %v", err) return } err = p.ClearSessionCookie(rw, req) if err != nil { - logger.Errorf("Error clearing session cookie: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.Logger.Errorf("Error clearing session cookie: %v", err) return } - - p.backendLogout(rw, req) - - http.Redirect(rw, req, redirect, http.StatusFound) -} - -func (p *OAuthProxy) backendLogout(rw http.ResponseWriter, req *http.Request) { + // odic hint_token_hint used to logout without promotion. session, err := p.getAuthenticatedSession(rw, req) - if err != nil { - logger.Errorf("error getting authenticated session during backend logout: %v", err) - return - } - - if session == nil { - return - } - providerData := p.provider.Data() - if providerData.BackendLogoutURL == "" { - return + values := url.Values{} + if session != nil { + values.Add("id_token_hint", session.IDToken) } - backendLogoutURL := strings.ReplaceAll(providerData.BackendLogoutURL, "{id_token}", session.IDToken) - // security exception because URL is dynamic ({id_token} replacement) but - // base is not end-user provided but comes from configuration somewhat secure - resp, err := http.Get(backendLogoutURL) // #nosec G107 - if err != nil { - logger.Errorf("error while calling backend logout: %v", err) - return + if len(values) > 0 { + redirectURL, err := url.Parse(redirect) + if err != nil { + util.Logger.Errorf("Error parsing redirect: %v", err) + return + } + query := redirectURL.Query() + if len(query) > 0 || redirectURL.Fragment != "" { + // If there are existing query parameters or a fragment, use "&" + redirect = redirect + "&" + values.Encode() + } else { + // If there are no query parameters or fragment, use "?" + redirect = redirect + "?" + values.Encode() + } } - defer resp.Body.Close() - if resp.StatusCode != 200 { - logger.Errorf("error while calling backend logout url, returned error code %v", resp.StatusCode) - } + redirectToLocation(rw, redirect) } // OAuthStart starts the OAuth2 authentication flow @@ -801,15 +257,13 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove codeChallengeMethod = p.provider.Data().CodeChallengeMethod codeVerifier, err = encryption.GenerateRandomASCIIString(96) if err != nil { - logger.Errorf("Unable to build random ASCII string for code verifier: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.SendError(fmt.Sprintf("Unable to build random ASCII string for code verifier: %v", err), rw, http.StatusInternalServerError) return } codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier) if err != nil { - logger.Errorf("Error creating code challenge: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.SendError(fmt.Sprintf("Error creating code challenge: %v", err), rw, http.StatusInternalServerError) return } @@ -819,19 +273,17 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove csrf, err := cookies.NewCSRF(p.CookieOptions, codeVerifier) if err != nil { - logger.Errorf("Error creating CSRF nonce: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.SendError(fmt.Sprintf("Error creating CSRF nonce: %v", err), rw, http.StatusInternalServerError) return } appRedirect, err := p.appDirector.GetRedirect(req) if err != nil { - logger.Errorf("Error obtaining application redirect: %v", err) - p.ErrorPage(rw, req, http.StatusBadRequest, err.Error()) + util.SendError(fmt.Sprintf("Error obtaining application redirect: %v", err), rw, http.StatusBadRequest) return } - callbackRedirect := p.getOAuthRedirectURI(req) + loginURL := p.provider.GetLoginURL( callbackRedirect, encodeState(csrf.HashOAuthState(), appRedirect, p.encodeState), @@ -840,258 +292,169 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove ) if _, err := csrf.SetCookie(rw, req); err != nil { - logger.Errorf("Error setting CSRF cookie: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.SendError(fmt.Sprintf("Error setting CSRF cookie: %v", err), rw, http.StatusInternalServerError) return } + redirectToLocation(rw, loginURL) +} + +// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will +// redirect clients to once authenticated. +// This is usually the OAuthProxy callback URL. +func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string { + // if `p.redirectURL` already has a host, return it + if p.relativeRedirectURL || p.redirectURL.Host != "" { + return p.redirectURL.String() + } + + // Otherwise figure out the scheme + host from the request + rd := *p.redirectURL + rd.Host = requestutil.GetRequestHost(req) + rd.Scheme = requestutil.GetRequestProto(req) - http.Redirect(rw, req, loginURL, http.StatusFound) + // If there's no scheme in the request, we should still include one + if rd.Scheme == "" { + rd.Scheme = schemeHTTP + } + + // If CookieSecure is true, return `https` no matter what + // Not all reverse proxies set X-Forwarded-Proto + if p.CookieOptions.Secure { + rd.Scheme = schemeHTTPS + } + return rd.String() } // OAuthCallback is the OAuth2 authentication flow callback that finishes the // OAuth2 authentication flow func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) { - remoteAddr := ip.GetClientString(p.realClientIPParser, req, true) - // finish the oauth cycle err := req.ParseForm() if err != nil { - logger.Errorf("Error while parsing OAuth2 callback: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.SendError(fmt.Sprintf("Error while parsing OAuth2 callback: %v", err), rw, http.StatusInternalServerError) return } errorString := req.Form.Get("error") if errorString != "" { - logger.Errorf("Error while parsing OAuth2 callback: %s", errorString) - message := fmt.Sprintf("Login Failed: The upstream identity provider returned an error: %s", errorString) - // Set the debug message and override the non debug message to be the same for this case - p.ErrorPage(rw, req, http.StatusForbidden, message, message) + util.SendError(fmt.Sprintf("Error while parsing OAuth2 callback: %s", errorString), rw, http.StatusForbidden) return } csrf, err := cookies.LoadCSRFCookie(req, p.CookieOptions) if err != nil { - logger.Println(req, logger.AuthFailure, "Invalid authentication via OAuth2. Error while loading CSRF cookie:", err.Error()) - p.ErrorPage(rw, req, http.StatusForbidden, err.Error(), "Login Failed: Unable to find a valid CSRF token. Please try again.") - return - } - - session, err := p.redeemCode(req, csrf.GetCodeVerifier()) - if err != nil { - logger.Errorf("Error redeeming code during OAuth2 callback: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - - err = p.enrichSessionState(req.Context(), session) - if err != nil { - logger.Errorf("Error creating session during OAuth2 callback: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - - csrf.ClearCookie(rw, req) - - nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState) - if err != nil { - logger.Errorf("Error while parsing OAuth2 state: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - return - } - - if !csrf.CheckOAuthState(nonce) { - logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: CSRF token mismatch, potential attack") - p.ErrorPage(rw, req, http.StatusForbidden, "CSRF token mismatch, potential attack", "Login Failed: Unable to find a valid CSRF token. Please try again.") + util.SendError(fmt.Sprintf("Invalid authentication via OAuth2. Error while loading CSRF cookie: %v", err), rw, http.StatusForbidden) return } - csrf.SetSessionNonce(session) - if !p.provider.ValidateSession(req.Context(), session) { - logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Session validation failed: %s", session) - p.ErrorPage(rw, req, http.StatusForbidden, "Session validation failed") - return - } - - if !p.redirectValidator.IsValidRedirect(appRedirect) { - appRedirect = "/" - } - - // set cookie, or deny - authorized, err := p.provider.Authorize(req.Context(), session) - if err != nil { - logger.Errorf("Error with authorization: %v", err) - } - if p.Validator(session.Email) && authorized { - logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Authenticated via OAuth2: %s", session) - err := p.SaveSession(rw, req, session) + callback := func(args ...interface{}) { + session := args[0].(*sessionsapi.SessionState) + csrf.ClearCookie(rw, req) + nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState) if err != nil { - logger.Errorf("Error saving session state for %s: %v", remoteAddr, err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) + util.SendError(fmt.Sprintf("Error while parsing OAuth2 state: %v", err), rw, http.StatusInternalServerError) return } - http.Redirect(rw, req, appRedirect, http.StatusFound) - } else { - logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized") - p.ErrorPage(rw, req, http.StatusForbidden, "Invalid session: unauthorized") - } -} -func (p *OAuthProxy) redeemCode(req *http.Request, codeVerifier string) (*sessionsapi.SessionState, error) { - code := req.Form.Get("code") - if code == "" { - return nil, providers.ErrMissingCode - } - - redirectURI := p.getOAuthRedirectURI(req) - s, err := p.provider.Redeem(req.Context(), redirectURI, code, codeVerifier) - if err != nil { - return nil, err - } - - // Force setting these in case the Provider didn't - if s.CreatedAt == nil { - s.CreatedAtNow() - } - if s.ExpiresOn == nil { - s.ExpiresIn(p.CookieOptions.Expire) - } - - return s, nil -} + if !csrf.CheckOAuthState(nonce) { + util.SendError("Invalid authentication via OAuth2: CSRF token mismatch, potential attack", rw, http.StatusForbidden) + return + } + csrf.SetSessionNonce(session) -func (p *OAuthProxy) enrichSessionState(ctx context.Context, s *sessionsapi.SessionState) error { - var err error - if s.Email == "" { - // TODO(@NickMeves): Remove once all provider are updated to implement EnrichSession - // nolint:staticcheck - s.Email, err = p.provider.GetEmailAddress(ctx, s) - if err != nil && !errors.Is(err, providers.ErrNotImplemented) { - return err + updateKeysCallback := func(args ...interface{}) { + if !p.provider.ValidateSession(req.Context(), session) { + util.SendError(fmt.Sprintf("Session validation failed: %s", session), rw, http.StatusForbidden) + return + } + if !p.redirectValidator.IsValidRedirect(appRedirect) { + appRedirect = "/" + } + // set cookie, or deny + authorized, err := p.provider.Authorize(req.Context(), session) + if err != nil { + util.Logger.Errorf("Error with authorization: %v", err) + } + if p.validator(session.Email) && authorized { + util.Logger.Infof("Authenticated successfully via OAuth2: %s", session) + err := p.SaveSession(rw, req, session) + if err != nil { + util.SendError(fmt.Sprintf("Error saving session state: %v", err), rw, http.StatusInternalServerError) + return + } + redirectToLocation(rw, appRedirect) + } else { + util.SendError("Invalid authentication via OAuth2: unauthorized", rw, http.StatusForbidden) + } + } + if p.provider.Data().NeedsVerifier { + if _, err := (*p.provider.Data().Verifier.GetKeySet()).VerifySignature(req.Context(), session.IDToken); err != nil { + (*p.provider.Data().Verifier.GetKeySet()).UpdateKeys(p.client, p.provider.Data().VerifierTimeout, updateKeysCallback) + } else { + updateKeysCallback() + } + } else { + updateKeysCallback() } } - return p.provider.EnrichSession(ctx, s) -} - -// AuthOnly checks whether the user is currently logged in (both authentication -// and optional authorization). -func (p *OAuthProxy) AuthOnly(rw http.ResponseWriter, req *http.Request) { - session, err := p.getAuthenticatedSession(rw, req) + err = p.redeemCode(req, csrf.GetCodeVerifier(), p.client, callback) if err != nil { - http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) - return - } - - // Unauthorized cases need to return 403 to prevent infinite redirects with - // subrequest architectures - if !authOnlyAuthorize(req, session) { - http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden) + util.SendError(fmt.Sprintf("Error redeeming code during OAuth2 callback: %v", err), rw, http.StatusInternalServerError) return } - - // we are authenticated - p.addHeadersForProxying(rw, session) - p.headersChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { - rw.WriteHeader(http.StatusAccepted) - })).ServeHTTP(rw, req) } // Proxy proxies the user request if the user is authenticated else it prompts // them to authenticate func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) { session, err := p.getAuthenticatedSession(rw, req) - switch err { - case nil: - // we are authenticated - p.addHeadersForProxying(rw, session) - p.headersChain.Then(p.upstreamProxy).ServeHTTP(rw, req) - case ErrNeedsLogin: - // we need to send the user to a login screen - if p.forceJSONErrors || isAjax(req) || p.isAPIPath(req) { - logger.Printf("No valid authentication in request. Access Denied.") - // no point redirecting an AJAX request - p.errorJSON(rw, http.StatusUnauthorized) - return + switch { + case err == nil: + rw.WriteHeader(http.StatusOK) + if p.passAuthorization { + proxywasm.AddHttpRequestHeader("Authorization", fmt.Sprintf("%s %s", providers.TokenTypeBearer, session.AccessToken)) } - - logger.Printf("No valid authentication in request. Initiating login.") - if p.SkipProviderButton { - // start OAuth flow, but only with the default login URL params - do not - // consider this request's query params as potential overrides, since - // the user did not explicitly start the login flow - p.doOAuthStart(rw, req, nil) + if cookies, ok := rw.Header()[SetCookieHeader]; ok && len(cookies) > 0 { + newCookieValue := strings.Join(cookies, ",") + if p.ctx != nil { + p.ctx.SetContext(SetCookieHeader, newCookieValue) + util.Logger.Info("Authentication and session refresh successfully .") + } else { + util.Logger.Error("Set Cookie failed cause HttpContext is nil.") + } } else { - p.SignInPage(rw, req, http.StatusForbidden) + util.Logger.Info("Authentication successfully.") } - - case ErrAccessDenied: - if p.forceJSONErrors { - p.errorJSON(rw, http.StatusForbidden) + case errors.Is(err, ErrNeedsLogin): + // we need to send the user to a login screen + if isAjax(req) { + util.SendError("No valid authentication in request. Access Denied.", rw, http.StatusUnauthorized) + return + } + util.Logger.Info("No valid authentication in request. Initiating login.") + // start OAuth flow, but only with the default login URL params - do not + // consider this request's query params as potential overrides, since + // the user did not explicitly start the login flow + p.doOAuthStart(rw, req, nil) + case errors.Is(err, ErrAccessDenied): + if cookies, ok := rw.Header()[SetCookieHeader]; ok && len(cookies) > 0 { + newCookieValue := strings.Join(cookies, ",") + errorMsg := "The session failed authorization checks. clear the cookie" + proxywasm.SendHttpResponseWithDetail(http.StatusForbidden, errorMsg, [][2]string{{SetCookieHeader, newCookieValue}}, []byte(http.StatusText(http.StatusForbidden)), -1) } else { - p.ErrorPage(rw, req, http.StatusForbidden, "The session failed authorization checks") + util.SendError("The session failed authorization checks", rw, http.StatusForbidden) } - default: // unknown error - logger.Errorf("Unexpected internal error: %v", err) - p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) - } -} - -// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en -var noCacheHeaders = map[string]string{ - "Expires": time.Unix(0, 0).Format(time.RFC1123), - "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0", - "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ -} - -// prepareNoCache prepares headers for preventing browser caching. -func prepareNoCache(w http.ResponseWriter) { - // Set NoCache headers - for k, v := range noCacheHeaders { - w.Header().Set(k, v) + util.SendError(fmt.Sprintf("Unexpected internal error: %v", err), rw, http.StatusInternalServerError) } } -func prepareNoCacheMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - prepareNoCache(rw) - next.ServeHTTP(rw, req) - }) -} - -// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will -// redirect clients to once authenticated. -// This is usually the OAuthProxy callback URL. -func (p *OAuthProxy) getOAuthRedirectURI(req *http.Request) string { - // if `p.redirectURL` already has a host, return it - if p.relativeRedirectURL || p.redirectURL.Host != "" { - return p.redirectURL.String() - } - - // Otherwise figure out the scheme + host from the request - rd := *p.redirectURL - rd.Host = requestutil.GetRequestHost(req) - rd.Scheme = requestutil.GetRequestProto(req) - - // If there's no scheme in the request, we should still include one - if rd.Scheme == "" { - rd.Scheme = schemeHTTP - } - - // If CookieSecure is true, return `https` no matter what - // Not all reverse proxies set X-Forwarded-Proto - if p.CookieOptions.Secure { - rd.Scheme = schemeHTTPS - } - return rd.String() -} - // getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so // Returns: -// - `nil, ErrNeedsLogin` if user needs to login. +// - `nil, ErrNeedsLogin` if user needs to log in. // - `nil, ErrAccessDenied` if the authenticated user is not authorized -// Set-Cookie headers may be set on the response as a side-effect of calling this method. +// Set-Cookie headers may be set on the response as a side effect of calling this method. func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) { session := middlewareapi.GetRequestScope(req).Session @@ -1104,23 +467,22 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R return nil, ErrNeedsLogin } - invalidEmail := session.Email != "" && !p.Validator(session.Email) + invalidEmail := session.Email != "" && !p.validator(session.Email) authorized, err := p.provider.Authorize(req.Context(), session) if err != nil { - logger.Errorf("Error with authorization: %v", err) + util.Logger.Errorf("Error with authorization: %v", err) } - if invalidEmail || !authorized { cause := "unauthorized" if invalidEmail { cause = "invalid email" } - logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authorization via session (%s): removing session %s", cause, session) + util.Logger.Errorf("Invalid authorization via session (%s): removing session", cause) // Invalid session, clear it err := p.ClearSessionCookie(rw, req) if err != nil { - logger.Errorf("Error clearing session cookie: %v", err) + util.Logger.Errorf("Error clearing session cookie: %v", err) } return nil, ErrAccessDenied } @@ -1128,106 +490,53 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R return session, nil } -// authOnlyAuthorize handles special authorization logic that is only done -// on the AuthOnly endpoint for use with Nginx subrequest architectures. -func authOnlyAuthorize(req *http.Request, s *sessionsapi.SessionState) bool { - // Allow requests previously allowed to be bypassed - if s == nil { - return true - } - - constraints := []func(*http.Request, *sessionsapi.SessionState) bool{ - checkAllowedGroups, - checkAllowedEmailDomains, - checkAllowedEmails, - } - - for _, constraint := range constraints { - if !constraint(req, s) { - return false - } - } - - return true +// IsAllowedRequest is used to check if auth should be skipped for this request +func (p *OAuthProxy) IsAllowedRequest(req *http.Request) bool { + isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS" + return isPreflightRequestAllowed } -// extractAllowedEntities aims to extract and split allowed entities linked by a key, -// from an HTTP request query. Output is a map[string]struct{} where keys are valuable, -// the goal is to avoid time complexity O(N^2) while finding matches during membership checks. -func extractAllowedEntities(req *http.Request, key string) map[string]struct{} { - entities := map[string]struct{}{} - - query := req.URL.Query() - for _, allowedEntities := range query[key] { - for _, entity := range strings.Split(allowedEntities, ",") { - if entity != "" { - entities[entity] = struct{}{} - } - } +func (p *OAuthProxy) ValidateVerifier() error { + if p.provider.Data().Verifier == nil && p.provider.Data().NeedsVerifier { + return errors.New("Failed to obtain OpenID configuration, current OIDC plugin is not working properly.") } - - return entities + return nil } -// checkAllowedEmailDomains allow email domain restrictions based on the `allowed_email_domains` -// querystring parameter -func checkAllowedEmailDomains(req *http.Request, s *sessionsapi.SessionState) bool { - allowedEmailDomains := extractAllowedEntities(req, "allowed_email_domains") - if len(allowedEmailDomains) == 0 { - return true - } - - splitEmail := strings.Split(s.Email, "@") - if len(splitEmail) != 2 { - return false - } - - endpoint, _ := url.Parse("") - endpoint.Host = splitEmail[1] - - allowedEmailDomainsList := []string{} - for ed := range allowedEmailDomains { - allowedEmailDomainsList = append(allowedEmailDomainsList, ed) - } - - return util.IsEndpointAllowed(endpoint, allowedEmailDomainsList) +func (p *OAuthProxy) SetContext(ctx wrapper.HttpContext) { + p.ctx = ctx } -// checkAllowedGroups allow secondary group restrictions based on the `allowed_groups` -// querystring parameter -func checkAllowedGroups(req *http.Request, s *sessionsapi.SessionState) bool { - allowedGroups := extractAllowedEntities(req, "allowed_groups") - if len(allowedGroups) == 0 { - return true - } - - for _, group := range s.Groups { - if _, ok := allowedGroups[group]; ok { - return true - } +func (p *OAuthProxy) SetVerifier(opts *options.Options) { + if p.provider.Data().Verifier == nil && p.provider.Data().NeedsVerifier { + providers.NewVerifierFromConfig(opts.Providers[0], p.provider.Data(), p.client) } - - return false } -// checkAllowedEmails allow email restrictions based on the `allowed_emails` -// querystring parameter -func checkAllowedEmails(req *http.Request, s *sessionsapi.SessionState) bool { - allowedEmails := extractAllowedEntities(req, "allowed_emails") - if len(allowedEmails) == 0 { - return true - } +func (p *OAuthProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { + p.serveMux.ServeHTTP(w, req) +} - allowed := false +// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en +var noCacheHeaders = map[string]string{ + "Expires": time.Unix(0, 0).Format(time.RFC1123), + "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0", + "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ +} - for email := range allowedEmails { - if email == s.Email { - allowed = true - break - } +// prepareNoCache prepares headers for preventing browser caching. +func prepareNoCache(w http.ResponseWriter) { + // Set NoCache headers + for k, v := range noCacheHeaders { + w.Header().Set(k, v) } +} - return allowed +func prepareNoCacheMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + prepareNoCache(rw) + next.ServeHTTP(rw, req) + }) } // encodedState builds the OAuth state param out of our nonce and @@ -1256,16 +565,40 @@ func decodeState(state string, encode bool) (string, string, error) { return parsedState[0], parsedState[1], nil } -// addHeadersForProxying adds the appropriate headers the request / response for proxying -func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, session *sessionsapi.SessionState) { - if session == nil { - return +// SaveSession creates a new session cookie value and sets this on the response +func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error { + return p.sessionStore.Save(rw, req, s) +} + +// ClearSessionCookie creates a cookie to unset the user's authentication cookie +// stored in the user's session +func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error { + return p.sessionStore.Clear(rw, req) +} + +func (p *OAuthProxy) redeemCode(req *http.Request, codeVerifier string, client wrapper.HttpClient, callback func(args ...interface{})) error { + code := req.Form.Get("code") + if code == "" { + return providers.ErrMissingCode } - if session.Email == "" { - rw.Header().Set("GAP-Auth", session.User) - } else { - rw.Header().Set("GAP-Auth", session.Email) + + setEmptyVar := func(args ...interface{}) { + s := args[0].(*sessionsapi.SessionState) + if s.CreatedAt == nil { + s.CreatedAtNow() + } + if s.ExpiresOn == nil { + s.ExpiresIn(p.CookieOptions.Expire) + } + } + combine := util.Combine(setEmptyVar, callback) + redirectURI := p.getOAuthRedirectURI(req) + err := p.provider.Redeem(req.Context(), redirectURI, code, codeVerifier, client, combine, p.provider.Data().RedeemTimeout) + if err != nil { + return err } + + return nil } // isAjax checks if a request is an ajax request @@ -1288,11 +621,17 @@ func isAjax(req *http.Request) bool { return false } -// errorJSON returns the error code with an application/json mime type -func (p *OAuthProxy) errorJSON(rw http.ResponseWriter, code int) { - rw.Header().Set("Content-Type", applicationJSON) - rw.WriteHeader(code) - // we need to send some JSON response because we set the Content-Type to - // application/json - rw.Write([]byte("{}")) +// redirect to the specified location through proxywasm +func redirectToLocation(rw http.ResponseWriter, location string) { + headersMap := [][2]string{{"Location", location}} + for key, value := range rw.Header() { + if strings.EqualFold(key, SetCookieHeader) { + for _, value := range value { + headersMap = append(headersMap, [2]string{SetCookieHeader, value}) + } + } else { + headersMap = append(headersMap, [2]string{key, strings.Join(value, ",")}) + } + } + proxywasm.SendHttpResponse(http.StatusFound, headersMap, nil, -1) } diff --git a/oauthproxy_test.go b/oauthproxy_test.go deleted file mode 100644 index 772543f3d3..0000000000 --- a/oauthproxy_test.go +++ /dev/null @@ -1,3466 +0,0 @@ -package main - -import ( - "context" - "crypto" - "encoding/base64" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "regexp" - "strings" - "testing" - "time" - - "github.com/coreos/go-oidc/v3/oidc" - "github.com/mbland/hmacauth" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc" - sessionscookie "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" - "github.com/oauth2-proxy/oauth2-proxy/v7/providers" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - // The rawCookieSecret is 32 bytes and the base64CookieSecret is the base64 - // encoded version of this. - rawCookieSecret = "secretthirtytwobytes+abcdefghijk" - base64CookieSecret = "c2VjcmV0dGhpcnR5dHdvYnl0ZXMrYWJjZGVmZ2hpams" - clientID = "3984n253984d7348dm8234yf982t" - clientSecret = "gv3498mfc9t23y23974dm2394dm9" -) - -func init() { - logger.SetFlags(logger.Lshortfile) -} - -func TestRobotsTxt(t *testing.T) { - opts := baseTestOptions() - err := validation.Validate(opts) - assert.NoError(t, err) - - proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) - if err != nil { - t.Fatal(err) - } - rw := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/robots.txt", nil) - proxy.ServeHTTP(rw, req) - assert.Equal(t, 200, rw.Code) - assert.Equal(t, "User-agent: *\nDisallow: /\n", rw.Body.String()) -} - -type TestProvider struct { - *providers.ProviderData - EmailAddress string - ValidToken bool - GroupValidator func(string) bool -} - -var _ providers.Provider = (*TestProvider)(nil) - -func NewTestProvider(providerURL *url.URL, emailAddress string) *TestProvider { - return &TestProvider{ - ProviderData: &providers.ProviderData{ - ProviderName: "Test Provider", - LoginURL: &url.URL{ - Scheme: "http", - Host: providerURL.Host, - Path: "/oauth/authorize", - }, - RedeemURL: &url.URL{ - Scheme: "http", - Host: providerURL.Host, - Path: "/oauth/token", - }, - ProfileURL: &url.URL{ - Scheme: "http", - Host: providerURL.Host, - Path: "/api/v1/profile", - }, - Scope: "profile.email", - }, - EmailAddress: emailAddress, - GroupValidator: func(s string) bool { - return true - }, - } -} - -func (tp *TestProvider) GetEmailAddress(_ context.Context, _ *sessions.SessionState) (string, error) { - return tp.EmailAddress, nil -} - -func (tp *TestProvider) ValidateSession(_ context.Context, _ *sessions.SessionState) bool { - return tp.ValidToken -} - -func Test_redeemCode(t *testing.T) { - opts := baseTestOptions() - err := validation.Validate(opts) - assert.NoError(t, err) - - proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - req := httptest.NewRequest(http.MethodGet, "/", nil) - _, err = proxy.redeemCode(req, "") - assert.Equal(t, providers.ErrMissingCode, err) -} - -func Test_enrichSession(t *testing.T) { - const ( - sessionUser = "Mr Session" - sessionEmail = "session@example.com" - providerEmail = "provider@example.com" - ) - - testCases := map[string]struct { - session *sessions.SessionState - expectedUser string - expectedEmail string - }{ - "Session already has enrichable fields": { - session: &sessions.SessionState{ - User: sessionUser, - Email: sessionEmail, - }, - expectedUser: sessionUser, - expectedEmail: sessionEmail, - }, - "Session is missing Email and GetEmailAddress is implemented": { - session: &sessions.SessionState{ - User: sessionUser, - }, - expectedUser: sessionUser, - expectedEmail: providerEmail, - }, - "Session is missing User and GetUserName is not implemented": { - session: &sessions.SessionState{ - Email: sessionEmail, - }, - expectedUser: "", - expectedEmail: sessionEmail, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - opts := baseTestOptions() - err := validation.Validate(opts) - assert.NoError(t, err) - - proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) - if err != nil { - t.Fatal(err) - } - proxy.provider = NewTestProvider(&url.URL{Host: "www.example.com"}, providerEmail) - - err = proxy.enrichSessionState(context.Background(), tc.session) - assert.NoError(t, err) - assert.Equal(t, tc.expectedUser, tc.session.User) - assert.Equal(t, tc.expectedEmail, tc.session.Email) - }) - } -} - -func TestBasicAuthPassword(t *testing.T) { - providerServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - logger.Printf("%#v", r) - var payload string - switch r.URL.Path { - case "/oauth/token": - payload = `{"access_token": "my_auth_token"}` - default: - payload = r.Header.Get("Authorization") - if payload == "" { - payload = "No Authorization header found." - } - } - w.WriteHeader(200) - _, err := w.Write([]byte(payload)) - if err != nil { - t.Fatal(err) - } - })) - - basicAuthPassword := "This is a secure password" - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: providerServer.URL, - Path: "/", - URI: providerServer.URL, - }, - }, - } - - opts.Cookie.Secure = false - opts.InjectRequestHeaders = []options.Header{ - { - Name: "Authorization", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - BasicAuthPassword: &options.SecretSource{ - Value: []byte(basicAuthPassword), - }, - }, - }, - }, - }, - } - - err := validation.Validate(opts) - assert.NoError(t, err) - - providerURL, _ := url.Parse(providerServer.URL) - const emailAddress = "john.doe@example.com" - - proxy, err := NewOAuthProxy(opts, func(email string) bool { - return email == emailAddress - }) - if err != nil { - t.Fatal(err) - } - proxy.provider = NewTestProvider(providerURL, emailAddress) - - // Save the required session - rw := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - err = proxy.sessionStore.Save(rw, req, &sessions.SessionState{ - Email: emailAddress, - }) - assert.NoError(t, err) - - // Extract the cookie value to inject into the test request - cookie := rw.Header().Values("Set-Cookie")[0] - - req, _ = http.NewRequest("GET", "/", nil) - req.Header.Set("Cookie", cookie) - rw = httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - // The username in the basic auth credentials is expected to be equal to the email address from the - // auth response, so we use the same variable here. - expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(emailAddress+":"+basicAuthPassword)) - assert.Equal(t, expectedHeader, rw.Body.String()) - providerServer.Close() -} - -func TestPassGroupsHeadersWithGroups(t *testing.T) { - opts := baseTestOptions() - opts.InjectRequestHeaders = []options.Header{ - { - Name: "X-Forwarded-Groups", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - } - - err := validation.Validate(opts) - assert.NoError(t, err) - - const emailAddress = "john.doe@example.com" - const userName = "9fcab5c9b889a557" - - groups := []string{"a", "b"} - created := time.Now() - session := &sessions.SessionState{ - User: userName, - Groups: groups, - Email: emailAddress, - AccessToken: "oauth_token", - CreatedAt: &created, - } - - proxy, err := NewOAuthProxy(opts, func(email string) bool { - return email == emailAddress - }) - assert.NoError(t, err) - - // Save the required session - rw := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/", nil) - err = proxy.sessionStore.Save(rw, req, session) - assert.NoError(t, err) - - // Extract the cookie value to inject into the test request - cookie := rw.Header().Values("Set-Cookie")[0] - - req, _ = http.NewRequest("GET", "/", nil) - req.Header.Set("Cookie", cookie) - rw = httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - assert.Equal(t, []string{"a,b"}, req.Header["X-Forwarded-Groups"]) -} - -type PassAccessTokenTest struct { - providerServer *httptest.Server - proxy *OAuthProxy - opts *options.Options -} - -type PassAccessTokenTestOptions struct { - PassAccessToken bool - ValidToken bool - ProxyUpstream options.Upstream -} - -func NewPassAccessTokenTest(opts PassAccessTokenTestOptions) (*PassAccessTokenTest, error) { - patt := &PassAccessTokenTest{} - - patt.providerServer = httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var payload string - switch r.URL.Path { - case "/oauth/token": - payload = `{"access_token": "my_auth_token"}` - default: - payload = r.Header.Get("X-Forwarded-Access-Token") - if payload == "" { - payload = "No access token found." - } - } - w.WriteHeader(200) - _, err := w.Write([]byte(payload)) - if err != nil { - panic(err) - } - })) - - patt.opts = baseTestOptions() - patt.opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: patt.providerServer.URL, - Path: "/", - URI: patt.providerServer.URL, - }, - }, - } - if opts.ProxyUpstream.ID != "" { - patt.opts.UpstreamServers.Upstreams = append(patt.opts.UpstreamServers.Upstreams, opts.ProxyUpstream) - } - - patt.opts.Cookie.Secure = false - if opts.PassAccessToken { - patt.opts.InjectRequestHeaders = []options.Header{ - { - Name: "X-Forwarded-Access-Token", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "access_token", - }, - }, - }, - }, - } - } - - err := validation.Validate(patt.opts) - if err != nil { - return nil, err - } - - providerURL, _ := url.Parse(patt.providerServer.URL) - const emailAddress = "michael.bland@gsa.gov" - - testProvider := NewTestProvider(providerURL, emailAddress) - testProvider.ValidToken = opts.ValidToken - patt.proxy, err = NewOAuthProxy(patt.opts, func(email string) bool { - return email == emailAddress - }) - patt.proxy.provider = testProvider - if err != nil { - return nil, err - } - return patt, nil -} - -func (patTest *PassAccessTokenTest) Close() { - patTest.providerServer.Close() -} - -func (patTest *PassAccessTokenTest) getCallbackEndpoint() (httpCode int, cookie string) { - rw := httptest.NewRecorder() - - csrf, err := cookies.NewCSRF(patTest.proxy.CookieOptions, "") - if err != nil { - panic(err) - } - - req, err := http.NewRequest( - http.MethodGet, - fmt.Sprintf( - "/oauth2/callback?code=callback_code&state=%s", - encodeState(csrf.HashOAuthState(), "%2F", false), - ), - strings.NewReader(""), - ) - if err != nil { - return 0, "" - } - - // rw is a dummy here, we just want the csrfCookie to add to our req - csrfCookie, err := csrf.SetCookie(httptest.NewRecorder(), req) - if err != nil { - panic(err) - } - req.AddCookie(csrfCookie) - - patTest.proxy.ServeHTTP(rw, req) - - if len(rw.Header().Values("Set-Cookie")) >= 2 { - cookie = rw.Header().Values("Set-Cookie")[1] - } - - return rw.Code, cookie -} - -// getEndpointWithCookie makes a requests againt the oauthproxy with passed requestPath -// and cookie and returns body and status code. -func (patTest *PassAccessTokenTest) getEndpointWithCookie(cookie string, endpoint string) (httpCode int, accessToken string) { - cookieName := patTest.proxy.CookieOptions.Name - var value string - keyPrefix := cookieName + "=" - - for _, field := range strings.Split(cookie, "; ") { - value = strings.TrimPrefix(field, keyPrefix) - if value != field { - break - } - value = "" - } - if value == "" { - return 0, "" - } - - req, err := http.NewRequest("GET", endpoint, strings.NewReader("")) - if err != nil { - return 0, "" - } - req.AddCookie(&http.Cookie{ - Name: cookieName, - Value: value, - Path: "/", - Expires: time.Now().Add(time.Duration(24)), - HttpOnly: true, - }) - - rw := httptest.NewRecorder() - patTest.proxy.ServeHTTP(rw, req) - return rw.Code, rw.Body.String() -} - -func TestForwardAccessTokenUpstream(t *testing.T) { - patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{ - PassAccessToken: true, - ValidToken: true, - }) - if err != nil { - t.Fatal(err) - } - t.Cleanup(patTest.Close) - - // A successful validation will redirect and set the auth cookie. - code, cookie := patTest.getCallbackEndpoint() - if code != 302 { - t.Fatalf("expected 302; got %d", code) - } - assert.NotNil(t, cookie) - - // Now we make a regular request; the access_token from the cookie is - // forwarded as the "X-Forwarded-Access-Token" header. The token is - // read by the test provider server and written in the response body. - code, payload := patTest.getEndpointWithCookie(cookie, "/") - if code != 200 { - t.Fatalf("expected 200; got %d", code) - } - assert.Equal(t, "my_auth_token", payload) -} - -func TestStaticProxyUpstream(t *testing.T) { - patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{ - PassAccessToken: true, - ValidToken: true, - ProxyUpstream: options.Upstream{ - ID: "static-proxy", - Path: "/static-proxy", - Static: true, - }, - }) - if err != nil { - t.Fatal(err) - } - t.Cleanup(patTest.Close) - - // A successful validation will redirect and set the auth cookie. - code, cookie := patTest.getCallbackEndpoint() - if code != 302 { - t.Fatalf("expected 302; got %d", code) - } - assert.NotEqual(t, nil, cookie) - - // Now we make a regular request against the upstream proxy; And validate - // the returned status code through the static proxy. - code, payload := patTest.getEndpointWithCookie(cookie, "/static-proxy") - if code != 200 { - t.Fatalf("expected 200; got %d", code) - } - assert.Equal(t, "Authenticated", payload) -} - -func TestDoNotForwardAccessTokenUpstream(t *testing.T) { - patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{ - PassAccessToken: false, - ValidToken: true, - }) - if err != nil { - t.Fatal(err) - } - t.Cleanup(patTest.Close) - - // A successful validation will redirect and set the auth cookie. - code, cookie := patTest.getCallbackEndpoint() - if code != 302 { - t.Fatalf("expected 302; got %d", code) - } - assert.NotEqual(t, nil, cookie) - - // Now we make a regular request, but the access token header should - // not be present. - code, payload := patTest.getEndpointWithCookie(cookie, "/") - if code != 200 { - t.Fatalf("expected 200; got %d", code) - } - assert.Equal(t, "No access token found.", payload) -} - -func TestSessionValidationFailure(t *testing.T) { - patTest, err := NewPassAccessTokenTest(PassAccessTokenTestOptions{ - ValidToken: false, - }) - require.NoError(t, err) - t.Cleanup(patTest.Close) - - // An unsuccessful validation will return 403 and not set the auth cookie. - code, cookie := patTest.getCallbackEndpoint() - assert.Equal(t, http.StatusForbidden, code) - assert.Equal(t, "", cookie) -} - -type SignInPageTest struct { - opts *options.Options - proxy *OAuthProxy - signInRegexp *regexp.Regexp - signInProviderRegexp *regexp.Regexp -} - -const signInRedirectPattern = `` -const signInSkipProvider = `>Found<` - -func NewSignInPageTest(skipProvider bool) (*SignInPageTest, error) { - var sipTest SignInPageTest - - sipTest.opts = baseTestOptions() - sipTest.opts.SkipProviderButton = skipProvider - err := validation.Validate(sipTest.opts) - if err != nil { - return nil, err - } - - sipTest.proxy, err = NewOAuthProxy(sipTest.opts, func(email string) bool { - return true - }) - if err != nil { - return nil, err - } - sipTest.signInRegexp = regexp.MustCompile(signInRedirectPattern) - sipTest.signInProviderRegexp = regexp.MustCompile(signInSkipProvider) - - return &sipTest, nil -} - -func (sipTest *SignInPageTest) GetEndpoint(endpoint string) (int, string) { - rw := httptest.NewRecorder() - req, _ := http.NewRequest("GET", endpoint, strings.NewReader("")) - sipTest.proxy.ServeHTTP(rw, req) - return rw.Code, rw.Body.String() -} - -type AlwaysSuccessfulValidator struct { -} - -func (AlwaysSuccessfulValidator) Validate(_, _ string) bool { - return true -} - -func TestManualSignInStoresUserGroupsInTheSession(t *testing.T) { - userGroups := []string{"somegroup", "someothergroup"} - - opts := baseTestOptions() - opts.HtpasswdUserGroups = userGroups - err := validation.Validate(opts) - if err != nil { - t.Fatal(err) - } - - proxy, err := NewOAuthProxy(opts, func(email string) bool { - return true - }) - if err != nil { - t.Fatal(err) - } - proxy.basicAuthValidator = AlwaysSuccessfulValidator{} - - rw := httptest.NewRecorder() - formData := url.Values{} - formData.Set("username", "someuser") - formData.Set("password", "somepass") - signInReq, _ := http.NewRequest(http.MethodPost, "/oauth2/sign_in", strings.NewReader(formData.Encode())) - signInReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") - proxy.ServeHTTP(rw, signInReq) - - assert.Equal(t, http.StatusFound, rw.Code) - - req, _ := http.NewRequest(http.MethodGet, "/something", strings.NewReader(formData.Encode())) - for _, c := range rw.Result().Cookies() { - req.AddCookie(c) - } - - s, err := proxy.sessionStore.Load(req) - if err != nil { - t.Fatal(err) - } - assert.Equal(t, userGroups, s.Groups) -} - -type ManualSignInValidator struct{} - -func (ManualSignInValidator) Validate(user, password string) bool { - switch { - case user == "admin" && password == "adminPass": - return true - default: - return false - } -} - -func ManualSignInWithCredentials(t *testing.T, user, pass string) int { - opts := baseTestOptions() - err := validation.Validate(opts) - if err != nil { - t.Fatal(err) - } - - proxy, err := NewOAuthProxy(opts, func(email string) bool { - return true - }) - if err != nil { - t.Fatal(err) - } - - proxy.basicAuthValidator = ManualSignInValidator{} - - rw := httptest.NewRecorder() - formData := url.Values{} - formData.Set("username", user) - formData.Set("password", pass) - signInReq, _ := http.NewRequest(http.MethodPost, "/oauth2/sign_in", strings.NewReader(formData.Encode())) - signInReq.Header.Add("Content-Type", "application/x-www-form-urlencoded") - proxy.ServeHTTP(rw, signInReq) - - return rw.Code -} - -func TestManualSignInEmptyUsernameAlert(t *testing.T) { - statusCode := ManualSignInWithCredentials(t, "", "") - assert.Equal(t, http.StatusBadRequest, statusCode) -} - -func TestManualSignInInvalidCredentialsAlert(t *testing.T) { - statusCode := ManualSignInWithCredentials(t, "admin", "") - assert.Equal(t, http.StatusUnauthorized, statusCode) -} - -func TestManualSignInCorrectCredentials(t *testing.T) { - statusCode := ManualSignInWithCredentials(t, "admin", "adminPass") - assert.Equal(t, http.StatusFound, statusCode) -} - -func TestSignInPageIncludesTargetRedirect(t *testing.T) { - sipTest, err := NewSignInPageTest(false) - if err != nil { - t.Fatal(err) - } - const endpoint = "/some/random/endpoint" - - code, body := sipTest.GetEndpoint(endpoint) - assert.Equal(t, 403, code) - - match := sipTest.signInRegexp.FindStringSubmatch(body) - if match == nil { - t.Fatal("Did not find pattern in body: " + - signInRedirectPattern + "\nBody:\n" + body) - } - if match[1] != endpoint { - t.Fatal(`expected redirect to "` + endpoint + - `", but was "` + match[1] + `"`) - } -} - -func TestSignInPageInvalidQueryStringReturnsBadRequest(t *testing.T) { - sipTest, err := NewSignInPageTest(true) - if err != nil { - t.Fatal(err) - } - const endpoint = "/?q=%va" - - code, _ := sipTest.GetEndpoint(endpoint) - assert.Equal(t, 400, code) -} - -func TestSignInPageDirectAccessRedirectsToRoot(t *testing.T) { - sipTest, err := NewSignInPageTest(false) - if err != nil { - t.Fatal(err) - } - code, body := sipTest.GetEndpoint("/oauth2/sign_in") - assert.Equal(t, 200, code) - - match := sipTest.signInRegexp.FindStringSubmatch(body) - if match == nil { - t.Fatal("Did not find pattern in body: " + - signInRedirectPattern + "\nBody:\n" + body) - } - if match[1] != "/" { - t.Fatal(`expected redirect to "/", but was "` + match[1] + `"`) - } -} - -func TestSignInPageSkipProvider(t *testing.T) { - sipTest, err := NewSignInPageTest(true) - if err != nil { - t.Fatal(err) - } - - endpoint := "/some/random/endpoint" - - code, body := sipTest.GetEndpoint(endpoint) - assert.Equal(t, 302, code) - - match := sipTest.signInProviderRegexp.FindStringSubmatch(body) - if match == nil { - t.Fatal("Did not find pattern in body: " + - signInSkipProvider + "\nBody:\n" + body) - } -} - -func TestSignInPageSkipProviderDirect(t *testing.T) { - sipTest, err := NewSignInPageTest(true) - if err != nil { - t.Fatal(err) - } - - endpoint := "/sign_in" - - code, body := sipTest.GetEndpoint(endpoint) - assert.Equal(t, 302, code) - - match := sipTest.signInProviderRegexp.FindStringSubmatch(body) - if match == nil { - t.Fatal("Did not find pattern in body: " + - signInSkipProvider + "\nBody:\n" + body) - } -} - -type ProcessCookieTest struct { - opts *options.Options - proxy *OAuthProxy - rw *httptest.ResponseRecorder - req *http.Request - validateUser bool -} - -type ProcessCookieTestOpts struct { - providerValidateCookieResponse bool -} - -type OptionsModifier func(*options.Options) - -func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifier) (*ProcessCookieTest, error) { - var pcTest ProcessCookieTest - - pcTest.opts = baseTestOptions() - for _, modifier := range modifiers { - modifier(pcTest.opts) - } - // First, set the CookieRefresh option so proxy.AesCipher is created, - // needed to encrypt the access_token. - pcTest.opts.Cookie.Refresh = time.Hour - err := validation.Validate(pcTest.opts) - if err != nil { - return nil, err - } - - pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool { - return pcTest.validateUser - }) - if err != nil { - return nil, err - } - testProvider := &TestProvider{ - ProviderData: &providers.ProviderData{}, - ValidToken: opts.providerValidateCookieResponse, - } - - groups := pcTest.opts.Providers[0].AllowedGroups - testProvider.ProviderData.AllowedGroups = make(map[string]struct{}, len(groups)) - for _, group := range groups { - testProvider.ProviderData.AllowedGroups[group] = struct{}{} - } - pcTest.proxy.provider = testProvider - - // Now, zero-out proxy.CookieRefresh for the cases that don't involve - // access_token validation. - pcTest.proxy.CookieOptions.Refresh = time.Duration(0) - pcTest.rw = httptest.NewRecorder() - pcTest.req, _ = http.NewRequest("GET", "/", strings.NewReader("")) - pcTest.validateUser = true - return &pcTest, nil -} - -func NewProcessCookieTestWithDefaults() (*ProcessCookieTest, error) { - return NewProcessCookieTest(ProcessCookieTestOpts{ - providerValidateCookieResponse: true, - }) -} - -func NewProcessCookieTestWithOptionsModifiers(modifiers ...OptionsModifier) (*ProcessCookieTest, error) { - return NewProcessCookieTest(ProcessCookieTestOpts{ - providerValidateCookieResponse: true, - }, modifiers...) -} - -func (p *ProcessCookieTest) SaveSession(s *sessions.SessionState) error { - err := p.proxy.SaveSession(p.rw, p.req, s) - if err != nil { - return err - } - for _, cookie := range p.rw.Result().Cookies() { - p.req.AddCookie(cookie) - } - return nil -} - -func (p *ProcessCookieTest) LoadCookiedSession() (*sessions.SessionState, error) { - return p.proxy.LoadCookiedSession(p.req) -} - -func TestLoadCookiedSession(t *testing.T) { - pcTest, err := NewProcessCookieTestWithDefaults() - if err != nil { - t.Fatal(err) - } - - created := time.Now() - startSession := &sessions.SessionState{Email: "john.doe@example.com", AccessToken: "my_access_token", CreatedAt: &created} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - session, err := pcTest.LoadCookiedSession() - if err != nil { - t.Fatal(err) - } - assert.Equal(t, startSession.Email, session.Email) - assert.Equal(t, "", session.User) - assert.Equal(t, startSession.AccessToken, session.AccessToken) -} - -func TestProcessCookieNoCookieError(t *testing.T) { - pcTest, err := NewProcessCookieTestWithDefaults() - if err != nil { - t.Fatal(err) - } - - session, err := pcTest.LoadCookiedSession() - assert.Error(t, err, "cookie \"_oauth2_proxy\" not present") - if session != nil { - t.Errorf("expected nil session. got %#v", session) - } -} - -func TestProcessCookieRefreshNotSet(t *testing.T) { - pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { - opts.Cookie.Expire = time.Duration(23) * time.Hour - }) - if err != nil { - t.Fatal(err) - } - - reference := time.Now().Add(time.Duration(-2) * time.Hour) - - startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - session, err := pcTest.LoadCookiedSession() - assert.Equal(t, nil, err) - if session.Age() < time.Duration(-2)*time.Hour { - t.Errorf("cookie too young %v", session.Age()) - } - assert.Equal(t, startSession.Email, session.Email) -} - -func TestProcessCookieFailIfCookieExpired(t *testing.T) { - pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { - opts.Cookie.Expire = time.Duration(24) * time.Hour - }) - if err != nil { - t.Fatal(err) - } - - reference := time.Now().Add(time.Duration(25) * time.Hour * -1) - startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - session, err := pcTest.LoadCookiedSession() - assert.NotEqual(t, nil, err) - if session != nil { - t.Errorf("expected nil session %#v", session) - } -} - -func TestProcessCookieFailIfRefreshSetAndCookieExpired(t *testing.T) { - pcTest, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { - opts.Cookie.Expire = time.Duration(24) * time.Hour - }) - if err != nil { - t.Fatal(err) - } - - reference := time.Now().Add(time.Duration(25) * time.Hour * -1) - startSession := &sessions.SessionState{Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - pcTest.proxy.CookieOptions.Refresh = time.Hour - session, err := pcTest.LoadCookiedSession() - assert.NotEqual(t, nil, err) - if session != nil { - t.Errorf("expected nil session %#v", session) - } -} - -func NewUserInfoEndpointTest() (*ProcessCookieTest, error) { - pcTest, err := NewProcessCookieTestWithDefaults() - if err != nil { - return nil, err - } - pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+"/userinfo", nil) - return pcTest, nil -} - -func TestUserInfoEndpointAccepted(t *testing.T) { - testCases := []struct { - name string - session *sessions.SessionState - expectedResponse string - }{ - { - name: "Full session", - session: &sessions.SessionState{ - User: "john.doe", - Email: "john.doe@example.com", - Groups: []string{"example", "groups"}, - AccessToken: "my_access_token", - }, - expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"]}\n", - }, - { - name: "Minimal session", - session: &sessions.SessionState{ - User: "john.doe", - Email: "john.doe@example.com", - Groups: []string{"example", "groups"}, - }, - expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"]}\n", - }, - { - name: "No groups", - session: &sessions.SessionState{ - User: "john.doe", - Email: "john.doe@example.com", - AccessToken: "my_access_token", - }, - expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\"}\n", - }, - { - name: "With Preferred Username", - session: &sessions.SessionState{ - User: "john.doe", - PreferredUsername: "john", - Email: "john.doe@example.com", - Groups: []string{"example", "groups"}, - AccessToken: "my_access_token", - }, - expectedResponse: "{\"user\":\"john.doe\",\"email\":\"john.doe@example.com\",\"groups\":[\"example\",\"groups\"],\"preferredUsername\":\"john\"}\n", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - test, err := NewUserInfoEndpointTest() - if err != nil { - t.Fatal(err) - } - err = test.SaveSession(tc.session) - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - assert.Equal(t, http.StatusOK, test.rw.Code) - bodyBytes, _ := io.ReadAll(test.rw.Body) - assert.Equal(t, tc.expectedResponse, string(bodyBytes)) - }) - } -} - -func TestUserInfoEndpointUnauthorizedOnNoCookieSetError(t *testing.T) { - test, err := NewUserInfoEndpointTest() - if err != nil { - t.Fatal(err) - } - - test.proxy.ServeHTTP(test.rw, test.req) - assert.Equal(t, http.StatusUnauthorized, test.rw.Code) -} - -func TestEncodedUrlsStayEncoded(t *testing.T) { - encodeTest, err := NewSignInPageTest(false) - if err != nil { - t.Fatal(err) - } - code, _ := encodeTest.GetEndpoint("/%2F/test1/%2F/test2") - assert.Equal(t, 403, code) -} - -func NewAuthOnlyEndpointTest(querystring string, modifiers ...OptionsModifier) (*ProcessCookieTest, error) { - pcTest, err := NewProcessCookieTestWithOptionsModifiers(modifiers...) - if err != nil { - return nil, err - } - pcTest.req, _ = http.NewRequest( - "GET", - fmt.Sprintf("%s/auth%s", pcTest.opts.ProxyPrefix, querystring), - nil) - return pcTest, nil -} - -func TestAuthOnlyEndpointAccepted(t *testing.T) { - test, err := NewAuthOnlyEndpointTest("") - if err != nil { - t.Fatal(err) - } - - created := time.Now() - startSession := &sessions.SessionState{ - Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created} - err = test.SaveSession(startSession) - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - assert.Equal(t, http.StatusAccepted, test.rw.Code) - bodyBytes, _ := io.ReadAll(test.rw.Body) - assert.Equal(t, "", string(bodyBytes)) -} - -func TestAuthOnlyEndpointUnauthorizedOnNoCookieSetError(t *testing.T) { - test, err := NewAuthOnlyEndpointTest("") - if err != nil { - t.Fatal(err) - } - - test.proxy.ServeHTTP(test.rw, test.req) - assert.Equal(t, http.StatusUnauthorized, test.rw.Code) - bodyBytes, _ := io.ReadAll(test.rw.Body) - assert.Equal(t, "Unauthorized\n", string(bodyBytes)) -} - -func TestAuthOnlyEndpointUnauthorizedOnExpiration(t *testing.T) { - test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) { - opts.Cookie.Expire = time.Duration(24) * time.Hour - }) - if err != nil { - t.Fatal(err) - } - - reference := time.Now().Add(time.Duration(25) * time.Hour * -1) - startSession := &sessions.SessionState{ - Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &reference} - err = test.SaveSession(startSession) - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - assert.Equal(t, http.StatusUnauthorized, test.rw.Code) - bodyBytes, _ := io.ReadAll(test.rw.Body) - assert.Equal(t, "Unauthorized\n", string(bodyBytes)) -} - -func TestAuthOnlyEndpointUnauthorizedOnEmailValidationFailure(t *testing.T) { - test, err := NewAuthOnlyEndpointTest("") - if err != nil { - t.Fatal(err) - } - - created := time.Now() - startSession := &sessions.SessionState{ - Email: "michael.bland@gsa.gov", AccessToken: "my_access_token", CreatedAt: &created} - err = test.SaveSession(startSession) - assert.NoError(t, err) - test.validateUser = false - - test.proxy.ServeHTTP(test.rw, test.req) - assert.Equal(t, http.StatusUnauthorized, test.rw.Code) - bodyBytes, _ := io.ReadAll(test.rw.Body) - assert.Equal(t, "Unauthorized\n", string(bodyBytes)) -} - -func TestAuthOnlyEndpointSetXAuthRequestHeaders(t *testing.T) { - var pcTest ProcessCookieTest - - pcTest.opts = baseTestOptions() - pcTest.opts.InjectResponseHeaders = []options.Header{ - { - Name: "X-Auth-Request-User", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Email", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Groups", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - { - Name: "X-Forwarded-Preferred-Username", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - }, - } - pcTest.opts.Providers[0].AllowedGroups = []string{"oauth_groups"} - err := validation.Validate(pcTest.opts) - assert.NoError(t, err) - - pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool { - return pcTest.validateUser - }) - if err != nil { - t.Fatal(err) - } - pcTest.proxy.provider = &TestProvider{ - ProviderData: &providers.ProviderData{}, - ValidToken: true, - } - - pcTest.validateUser = true - - pcTest.rw = httptest.NewRecorder() - pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+"/auth", nil) - - created := time.Now() - startSession := &sessions.SessionState{ - User: "oauth_user", Groups: []string{"oauth_groups"}, Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) - assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) - assert.Equal(t, "oauth_user", pcTest.rw.Header().Get("X-Auth-Request-User")) - assert.Equal(t, startSession.Groups, pcTest.rw.Header().Values("X-Auth-Request-Groups")) - assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Get("X-Auth-Request-Email")) -} - -func TestAuthOnlyEndpointSetBasicAuthTrueRequestHeaders(t *testing.T) { - var pcTest ProcessCookieTest - - pcTest.opts = baseTestOptions() - pcTest.opts.InjectResponseHeaders = []options.Header{ - { - Name: "X-Auth-Request-User", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Email", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Groups", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - { - Name: "X-Forwarded-Preferred-Username", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - }, - { - Name: "Authorization", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - BasicAuthPassword: &options.SecretSource{ - Value: []byte("This is a secure password"), - }, - }, - }, - }, - }, - } - - err := validation.Validate(pcTest.opts) - assert.NoError(t, err) - - pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool { - return pcTest.validateUser - }) - if err != nil { - t.Fatal(err) - } - pcTest.proxy.provider = &TestProvider{ - ProviderData: &providers.ProviderData{}, - ValidToken: true, - } - - pcTest.validateUser = true - - pcTest.rw = httptest.NewRecorder() - pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+"/auth", nil) - - created := time.Now() - startSession := &sessions.SessionState{ - User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) - assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) - assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0]) - assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0]) - expectedHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte("oauth_user:This is a secure password")) - assert.Equal(t, expectedHeader, pcTest.rw.Header().Values("Authorization")[0]) -} - -func TestAuthOnlyEndpointSetBasicAuthFalseRequestHeaders(t *testing.T) { - var pcTest ProcessCookieTest - - pcTest.opts = baseTestOptions() - pcTest.opts.InjectResponseHeaders = []options.Header{ - { - Name: "X-Auth-Request-User", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Email", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Groups", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - { - Name: "X-Forwarded-Preferred-Username", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - }, - } - err := validation.Validate(pcTest.opts) - assert.NoError(t, err) - - pcTest.proxy, err = NewOAuthProxy(pcTest.opts, func(email string) bool { - return pcTest.validateUser - }) - if err != nil { - t.Fatal(err) - } - pcTest.proxy.provider = &TestProvider{ - ProviderData: &providers.ProviderData{}, - ValidToken: true, - } - - pcTest.validateUser = true - - pcTest.rw = httptest.NewRecorder() - pcTest.req, _ = http.NewRequest("GET", - pcTest.opts.ProxyPrefix+"/auth", nil) - - created := time.Now() - startSession := &sessions.SessionState{ - User: "oauth_user", Email: "oauth_user@example.com", AccessToken: "oauth_token", CreatedAt: &created} - err = pcTest.SaveSession(startSession) - assert.NoError(t, err) - - pcTest.proxy.ServeHTTP(pcTest.rw, pcTest.req) - assert.Equal(t, http.StatusAccepted, pcTest.rw.Code) - assert.Equal(t, "oauth_user", pcTest.rw.Header().Values("X-Auth-Request-User")[0]) - assert.Equal(t, "oauth_user@example.com", pcTest.rw.Header().Values("X-Auth-Request-Email")[0]) - assert.Equal(t, 0, len(pcTest.rw.Header().Values("Authorization")), "should not have Authorization header entries") -} - -func TestAuthSkippedForPreflightRequests(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("response")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(upstreamServer.Close) - - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - opts.SkipAuthPreflight = true - err := validation.Validate(opts) - assert.NoError(t, err) - - upstreamURL, _ := url.Parse(upstreamServer.URL) - - proxy, err := NewOAuthProxy(opts, func(string) bool { return false }) - if err != nil { - t.Fatal(err) - } - proxy.provider = NewTestProvider(upstreamURL, "") - rw := httptest.NewRecorder() - req, _ := http.NewRequest("OPTIONS", "/preflight-request", nil) - proxy.ServeHTTP(rw, req) - - assert.Equal(t, 200, rw.Code) - assert.Equal(t, "response", rw.Body.String()) -} - -type SignatureAuthenticator struct { - auth hmacauth.HmacAuth -} - -func (v *SignatureAuthenticator) Authenticate(w http.ResponseWriter, r *http.Request) { - result, headerSig, computedSig := v.auth.AuthenticateRequest(r) - - var msg string - switch result { - case hmacauth.ResultNoSignature: - msg = "no signature received" - case hmacauth.ResultMatch: - msg = "signatures match" - case hmacauth.ResultMismatch: - msg = fmt.Sprintf( - "signatures do not match:\n received: %s\n computed: %s", - headerSig, - computedSig) - default: - panic("unknown result value: " + result.String()) - } - - _, err := w.Write([]byte(msg)) - if err != nil { - panic(err) - } -} - -type SignatureTest struct { - opts *options.Options - upstream *httptest.Server - upstreamHost string - provider *httptest.Server - header http.Header - rw *httptest.ResponseRecorder - authenticator *SignatureAuthenticator - authProvider providers.Provider -} - -func NewSignatureTest() (*SignatureTest, error) { - opts := baseTestOptions() - opts.EmailDomains = []string{"acm.org"} - - authenticator := &SignatureAuthenticator{} - upstreamServer := httptest.NewServer( - http.HandlerFunc(authenticator.Authenticate)) - upstreamURL, err := url.Parse(upstreamServer.URL) - if err != nil { - return nil, err - } - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - - providerHandler := func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte(`{"access_token": "my_auth_token"}`)) - if err != nil { - panic(err) - } - } - provider := httptest.NewServer(http.HandlerFunc(providerHandler)) - providerURL, err := url.Parse(provider.URL) - if err != nil { - return nil, err - } - testProvider := NewTestProvider(providerURL, "mbland@acm.org") - - return &SignatureTest{ - opts, - upstreamServer, - upstreamURL.Host, - provider, - make(http.Header), - httptest.NewRecorder(), - authenticator, - testProvider, - }, nil -} - -func (st *SignatureTest) Close() { - st.provider.Close() - st.upstream.Close() -} - -// fakeNetConn simulates an http.Request.Body buffer that will be consumed -// when it is read by the hmacauth.HmacAuth if not handled properly. See: -// -// https://github.com/18F/hmacauth/pull/4 -type fakeNetConn struct { - reqBody string -} - -func (fnc *fakeNetConn) Read(p []byte) (n int, err error) { - if bodyLen := len(fnc.reqBody); bodyLen != 0 { - copy(p, fnc.reqBody) - fnc.reqBody = "" - return bodyLen, io.EOF - } - return 0, io.EOF -} - -func (st *SignatureTest) MakeRequestWithExpectedKey(method, body, key string) error { - err := validation.Validate(st.opts) - if err != nil { - return err - } - proxy, err := NewOAuthProxy(st.opts, func(email string) bool { return true }) - if err != nil { - return err - } - proxy.provider = st.authProvider - - var bodyBuf io.ReadCloser - if body != "" { - bodyBuf = io.NopCloser(&fakeNetConn{reqBody: body}) - } - req := httptest.NewRequest(method, "/foo/bar", bodyBuf) - req.Header = st.header - - state := &sessions.SessionState{ - Email: "mbland@acm.org", AccessToken: "my_access_token"} - err = proxy.SaveSession(st.rw, req, state) - if err != nil { - return err - } - for _, c := range st.rw.Result().Cookies() { - req.AddCookie(c) - } - // This is used by the upstream to validate the signature. - st.authenticator.auth = hmacauth.NewHmacAuth( - crypto.SHA1, []byte(key), upstream.SignatureHeader, upstream.SignatureHeaders) - proxy.ServeHTTP(st.rw, req) - - return nil -} - -func TestRequestSignature(t *testing.T) { - testCases := map[string]struct { - method string - body string - key string - resp string - }{ - "No request signature": { - method: "GET", - body: "", - key: "", - resp: "no signature received", - }, - "Get request": { - method: "GET", - body: "", - key: "7d9e1aa87a5954e6f9fc59266b3af9d7c35fda2d", - resp: "signatures match", - }, - "Post request": { - method: "POST", - body: `{ "hello": "world!" }`, - key: "d90df39e2d19282840252612dd7c81421a372f61", - resp: "signatures match", - }, - } - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - st, err := NewSignatureTest() - if err != nil { - t.Fatal(err) - } - t.Cleanup(st.Close) - if tc.key != "" { - st.opts.SignatureKey = fmt.Sprintf("sha1:%s", tc.key) - } - err = st.MakeRequestWithExpectedKey(tc.method, tc.body, tc.key) - assert.NoError(t, err) - assert.Equal(t, 200, st.rw.Code) - assert.Equal(t, tc.resp, st.rw.Body.String()) - }) - } -} - -type ajaxRequestTest struct { - opts *options.Options - proxy *OAuthProxy -} - -func newAjaxRequestTest(forceJSONErrors bool) (*ajaxRequestTest, error) { - test := &ajaxRequestTest{} - test.opts = baseTestOptions() - test.opts.ForceJSONErrors = forceJSONErrors - err := validation.Validate(test.opts) - if err != nil { - return nil, err - } - - test.proxy, err = NewOAuthProxy(test.opts, func(email string) bool { - return true - }) - if err != nil { - return nil, err - } - return test, nil -} - -func (test *ajaxRequestTest) getEndpoint(endpoint string, header http.Header) (int, http.Header, []byte, error) { - rw := httptest.NewRecorder() - req, err := http.NewRequest(http.MethodGet, endpoint, strings.NewReader("")) - if err != nil { - return 0, nil, nil, err - } - req.Header = header - test.proxy.ServeHTTP(rw, req) - return rw.Code, rw.Header(), rw.Body.Bytes(), nil -} - -func testAjaxUnauthorizedRequest(t *testing.T, header http.Header, forceJSONErrors bool) { - test, err := newAjaxRequestTest(forceJSONErrors) - if err != nil { - t.Fatal(err) - } - endpoint := "/test" - - code, rh, body, err := test.getEndpoint(endpoint, header) - assert.NoError(t, err) - assert.Equal(t, http.StatusUnauthorized, code) - mime := rh.Get("Content-Type") - assert.Equal(t, applicationJSON, mime) - assert.Equal(t, []byte("{}"), body) -} -func TestAjaxUnauthorizedRequest1(t *testing.T) { - header := make(http.Header) - header.Add("accept", applicationJSON) - - testAjaxUnauthorizedRequest(t, header, false) -} - -func TestAjaxUnauthorizedRequest2(t *testing.T) { - header := make(http.Header) - header.Add("Accept", applicationJSON) - - testAjaxUnauthorizedRequest(t, header, false) -} - -func TestAjaxUnauthorizedRequestAccept1(t *testing.T) { - header := make(http.Header) - header.Add("Accept", "application/json, text/plain, */*") - - testAjaxUnauthorizedRequest(t, header, false) -} - -func TestForceJSONErrorsUnauthorizedRequest(t *testing.T) { - testAjaxUnauthorizedRequest(t, nil, true) -} - -func TestAjaxForbiddendRequest(t *testing.T) { - test, err := newAjaxRequestTest(false) - if err != nil { - t.Fatal(err) - } - endpoint := "/test" - header := make(http.Header) - code, rh, _, err := test.getEndpoint(endpoint, header) - assert.NoError(t, err) - assert.Equal(t, http.StatusForbidden, code) - mime := rh.Get("Content-Type") - assert.NotEqual(t, applicationJSON, mime) -} - -func TestClearSplitCookie(t *testing.T) { - opts := baseTestOptions() - opts.Cookie.Secret = base64CookieSecret - opts.Cookie.Name = "oauth2" - opts.Cookie.Domains = []string{"abc"} - err := validation.Validate(opts) - assert.NoError(t, err) - - store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie) - if err != nil { - t.Fatal(err) - } - - p := OAuthProxy{CookieOptions: &opts.Cookie, sessionStore: store} - var rw = httptest.NewRecorder() - req := httptest.NewRequest("get", "/", nil) - - req.AddCookie(&http.Cookie{ - Name: "test1", - Value: "test1", - }) - req.AddCookie(&http.Cookie{ - Name: "oauth2_0", - Value: "oauth2_0", - }) - req.AddCookie(&http.Cookie{ - Name: "oauth2_1", - Value: "oauth2_1", - }) - - err = p.ClearSessionCookie(rw, req) - assert.NoError(t, err) - header := rw.Header() - - assert.Equal(t, 2, len(header["Set-Cookie"]), "should have 3 set-cookie header entries") -} - -func TestClearSingleCookie(t *testing.T) { - opts := baseTestOptions() - opts.Cookie.Name = "oauth2" - opts.Cookie.Domains = []string{"abc"} - store, err := sessionscookie.NewCookieSessionStore(&opts.Session, &opts.Cookie) - if err != nil { - t.Fatal(err) - } - - p := OAuthProxy{CookieOptions: &opts.Cookie, sessionStore: store} - var rw = httptest.NewRecorder() - req := httptest.NewRequest("get", "/", nil) - - req.AddCookie(&http.Cookie{ - Name: "test1", - Value: "test1", - }) - req.AddCookie(&http.Cookie{ - Name: "oauth2", - Value: "oauth2", - }) - - err = p.ClearSessionCookie(rw, req) - assert.NoError(t, err) - header := rw.Header() - - assert.Equal(t, 1, len(header["Set-Cookie"]), "should have 1 set-cookie header entries") -} - -type NoOpKeySet struct { -} - -func (NoOpKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) { - splitStrings := strings.Split(jwt, ".") - payloadString := splitStrings[1] - return base64.RawURLEncoding.DecodeString(payloadString) -} - -func TestGetJwtSession(t *testing.T) { - /* token payload: - { - "sub": "1234567890", - "aud": "https://test.myapp.com", - "name": "John Doe", - "email": "john@example.com", - "iss": "https://issuer.example.com", - "iat": 1553691215, - "exp": 1912151821 - } - */ - goodJwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + - "eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiaHR0cHM6Ly90ZXN0Lm15YXBwLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImVtY" + - "WlsIjoiam9obkBleGFtcGxlLmNvbSIsImlzcyI6Imh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNTUzNjkxMj" + - "E1LCJleHAiOjE5MTIxNTE4MjF9." + - "rLVyzOnEldUq_pNkfa-WiV8TVJYWyZCaM2Am_uo8FGg11zD7l-qmz3x1seTvqpH6Y0Ty00fmv6dJnGnC8WMnPXQiodRTfhBSe" + - "OKZMu0HkMD2sg52zlKkbfLTO6ic5VnbVgwjjrB8am_Ta6w7kyFUaB5C1BsIrrLMldkWEhynbb8" - - keyset := NoOpKeySet{} - verifier := oidc.NewVerifier("https://issuer.example.com", keyset, - &oidc.Config{ClientID: "https://test.myapp.com", SkipExpiryCheck: true, - SkipClientIDCheck: true}) - verificationOptions := internaloidc.IDTokenVerificationOptions{ - AudienceClaims: []string{"aud"}, - ClientID: "https://test.myapp.com", - ExtraAudiences: []string{}, - } - internalVerifier := internaloidc.NewVerifier(verifier, verificationOptions) - - test, err := NewAuthOnlyEndpointTest("", func(opts *options.Options) { - opts.InjectRequestHeaders = []options.Header{ - { - Name: "Authorization", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "id_token", - Prefix: "Bearer ", - }, - }, - }, - }, - { - Name: "X-Forwarded-User", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Forwarded-Email", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - }, - }, - }, - }, - } - - opts.InjectResponseHeaders = []options.Header{ - { - Name: "Authorization", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "id_token", - Prefix: "Bearer ", - }, - }, - }, - }, - { - Name: "X-Auth-Request-User", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Email", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - }, - }, - }, - }, - } - opts.SkipJwtBearerTokens = true - opts.SetJWTBearerVerifiers(append(opts.GetJWTBearerVerifiers(), internalVerifier)) - }) - if err != nil { - t.Fatal(err) - } - tp, _ := test.proxy.provider.(*TestProvider) - tp.GroupValidator = func(s string) bool { - return true - } - - authHeader := fmt.Sprintf("Bearer %s", goodJwt) - test.req.Header = map[string][]string{ - "Authorization": {authHeader}, - } - - test.proxy.ServeHTTP(test.rw, test.req) - if test.rw.Code >= 400 { - t.Fatalf("expected 3xx got %d", test.rw.Code) - } - - // Check PassAuthorization, should overwrite Basic header - assert.Equal(t, test.req.Header.Get("Authorization"), authHeader) - assert.Equal(t, test.req.Header.Get("X-Forwarded-User"), "1234567890") - assert.Equal(t, test.req.Header.Get("X-Forwarded-Email"), "john@example.com") - - // SetAuthorization and SetXAuthRequest - assert.Equal(t, test.rw.Header().Get("Authorization"), authHeader) - assert.Equal(t, test.rw.Header().Get("X-Auth-Request-User"), "1234567890") - assert.Equal(t, test.rw.Header().Get("X-Auth-Request-Email"), "john@example.com") -} - -func Test_prepareNoCache(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - prepareNoCache(w) - }) - mux := http.NewServeMux() - mux.Handle("/", handler) - - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/", nil) - mux.ServeHTTP(rec, req) - - for k, v := range noCacheHeaders { - assert.Equal(t, rec.Header().Get(k), v) - } -} - -func Test_noCacheHeaders(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := w.Write([]byte("upstream")) - if err != nil { - t.Error(err) - } - })) - t.Cleanup(upstreamServer.Close) - - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - opts.SkipAuthRegex = []string{".*"} - err := validation.Validate(opts) - assert.NoError(t, err) - proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - t.Run("not exist in response from upstream", func(t *testing.T) { - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, "/upstream", nil) - proxy.ServeHTTP(rec, req) - - assert.Equal(t, http.StatusOK, rec.Code) - assert.Equal(t, "upstream", rec.Body.String()) - - // checking noCacheHeaders does not exists in response headers from upstream - for k := range noCacheHeaders { - assert.Equal(t, "", rec.Header().Get(k)) - } - }) - - t.Run("has no-cache", func(t *testing.T) { - tests := []struct { - path string - hasNoCache bool - }{ - { - path: "/oauth2/sign_in", - hasNoCache: true, - }, - { - path: "/oauth2/sign_out", - hasNoCache: true, - }, - { - path: "/oauth2/start", - hasNoCache: true, - }, - { - path: "/oauth2/callback", - hasNoCache: true, - }, - { - path: "/oauth2/auth", - hasNoCache: false, - }, - { - path: "/oauth2/userinfo", - hasNoCache: true, - }, - { - path: "/upstream", - hasNoCache: false, - }, - } - - for _, tt := range tests { - t.Run(tt.path, func(t *testing.T) { - rec := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, tt.path, nil) - proxy.ServeHTTP(rec, req) - cacheControl := rec.Result().Header.Get("Cache-Control") - if tt.hasNoCache != (strings.Contains(cacheControl, "no-cache")) { - t.Errorf(`unexpected "Cache-Control" header: %s`, cacheControl) - } - }) - } - - }) -} - -func baseTestOptions() *options.Options { - opts := options.NewOptions() - opts.Cookie.Secret = rawCookieSecret - opts.Providers[0].ID = "providerID" - opts.Providers[0].ClientID = clientID - opts.Providers[0].ClientSecret = clientSecret - opts.EmailDomains = []string{"*"} - - // Default injected headers for legacy configuration - opts.InjectRequestHeaders = []options.Header{ - { - Name: "Authorization", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - BasicAuthPassword: &options.SecretSource{ - Value: []byte(base64.StdEncoding.EncodeToString([]byte("This is a secure password"))), - }, - }, - }, - }, - }, - { - Name: "X-Forwarded-User", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Forwarded-Email", - Values: []options.HeaderValue{ - { - ClaimSource: &options.ClaimSource{ - Claim: "email", - }, - }, - }, - }, - } - - return opts -} - -func TestTrustedIPs(t *testing.T) { - tests := []struct { - name string - trustedIPs []string - reverseProxy bool - realClientIPHeader string - req *http.Request - expectTrusted bool - }{ - // Check unconfigured behavior. - { - name: "Default", - trustedIPs: nil, - reverseProxy: false, - realClientIPHeader: "X-Real-IP", // Default value - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - return req - }(), - expectTrusted: false, - }, - // Check using req.RemoteAddr (Options.ReverseProxy == false). - { - name: "WithRemoteAddr", - trustedIPs: []string{"127.0.0.1"}, - reverseProxy: false, - realClientIPHeader: "X-Real-IP", // Default value - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.RemoteAddr = "127.0.0.1:43670" - return req - }(), - expectTrusted: true, - }, - // Check ignores req.RemoteAddr match when behind a reverse proxy / missing header. - { - name: "IgnoresRemoteAddrInReverseProxyMode", - trustedIPs: []string{"127.0.0.1"}, - reverseProxy: true, - realClientIPHeader: "X-Real-IP", // Default value - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.RemoteAddr = "127.0.0.1:44324" - return req - }(), - expectTrusted: false, - }, - // Check successful trusting of localhost in IPv4. - { - name: "TrustsLocalhostInReverseProxyMode", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: true, - realClientIPHeader: "X-Forwarded-For", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Add("X-Forwarded-For", "127.0.0.1") - return req - }(), - expectTrusted: true, - }, - // Check successful trusting of localhost in IPv6. - { - name: "TrustsIP6LocalostInReverseProxyMode", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: true, - realClientIPHeader: "X-Forwarded-For", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Add("X-Forwarded-For", "::1") - return req - }(), - expectTrusted: true, - }, - // Check does not trust random IPv4 address. - { - name: "DoesNotTrustRandomIP4Address", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: true, - realClientIPHeader: "X-Forwarded-For", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Add("X-Forwarded-For", "12.34.56.78") - return req - }(), - expectTrusted: false, - }, - // Check does not trust random IPv6 address. - { - name: "DoesNotTrustRandomIP6Address", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: true, - realClientIPHeader: "X-Forwarded-For", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Add("X-Forwarded-For", "::2") - return req - }(), - expectTrusted: false, - }, - // Check respects correct header. - { - name: "RespectsCorrectHeaderInReverseProxyMode", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: true, - realClientIPHeader: "X-Forwarded-For", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Add("X-Real-IP", "::1") - return req - }(), - expectTrusted: false, - }, - // Check doesn't trust if garbage is provided. - { - name: "DoesNotTrustGarbageInReverseProxyMode", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: true, - realClientIPHeader: "X-Forwarded-For", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.Header.Add("X-Forwarded-For", "adsfljk29242as!!") - return req - }(), - expectTrusted: false, - }, - // Check doesn't trust if garbage is provided (no reverse-proxy). - { - name: "DoesNotTrustGarbage", - trustedIPs: []string{"127.0.0.0/8", "::1"}, - reverseProxy: false, - realClientIPHeader: "X-Real-IP", - req: func() *http.Request { - req, _ := http.NewRequest("GET", "/", nil) - req.RemoteAddr = "adsfljk29242as!!" - return req - }(), - expectTrusted: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: "static", - Path: "/", - Static: true, - }, - }, - } - opts.TrustedIPs = tt.trustedIPs - opts.ReverseProxy = tt.reverseProxy - opts.RealClientIPHeader = tt.realClientIPHeader - err := validation.Validate(opts) - assert.NoError(t, err) - - proxy, err := NewOAuthProxy(opts, func(string) bool { return true }) - assert.NoError(t, err) - rw := httptest.NewRecorder() - - proxy.ServeHTTP(rw, tt.req) - if tt.expectTrusted { - assert.Equal(t, 200, rw.Code) - } else { - assert.Equal(t, 403, rw.Code) - } - }) - } -} - -func Test_buildRoutesAllowlist(t *testing.T) { - type expectedAllowedRoute struct { - method string - negate bool - regexString string - } - - testCases := []struct { - name string - skipAuthRegex []string - skipAuthRoutes []string - expectedRoutes []expectedAllowedRoute - shouldError bool - }{ - { - name: "No skip auth configured", - skipAuthRegex: []string{}, - skipAuthRoutes: []string{}, - expectedRoutes: []expectedAllowedRoute{}, - shouldError: false, - }, - { - name: "Only skipAuthRegex configured", - skipAuthRegex: []string{ - "^/foo/bar", - "^/baz/[0-9]+/thing", - }, - skipAuthRoutes: []string{}, - expectedRoutes: []expectedAllowedRoute{ - { - method: "", - negate: false, - regexString: "^/foo/bar", - }, - { - method: "", - negate: false, - regexString: "^/baz/[0-9]+/thing", - }, - }, - shouldError: false, - }, - { - name: "Only skipAuthRoutes configured", - skipAuthRegex: []string{}, - skipAuthRoutes: []string{ - "GET=^/foo/bar", - "POST=^/baz/[0-9]+/thing", - "^/all/methods$", - "WEIRD=^/methods/are/allowed", - "PATCH=/second/equals?are=handled&just=fine", - "!=^/api", - "METHOD!=^/api", - }, - expectedRoutes: []expectedAllowedRoute{ - { - method: "GET", - negate: false, - regexString: "^/foo/bar", - }, - { - method: "POST", - negate: false, - regexString: "^/baz/[0-9]+/thing", - }, - { - method: "", - negate: false, - regexString: "^/all/methods$", - }, - { - method: "WEIRD", - negate: false, - regexString: "^/methods/are/allowed", - }, - { - method: "PATCH", - negate: false, - regexString: "/second/equals?are=handled&just=fine", - }, - { - method: "", - negate: true, - regexString: "^/api", - }, - { - method: "METHOD", - negate: true, - regexString: "^/api", - }, - }, - shouldError: false, - }, - { - name: "Both skipAuthRegexes and skipAuthRoutes configured", - skipAuthRegex: []string{ - "^/foo/bar/regex", - "^/baz/[0-9]+/thing/regex", - }, - skipAuthRoutes: []string{ - "GET=^/foo/bar", - "POST=^/baz/[0-9]+/thing", - "^/all/methods$", - }, - expectedRoutes: []expectedAllowedRoute{ - { - method: "", - regexString: "^/foo/bar/regex", - }, - { - method: "", - regexString: "^/baz/[0-9]+/thing/regex", - }, - { - method: "GET", - regexString: "^/foo/bar", - }, - { - method: "POST", - regexString: "^/baz/[0-9]+/thing", - }, - { - method: "", - regexString: "^/all/methods$", - }, - }, - shouldError: false, - }, - { - name: "Invalid skipAuthRegex entry", - skipAuthRegex: []string{ - "^/foo/bar", - "^/baz/[0-9]+/thing", - "(bad[regex", - }, - skipAuthRoutes: []string{}, - expectedRoutes: []expectedAllowedRoute{}, - shouldError: true, - }, - { - name: "Invalid skipAuthRoutes entry", - skipAuthRegex: []string{}, - skipAuthRoutes: []string{ - "GET=^/foo/bar", - "POST=^/baz/[0-9]+/thing", - "^/all/methods$", - "PUT=(bad[regex", - }, - expectedRoutes: []expectedAllowedRoute{}, - shouldError: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - opts := &options.Options{ - SkipAuthRegex: tc.skipAuthRegex, - SkipAuthRoutes: tc.skipAuthRoutes, - } - routes, err := buildRoutesAllowlist(opts) - if tc.shouldError { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - for i, route := range routes { - assert.Greater(t, len(tc.expectedRoutes), i) - assert.Equal(t, route.method, tc.expectedRoutes[i].method) - assert.Equal(t, route.negate, tc.expectedRoutes[i].negate) - assert.Equal(t, route.pathRegex.String(), tc.expectedRoutes[i].regexString) - } - }) - } -} - -func TestApiRoutes(t *testing.T) { - - ajaxAPIServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("AJAX API Request")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(ajaxAPIServer.Close) - - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("API Request")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(apiServer.Close) - - uiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("API Request")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(uiServer.Close) - - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: apiServer.URL, - Path: "/api", - URI: apiServer.URL, - }, - { - ID: ajaxAPIServer.URL, - Path: "/ajaxapi", - URI: ajaxAPIServer.URL, - }, - { - ID: uiServer.URL, - Path: "/ui", - URI: uiServer.URL, - }, - }, - } - opts.APIRoutes = []string{ - "^/api", - } - opts.SkipProviderButton = true - err := validation.Validate(opts) - assert.NoError(t, err) - proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - contentType string - url string - shouldRedirect bool - }{ - { - name: "AJAX request matching API regex", - contentType: "application/json", - url: "/api/v1/UserInfo", - shouldRedirect: false, - }, - { - name: "AJAX request not matching API regex", - contentType: "application/json", - url: "/ajaxapi/v1/UserInfo", - shouldRedirect: false, - }, - { - name: "Other Request matching API regex", - contentType: "application/grpcwebtext", - url: "/api/v1/UserInfo", - shouldRedirect: false, - }, - { - name: "UI request", - contentType: "html", - url: "/ui/index.html", - shouldRedirect: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest("GET", tc.url, nil) - req.Header.Set("Accept", tc.contentType) - assert.NoError(t, err) - - rw := httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - if tc.shouldRedirect { - assert.Equal(t, 302, rw.Code) - } else { - assert.Equal(t, 401, rw.Code) - } - }) - } -} - -func TestAllowedRequest(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("Allowed Request")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(upstreamServer.Close) - - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - opts.SkipAuthRegex = []string{ - "^/skip/auth/regex$", - } - opts.SkipAuthRoutes = []string{ - "GET=^/skip/auth/routes/get", - } - err := validation.Validate(opts) - assert.NoError(t, err) - proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - method string - url string - allowed bool - }{ - { - name: "Regex GET allowed", - method: "GET", - url: "/skip/auth/regex", - allowed: true, - }, - { - name: "Regex POST allowed ", - method: "POST", - url: "/skip/auth/regex", - allowed: true, - }, - { - name: "Regex denied", - method: "GET", - url: "/wrong/denied", - allowed: false, - }, - { - name: "Route allowed", - method: "GET", - url: "/skip/auth/routes/get", - allowed: true, - }, - { - name: "Route denied with wrong method", - method: "PATCH", - url: "/skip/auth/routes/get", - allowed: false, - }, - { - name: "Route denied with wrong path", - method: "GET", - url: "/skip/auth/routes/wrong/path", - allowed: false, - }, - { - name: "Route denied with wrong path and method", - method: "POST", - url: "/skip/auth/routes/wrong/path", - allowed: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest(tc.method, tc.url, nil) - assert.NoError(t, err) - assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req)) - - rw := httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - if tc.allowed { - assert.Equal(t, 200, rw.Code) - assert.Equal(t, "Allowed Request", rw.Body.String()) - } else { - assert.Equal(t, 403, rw.Code) - } - }) - } -} - -func TestAllowedRequestWithForwardedUriHeader(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - })) - t.Cleanup(upstreamServer.Close) - - opts := baseTestOptions() - opts.ReverseProxy = true - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - opts.SkipAuthRegex = []string{ - "^/skip/auth/regex$", - } - opts.SkipAuthRoutes = []string{ - "GET=^/skip/auth/routes/get", - } - err := validation.Validate(opts) - assert.NoError(t, err) - proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - method string - url string - allowed bool - }{ - { - name: "Regex GET allowed", - method: "GET", - url: "/skip/auth/regex", - allowed: true, - }, - { - name: "Regex POST allowed ", - method: "POST", - url: "/skip/auth/regex", - allowed: true, - }, - { - name: "Regex denied", - method: "GET", - url: "/wrong/denied", - allowed: false, - }, - { - name: "Route allowed", - method: "GET", - url: "/skip/auth/routes/get", - allowed: true, - }, - { - name: "Route denied with wrong method", - method: "PATCH", - url: "/skip/auth/routes/get", - allowed: false, - }, - { - name: "Route denied with wrong path", - method: "GET", - url: "/skip/auth/routes/wrong/path", - allowed: false, - }, - { - name: "Route denied with wrong path and method", - method: "POST", - url: "/skip/auth/routes/wrong/path", - allowed: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest(tc.method, opts.ProxyPrefix+authOnlyPath, nil) - req.Header.Set("X-Forwarded-Uri", tc.url) - assert.NoError(t, err) - - rw := httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - if tc.allowed { - assert.Equal(t, 202, rw.Code) - } else { - assert.Equal(t, 401, rw.Code) - } - }) - } -} - -func TestAllowedRequestNegateWithoutMethod(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("Allowed Request")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(upstreamServer.Close) - - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - opts.SkipAuthRoutes = []string{ - "!=^/api", // any non-api routes - "POST=^/api/public-entity/?$", - } - err := validation.Validate(opts) - assert.NoError(t, err) - proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - method string - url string - allowed bool - }{ - { - name: "Some static file allowed", - method: "GET", - url: "/static/file.txt", - allowed: true, - }, - { - name: "POST to contact form allowed", - method: "POST", - url: "/contact", - allowed: true, - }, - { - name: "Regex POST allowed", - method: "POST", - url: "/api/public-entity", - allowed: true, - }, - { - name: "Regex POST with trailing slash allowed", - method: "POST", - url: "/api/public-entity/", - allowed: true, - }, - { - name: "Regex GET api route denied", - method: "GET", - url: "/api/users", - allowed: false, - }, - { - name: "Regex POST api route denied", - method: "POST", - url: "/api/users", - allowed: false, - }, - { - name: "Regex DELETE api route denied", - method: "DELETE", - url: "/api/users/1", - allowed: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest(tc.method, tc.url, nil) - assert.NoError(t, err) - assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req)) - - rw := httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - if tc.allowed { - assert.Equal(t, 200, rw.Code) - assert.Equal(t, "Allowed Request", rw.Body.String()) - } else { - assert.Equal(t, 403, rw.Code) - } - }) - } -} - -func TestAllowedRequestNegateWithMethod(t *testing.T) { - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - _, err := w.Write([]byte("Allowed Request")) - if err != nil { - t.Fatal(err) - } - })) - t.Cleanup(upstreamServer.Close) - - opts := baseTestOptions() - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - opts.SkipAuthRoutes = []string{ - "GET!=^/api", // any non-api routes - "POST=^/api/public-entity/?$", - } - err := validation.Validate(opts) - assert.NoError(t, err) - proxy, err := NewOAuthProxy(opts, func(_ string) bool { return true }) - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - method string - url string - allowed bool - }{ - { - name: "Some static file allowed", - method: "GET", - url: "/static/file.txt", - allowed: true, - }, - { - name: "POST to contact form not allowed", - method: "POST", - url: "/contact", - allowed: false, - }, - { - name: "Regex POST allowed", - method: "POST", - url: "/api/public-entity", - allowed: true, - }, - { - name: "Regex POST with trailing slash allowed", - method: "POST", - url: "/api/public-entity/", - allowed: true, - }, - { - name: "Regex GET api route denied", - method: "GET", - url: "/api/users", - allowed: false, - }, - { - name: "Regex POST api route denied", - method: "POST", - url: "/api/users", - allowed: false, - }, - { - name: "Regex DELETE api route denied", - method: "DELETE", - url: "/api/users/1", - allowed: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - req, err := http.NewRequest(tc.method, tc.url, nil) - assert.NoError(t, err) - assert.Equal(t, tc.allowed, proxy.isAllowedRoute(req)) - - rw := httptest.NewRecorder() - proxy.ServeHTTP(rw, req) - - if tc.allowed { - assert.Equal(t, 200, rw.Code) - assert.Equal(t, "Allowed Request", rw.Body.String()) - } else { - assert.Equal(t, 403, rw.Code) - } - }) - } -} - -func TestProxyAllowedGroups(t *testing.T) { - tests := []struct { - name string - allowedGroups []string - groups []string - expectUnauthorized bool - }{ - {"NoAllowedGroups", []string{}, []string{}, false}, - {"NoAllowedGroupsUserHasGroups", []string{}, []string{"a", "b"}, false}, - {"UserInAllowedGroup", []string{"a"}, []string{"a", "b"}, false}, - {"UserNotInAllowedGroup", []string{"a"}, []string{"c"}, true}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - emailAddress := "test" - created := time.Now() - - session := &sessions.SessionState{ - Groups: tt.groups, - Email: emailAddress, - AccessToken: "oauth_token", - CreatedAt: &created, - } - - upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - })) - t.Cleanup(upstreamServer.Close) - - test, err := NewProcessCookieTestWithOptionsModifiers(func(opts *options.Options) { - opts.Providers[0].AllowedGroups = tt.allowedGroups - opts.UpstreamServers = options.UpstreamConfig{ - Upstreams: []options.Upstream{ - { - ID: upstreamServer.URL, - Path: "/", - URI: upstreamServer.URL, - }, - }, - } - }) - if err != nil { - t.Fatal(err) - } - - test.req, _ = http.NewRequest("GET", "/", nil) - - test.req.Header.Add("accept", applicationJSON) - err = test.SaveSession(session) - assert.NoError(t, err) - test.proxy.ServeHTTP(test.rw, test.req) - - if tt.expectUnauthorized { - assert.Equal(t, http.StatusForbidden, test.rw.Code) - } else { - assert.Equal(t, http.StatusOK, test.rw.Code) - } - }) - } -} - -func TestAuthOnlyAllowedGroups(t *testing.T) { - testCases := []struct { - name string - allowedGroups []string - groups []string - querystring string - expectedStatusCode int - }{ - { - name: "NoAllowedGroups", - allowedGroups: []string{}, - groups: []string{}, - querystring: "", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "NoAllowedGroupsUserHasGroups", - allowedGroups: []string{}, - groups: []string{"a", "b"}, - querystring: "", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInAllowedGroup", - allowedGroups: []string{"a"}, - groups: []string{"a", "b"}, - querystring: "", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserNotInAllowedGroup", - allowedGroups: []string{"a"}, - groups: []string{"c"}, - querystring: "", - expectedStatusCode: http.StatusUnauthorized, - }, - { - name: "UserInQuerystringGroup", - allowedGroups: []string{"a", "b"}, - groups: []string{"a", "c"}, - querystring: "?allowed_groups=a", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInMultiParamQuerystringGroup", - allowedGroups: []string{"a", "b"}, - groups: []string{"b"}, - querystring: "?allowed_groups=a&allowed_groups=b,d", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInOnlyQuerystringGroup", - allowedGroups: []string{}, - groups: []string{"a", "c"}, - querystring: "?allowed_groups=a,b", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInDelimitedQuerystringGroup", - allowedGroups: []string{"a", "b", "c"}, - groups: []string{"c"}, - querystring: "?allowed_groups=a,c", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserNotInQuerystringGroup", - allowedGroups: []string{}, - groups: []string{"c"}, - querystring: "?allowed_groups=a,b", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserInConfigGroupNotInQuerystringGroup", - allowedGroups: []string{"a", "b", "c"}, - groups: []string{"c"}, - querystring: "?allowed_groups=a,b", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserInQuerystringGroupNotInConfigGroup", - allowedGroups: []string{"a", "b"}, - groups: []string{"c"}, - querystring: "?allowed_groups=b,c", - expectedStatusCode: http.StatusUnauthorized, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - emailAddress := "test" - created := time.Now() - - session := &sessions.SessionState{ - Groups: tc.groups, - Email: emailAddress, - AccessToken: "oauth_token", - CreatedAt: &created, - } - - test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) { - opts.Providers[0].AllowedGroups = tc.allowedGroups - }) - if err != nil { - t.Fatal(err) - } - - err = test.SaveSession(session) - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - - assert.Equal(t, tc.expectedStatusCode, test.rw.Code) - }) - } -} - -func TestAuthOnlyAllowedGroupsWithSkipMethods(t *testing.T) { - testCases := []struct { - name string - groups []string - method string - ip string - withSession bool - expectedStatusCode int - }{ - { - name: "UserWithGroupSkipAuthPreflight", - groups: []string{"a", "c"}, - method: "OPTIONS", - ip: "1.2.3.5:43670", - withSession: true, - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserWithGroupTrustedIp", - groups: []string{"a", "c"}, - method: "GET", - ip: "1.2.3.4:43670", - withSession: true, - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserWithoutGroupSkipAuthPreflight", - groups: []string{"c"}, - method: "OPTIONS", - ip: "1.2.3.5:43670", - withSession: true, - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserWithoutGroupTrustedIp", - groups: []string{"c"}, - method: "GET", - ip: "1.2.3.4:43670", - withSession: true, - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserWithoutSessionSkipAuthPreflight", - method: "OPTIONS", - ip: "1.2.3.5:43670", - withSession: false, - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserWithoutSessionTrustedIp", - method: "GET", - ip: "1.2.3.4:43670", - withSession: false, - expectedStatusCode: http.StatusAccepted, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - test, err := NewAuthOnlyEndpointTest("?allowed_groups=a,b", func(opts *options.Options) { - opts.SkipAuthPreflight = true - opts.TrustedIPs = []string{"1.2.3.4"} - }) - if err != nil { - t.Fatal(err) - } - - test.req.Method = tc.method - test.req.RemoteAddr = tc.ip - - if tc.withSession { - created := time.Now() - session := &sessions.SessionState{ - Groups: tc.groups, - Email: "test", - AccessToken: "oauth_token", - CreatedAt: &created, - } - err = test.SaveSession(session) - } - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - - assert.Equal(t, tc.expectedStatusCode, test.rw.Code) - }) - } -} - -func TestAuthOnlyAllowedEmailDomains(t *testing.T) { - testCases := []struct { - name string - email string - querystring string - expectedStatusCode int - }{ - { - name: "NotEmailRestriction", - email: "toto@example.com", - querystring: "", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInAllowedEmailDomain", - email: "toto@example.com", - querystring: "?allowed_email_domains=example.com", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserNotInAllowedEmailDomain", - email: "toto@example.com", - querystring: "?allowed_email_domains=a.example.com", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserNotInAllowedEmailDomains", - email: "toto@example.com", - querystring: "?allowed_email_domains=a.example.com,b.example.com", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserInAllowedEmailDomains", - email: "toto@example.com", - querystring: "?allowed_email_domains=a.example.com,example.com", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInAllowedEmailDomainWildcard", - email: "toto@foo.example.com", - querystring: "?allowed_email_domains=*.example.com", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserNotInAllowedEmailDomainWildcard", - email: "toto@example.com", - querystring: "?allowed_email_domains=*.a.example.com", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserInAllowedEmailDomainsWildcard", - email: "toto@example.com", - querystring: "?allowed_email_domains=*.a.example.com,*.b.example.com", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserInAllowedEmailDomainsWildcard", - email: "toto@c.example.com", - querystring: "?allowed_email_domains=a.b.c.example.com,*.c.example.com", - expectedStatusCode: http.StatusAccepted, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - groups := []string{} - - created := time.Now() - - session := &sessions.SessionState{ - Groups: groups, - Email: tc.email, - AccessToken: "oauth_token", - CreatedAt: &created, - } - - test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {}) - if err != nil { - t.Fatal(err) - } - - err = test.SaveSession(session) - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - - assert.Equal(t, tc.expectedStatusCode, test.rw.Code) - }) - } -} - -func TestStateEncodesCorrectly(t *testing.T) { - state := "some_state_to_test" - nonce := "some_nonce_to_test" - - encodedResult := encodeState(nonce, state, true) - assert.Equal(t, "c29tZV9ub25jZV90b190ZXN0OnNvbWVfc3RhdGVfdG9fdGVzdA", encodedResult) - - notEncodedResult := encodeState(nonce, state, false) - assert.Equal(t, "some_nonce_to_test:some_state_to_test", notEncodedResult) -} - -func TestStateDecodesCorrectly(t *testing.T) { - nonce, redirect, _ := decodeState("c29tZV9ub25jZV90b190ZXN0OnNvbWVfc3RhdGVfdG9fdGVzdA", true) - - assert.Equal(t, "some_nonce_to_test", nonce) - assert.Equal(t, "some_state_to_test", redirect) - - nonce2, redirect2, _ := decodeState("some_nonce_to_test:some_state_to_test", false) - - assert.Equal(t, "some_nonce_to_test", nonce2) - assert.Equal(t, "some_state_to_test", redirect2) -} - -func TestAuthOnlyAllowedEmails(t *testing.T) { - testCases := []struct { - name string - email string - querystring string - expectedStatusCode int - }{ - { - name: "NotEmailRestriction", - email: "toto@example.com", - querystring: "", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserInAllowedEmail", - email: "toto@example.com", - querystring: "?allowed_emails=toto@example.com", - expectedStatusCode: http.StatusAccepted, - }, - { - name: "UserNotInAllowedEmail", - email: "toto@example.com", - querystring: "?allowed_emails=tete@example.com", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserNotInAllowedEmails", - email: "toto@example.com", - querystring: "?allowed_emails=tete@example.com,tutu@example.com", - expectedStatusCode: http.StatusForbidden, - }, - { - name: "UserInAllowedEmails", - email: "toto@example.com", - querystring: "?allowed_emails=tete@example.com,toto@example.com", - expectedStatusCode: http.StatusAccepted, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - groups := []string{} - - created := time.Now() - - session := &sessions.SessionState{ - Groups: groups, - Email: tc.email, - AccessToken: "oauth_token", - CreatedAt: &created, - } - - test, err := NewAuthOnlyEndpointTest(tc.querystring, func(opts *options.Options) {}) - if err != nil { - t.Fatal(err) - } - - err = test.SaveSession(session) - assert.NoError(t, err) - - test.proxy.ServeHTTP(test.rw, test.req) - - assert.Equal(t, tc.expectedStatusCode, test.rw.Code) - }) - } -} - -func TestGetOAuthRedirectURI(t *testing.T) { - tests := []struct { - name string - setupOpts func(*options.Options) *options.Options - req *http.Request - want string - }{ - { - name: "redirect with https schema", - setupOpts: func(baseOpts *options.Options) *options.Options { - return baseOpts - }, - req: &http.Request{ - Host: "example", - URL: &url.URL{ - Scheme: schemeHTTPS, - }, - }, - want: "https://example/oauth2/callback", - }, - { - name: "redirect with http schema", - setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.Cookie.Secure = false - return baseOpts - }, - req: &http.Request{ - Host: "example", - URL: &url.URL{ - Scheme: schemeHTTP, - }, - }, - want: "http://example/oauth2/callback", - }, - { - name: "relative redirect url", - setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.RelativeRedirectURL = true - return baseOpts - }, - req: &http.Request{}, - want: "/oauth2/callback", - }, - { - name: "proxy prefix", - setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.ProxyPrefix = "/prefix" - return baseOpts - }, - req: &http.Request{ - Host: "example", - URL: &url.URL{ - Scheme: schemeHTTP, - }, - }, - want: "https://example/prefix/callback", - }, - { - name: "proxy prefix with relative redirect", - setupOpts: func(baseOpts *options.Options) *options.Options { - baseOpts.ProxyPrefix = "/prefix" - baseOpts.RelativeRedirectURL = true - return baseOpts - }, - req: &http.Request{ - Host: "example", - URL: &url.URL{ - Scheme: schemeHTTP, - }, - }, - want: "/prefix/callback", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - baseOpts := baseTestOptions() - err := validation.Validate(baseOpts) - assert.NoError(t, err) - - proxy, err := NewOAuthProxy(tt.setupOpts(baseOpts), func(string) bool { return true }) - assert.NoError(t, err) - - assert.Equalf(t, tt.want, proxy.getOAuthRedirectURI(tt.req), "getOAuthRedirectURI(%v)", tt.req) - }) - } -} diff --git a/pkg/apis/middleware/middleware_suite_test.go b/pkg/apis/middleware/middleware_suite_test.go deleted file mode 100644 index 81833e999c..0000000000 --- a/pkg/apis/middleware/middleware_suite_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package middleware_test - -import ( - "testing" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -// TestMiddlewareSuite and related tests are in a *_test package -// to prevent circular imports with the `logger` package which uses -// this functionality -func TestMiddlewareSuite(t *testing.T) { - logger.SetOutput(GinkgoWriter) - logger.SetErrOutput(GinkgoWriter) - - RegisterFailHandler(Fail) - RunSpecs(t, "Middleware API") -} diff --git a/pkg/apis/middleware/scope.go b/pkg/apis/middleware/scope.go index 2d84f00ec0..ea3ea5059c 100644 --- a/pkg/apis/middleware/scope.go +++ b/pkg/apis/middleware/scope.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + "github.com/higress-group/oauth2-proxy/pkg/apis/sessions" ) type scopeKey string @@ -38,9 +38,6 @@ type RequestScope struct { // SessionRevalidated indicates whether the session has been revalidated since // it was loaded or not. SessionRevalidated bool - - // Upstream tracks which upstream was used for this request - Upstream string } // GetRequestScope returns the current request scope from the given request diff --git a/pkg/apis/middleware/scope_test.go b/pkg/apis/middleware/scope_test.go deleted file mode 100644 index 355365bf0a..0000000000 --- a/pkg/apis/middleware/scope_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package middleware_test - -import ( - "net/http" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Scope Suite", func() { - Context("GetRequestScope", func() { - var request *http.Request - - BeforeEach(func() { - var err error - request, err = http.NewRequest("", "http://127.0.0.1/", nil) - Expect(err).ToNot(HaveOccurred()) - }) - - Context("with a scope", func() { - var scope *middleware.RequestScope - - BeforeEach(func() { - scope = &middleware.RequestScope{} - request = middleware.AddRequestScope(request, scope) - }) - - It("returns the scope", func() { - s := middleware.GetRequestScope(request) - Expect(s).ToNot(BeNil()) - Expect(s).To(Equal(scope)) - }) - - Context("if the scope is then modified", func() { - BeforeEach(func() { - Expect(scope.SaveSession).To(BeFalse()) - scope.SaveSession = true - }) - - It("returns the updated session", func() { - s := middleware.GetRequestScope(request) - Expect(s).ToNot(BeNil()) - Expect(s).To(Equal(scope)) - Expect(s.SaveSession).To(BeTrue()) - }) - }) - }) - - Context("without a scope", func() { - It("returns nil", func() { - Expect(middleware.GetRequestScope(request)).To(BeNil()) - }) - }) - }) -}) diff --git a/pkg/apis/middleware/session.go b/pkg/apis/middleware/session.go index 9fcd974b89..9fa92ff0ad 100644 --- a/pkg/apis/middleware/session.go +++ b/pkg/apis/middleware/session.go @@ -4,8 +4,9 @@ import ( "context" "fmt" - "github.com/coreos/go-oidc/v3/oidc" - sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" + sessionsapi "github.com/higress-group/oauth2-proxy/pkg/apis/sessions" + + oidc "github.com/higress-group/oauth2-proxy/pkg/providers/go_oidc" ) // TokenToSessionFunc takes a raw ID Token and converts it into a SessionState. diff --git a/pkg/apis/options/alpha_options.go b/pkg/apis/options/alpha_options.go deleted file mode 100644 index 04769d7f3d..0000000000 --- a/pkg/apis/options/alpha_options.go +++ /dev/null @@ -1,68 +0,0 @@ -package options - -// AlphaOptions contains alpha structured configuration options. -// Usage of these options allows users to access alpha features that are not -// available as part of the primary configuration structure for OAuth2 Proxy. -// -// :::warning -// The options within this structure are considered alpha. -// They may change between releases without notice. -// ::: -type AlphaOptions struct { - // UpstreamConfig is used to configure upstream servers. - // Once a user is authenticated, requests to the server will be proxied to - // these upstream servers based on the path mappings defined in this list. - UpstreamConfig UpstreamConfig `json:"upstreamConfig,omitempty"` - - // InjectRequestHeaders is used to configure headers that should be added - // to requests to upstream servers. - // Headers may source values from either the authenticated user's session - // or from a static secret value. - InjectRequestHeaders []Header `json:"injectRequestHeaders,omitempty"` - - // InjectResponseHeaders is used to configure headers that should be added - // to responses from the proxy. - // This is typically used when using the proxy as an external authentication - // provider in conjunction with another proxy such as NGINX and its - // auth_request module. - // Headers may source values from either the authenticated user's session - // or from a static secret value. - InjectResponseHeaders []Header `json:"injectResponseHeaders,omitempty"` - - // Server is used to configure the HTTP(S) server for the proxy application. - // You may choose to run both HTTP and HTTPS servers simultaneously. - // This can be done by setting the BindAddress and the SecureBindAddress simultaneously. - // To use the secure server you must configure a TLS certificate and key. - Server Server `json:"server,omitempty"` - - // MetricsServer is used to configure the HTTP(S) server for metrics. - // You may choose to run both HTTP and HTTPS servers simultaneously. - // This can be done by setting the BindAddress and the SecureBindAddress simultaneously. - // To use the secure server you must configure a TLS certificate and key. - MetricsServer Server `json:"metricsServer,omitempty"` - - // Providers is used to configure multiple providers. - Providers Providers `json:"providers,omitempty"` -} - -// MergeInto replaces alpha options in the Options struct with the values -// from the AlphaOptions -func (a *AlphaOptions) MergeInto(opts *Options) { - opts.UpstreamServers = a.UpstreamConfig - opts.InjectRequestHeaders = a.InjectRequestHeaders - opts.InjectResponseHeaders = a.InjectResponseHeaders - opts.Server = a.Server - opts.MetricsServer = a.MetricsServer - opts.Providers = a.Providers -} - -// ExtractFrom populates the fields in the AlphaOptions with the values from -// the Options -func (a *AlphaOptions) ExtractFrom(opts *Options) { - a.UpstreamConfig = opts.UpstreamServers - a.InjectRequestHeaders = opts.InjectRequestHeaders - a.InjectResponseHeaders = opts.InjectResponseHeaders - a.Server = opts.Server - a.MetricsServer = opts.MetricsServer - a.Providers = opts.Providers -} diff --git a/pkg/apis/options/app.go b/pkg/apis/options/app.go deleted file mode 100644 index 57f5b93523..0000000000 --- a/pkg/apis/options/app.go +++ /dev/null @@ -1,58 +0,0 @@ -package options - -import "github.com/spf13/pflag" - -// Templates includes options for configuring the sign in and error pages -// appearance. -type Templates struct { - // Path is the path to a folder containing a sign_in.html and an error.html - // template. - // These files will be used instead of the default templates if present. - // If either file is missing, the default will be used instead. - Path string `flag:"custom-templates-dir" cfg:"custom_templates_dir"` - - // CustomLogo is the path or a URL to a logo that should replace the default logo - // on the sign_in page template. - // Supported formats are .svg, .png, .jpg and .jpeg. - // If URL is used the format support depends on the browser. - // To disable the default logo, set this value to "-". - CustomLogo string `flag:"custom-sign-in-logo" cfg:"custom_sign_in_logo"` - - // Banner overides the default sign_in page banner text. If unspecified, - // the message will give users a list of allowed email domains. - Banner string `flag:"banner" cfg:"banner"` - - // Footer overrides the default sign_in page footer text. - Footer string `flag:"footer" cfg:"footer"` - - // DisplayLoginForm determines whether the sign_in page should render a - // password form if a static passwords file (htpasswd file) has been - // configured. - DisplayLoginForm bool `flag:"display-htpasswd-form" cfg:"display_htpasswd_form"` - - // Debug renders detailed errors when an error page is shown. - // It is not advised to use this in production as errors may contain sensitive - // information. - // Use only for diagnosing backend errors. - Debug bool `flag:"show-debug-on-error" cfg:"show_debug_on_error"` -} - -func templatesFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("templates", pflag.ExitOnError) - - flagSet.String("custom-templates-dir", "", "path to custom html templates") - flagSet.String("custom-sign-in-logo", "", "path or URL to an custom image for the sign_in page logo. Use \"-\" to disable default logo.") - flagSet.String("banner", "", "custom banner string. Use \"-\" to disable default banner.") - flagSet.String("footer", "", "custom footer string. Use \"-\" to disable default footer.") - flagSet.Bool("display-htpasswd-form", true, "display username / password login form if an htpasswd file is provided") - flagSet.Bool("show-debug-on-error", false, "show detailed error information on error pages (WARNING: this may contain sensitive information - do not use in production)") - - return flagSet -} - -// templatesDefaults creates a Templates and populates it with any default values -func templatesDefaults() Templates { - return Templates{ - DisplayLoginForm: true, - } -} diff --git a/pkg/apis/options/common.go b/pkg/apis/options/common.go deleted file mode 100644 index 88d24d82ba..0000000000 --- a/pkg/apis/options/common.go +++ /dev/null @@ -1,63 +0,0 @@ -package options - -import ( - "fmt" - "strconv" - "time" -) - -// SecretSource references an individual secret value. -// Only one source within the struct should be defined at any time. -type SecretSource struct { - // Value expects a base64 encoded string value. - Value []byte `json:"value,omitempty"` - - // FromEnv expects the name of an environment variable. - FromEnv string `json:"fromEnv,omitempty"` - - // FromFile expects a path to a file containing the secret value. - FromFile string `json:"fromFile,omitempty"` -} - -// Duration is an alias for time.Duration so that we can ensure the marshalling -// and unmarshalling of string durations is done as users expect. -// Intentional blank line below to keep this first part of the comment out of -// any generated references. - -// Duration is as string representation of a period of time. -// A duration string is a is a possibly signed sequence of decimal numbers, -// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". -// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -// +reference-gen:alias-name=string -type Duration time.Duration - -// UnmarshalJSON parses the duration string and sets the value of duration -// to the value of the duration string. -func (d *Duration) UnmarshalJSON(data []byte) error { - input := string(data) - if unquoted, err := strconv.Unquote(input); err == nil { - input = unquoted - } - - du, err := time.ParseDuration(input) - if err != nil { - return err - } - *d = Duration(du) - return nil -} - -// MarshalJSON ensures that when the string is marshalled to JSON as a human -// readable string. -func (d *Duration) MarshalJSON() ([]byte, error) { - dStr := fmt.Sprintf("%q", d.Duration().String()) - return []byte(dStr), nil -} - -// Duration returns the time.Duration version of this Duration -func (d *Duration) Duration() time.Duration { - if d == nil { - return time.Duration(0) - } - return time.Duration(*d) -} diff --git a/pkg/apis/options/common_test.go b/pkg/apis/options/common_test.go deleted file mode 100644 index 8fc4176b27..0000000000 --- a/pkg/apis/options/common_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package options - -import ( - "encoding/json" - "errors" - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("Common", func() { - Context("Duration", func() { - type marshalJSONTableInput struct { - duration Duration - expectedJSON string - } - - DescribeTable("MarshalJSON", - func(in marshalJSONTableInput) { - data, err := in.duration.MarshalJSON() - Expect(err).ToNot(HaveOccurred()) - Expect(string(data)).To(Equal(in.expectedJSON)) - - var d Duration - Expect(json.Unmarshal(data, &d)).To(Succeed()) - Expect(d).To(Equal(in.duration)) - }, - Entry("30 seconds", marshalJSONTableInput{ - duration: Duration(30 * time.Second), - expectedJSON: "\"30s\"", - }), - Entry("1 minute", marshalJSONTableInput{ - duration: Duration(1 * time.Minute), - expectedJSON: "\"1m0s\"", - }), - Entry("1 hour 15 minutes", marshalJSONTableInput{ - duration: Duration(75 * time.Minute), - expectedJSON: "\"1h15m0s\"", - }), - Entry("A zero Duration", marshalJSONTableInput{ - duration: Duration(0), - expectedJSON: "\"0s\"", - }), - ) - - type unmarshalJSONTableInput struct { - json string - expectedErr error - expectedDuration Duration - } - - DescribeTable("UnmarshalJSON", - func(in unmarshalJSONTableInput) { - // A duration must be initialised pointer before UnmarshalJSON will work. - zero := Duration(0) - d := &zero - - err := d.UnmarshalJSON([]byte(in.json)) - if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) - } else { - Expect(err).ToNot(HaveOccurred()) - } - Expect(d).ToNot(BeNil()) - Expect(*d).To(Equal(in.expectedDuration)) - }, - Entry("1m", unmarshalJSONTableInput{ - json: "\"1m\"", - expectedDuration: Duration(1 * time.Minute), - }), - Entry("30s", unmarshalJSONTableInput{ - json: "\"30s\"", - expectedDuration: Duration(30 * time.Second), - }), - Entry("1h15m", unmarshalJSONTableInput{ - json: "\"1h15m\"", - expectedDuration: Duration(75 * time.Minute), - }), - Entry("am", unmarshalJSONTableInput{ - json: "\"am\"", - expectedErr: errors.New("time: invalid duration \"am\""), - expectedDuration: Duration(0), - }), - ) - }) -}) diff --git a/pkg/apis/options/cookie.go b/pkg/apis/options/cookie.go index 6917bdc570..3af74727c4 100644 --- a/pkg/apis/options/cookie.go +++ b/pkg/apis/options/cookie.go @@ -2,40 +2,21 @@ package options import ( "time" - - "github.com/spf13/pflag" ) // Cookie contains configuration options relating to Cookie configuration type Cookie struct { - Name string `flag:"cookie-name" cfg:"cookie_name"` - Secret string `flag:"cookie-secret" cfg:"cookie_secret"` - Domains []string `flag:"cookie-domain" cfg:"cookie_domains"` - Path string `flag:"cookie-path" cfg:"cookie_path"` - Expire time.Duration `flag:"cookie-expire" cfg:"cookie_expire"` - Refresh time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"` - Secure bool `flag:"cookie-secure" cfg:"cookie_secure"` - HTTPOnly bool `flag:"cookie-httponly" cfg:"cookie_httponly"` - SameSite string `flag:"cookie-samesite" cfg:"cookie_samesite"` - CSRFPerRequest bool `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"` - CSRFExpire time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"` -} - -func cookieFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("cookie", pflag.ExitOnError) - - flagSet.String("cookie-name", "_oauth2_proxy", "the name of the cookie that the oauth_proxy creates") - flagSet.String("cookie-secret", "", "the seed string for secure cookies (optionally base64 encoded)") - flagSet.StringSlice("cookie-domain", []string{}, "Optional cookie domains to force cookies to (ie: `.yourcompany.com`). The longest domain matching the request's host will be used (or the shortest cookie domain if there is no match).") - flagSet.String("cookie-path", "/", "an optional cookie path to force cookies to (ie: /poc/)*") - flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie") - flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable") - flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") - flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") - flagSet.String("cookie-samesite", "", "set SameSite cookie attribute (ie: \"lax\", \"strict\", \"none\", or \"\"). ") - flagSet.Bool("cookie-csrf-per-request", false, "When this property is set to true, then the CSRF cookie name is built based on the state and varies per request. If property is set to false, then CSRF cookie has the same name for all requests.") - flagSet.Duration("cookie-csrf-expire", time.Duration(15)*time.Minute, "expire timeframe for CSRF cookie") - return flagSet + Name string `mapstructure:"cookie_name"` + Secret string `mapstructure:"cookie_secret"` + Domains []string `mapstructure:"cookie_domains"` + Path string `mapstructure:"cookie_path"` + Expire time.Duration `mapstructure:"cookie_expire"` + Refresh time.Duration `mapstructure:"cookie_refresh"` + Secure bool `mapstructure:"cookie_secure"` + HTTPOnly bool `mapstructure:"cookie_httponly"` + SameSite string `mapstructure:"cookie_samesite"` + CSRFPerRequest bool `mapstructure:"cookie_csrf_per_request"` + CSRFExpire time.Duration `mapstructure:"cookie_csrf_expire"` } // cookieDefaults creates a Cookie populating each field with its default value @@ -45,11 +26,11 @@ func cookieDefaults() Cookie { Secret: "", Domains: nil, Path: "/", - Expire: time.Duration(168) * time.Hour, - Refresh: time.Duration(0), - Secure: true, + Expire: time.Duration(24) * time.Hour, + Refresh: time.Duration(1) * time.Hour, + Secure: false, HTTPOnly: true, - SameSite: "", + SameSite: "lax", CSRFPerRequest: false, CSRFExpire: time.Duration(15) * time.Minute, } diff --git a/pkg/apis/options/doc.go b/pkg/apis/options/doc.go deleted file mode 100644 index 8209369a46..0000000000 --- a/pkg/apis/options/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -//go:generate go run github.com/oauth2-proxy/tools/reference-gen/cmd/reference-gen --package github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options --types AlphaOptions --header-file ../../../docs/docs/configuration/alpha_config.md.tmpl --out-file ../../../docs/docs/configuration/alpha_config.md -package options diff --git a/pkg/apis/options/header.go b/pkg/apis/options/header.go deleted file mode 100644 index 8795665ccf..0000000000 --- a/pkg/apis/options/header.go +++ /dev/null @@ -1,44 +0,0 @@ -package options - -// Header represents an individual header that will be added to a request or -// response header. -type Header struct { - // Name is the header name to be used for this set of values. - // Names should be unique within a list of Headers. - Name string `json:"name,omitempty"` - - // PreserveRequestValue determines whether any values for this header - // should be preserved for the request to the upstream server. - // This option only applies to injected request headers. - // Defaults to false (headers that match this header will be stripped). - PreserveRequestValue bool `json:"preserveRequestValue,omitempty"` - - // Values contains the desired values for this header - Values []HeaderValue `json:"values,omitempty"` -} - -// HeaderValue represents a single header value and the sources that can -// make up the header value -type HeaderValue struct { - // Allow users to load the value from a secret source - *SecretSource `json:",omitempty"` - - // Allow users to load the value from a session claim - *ClaimSource `json:",omitempty"` -} - -// ClaimSource allows loading a header value from a claim within the session -type ClaimSource struct { - // Claim is the name of the claim in the session that the value should be - // loaded from. - Claim string `json:"claim,omitempty"` - - // Prefix is an optional prefix that will be prepended to the value of the - // claim if it is non-empty. - Prefix string `json:"prefix,omitempty"` - - // BasicAuthPassword converts this claim into a basic auth header. - // Note the value of claim will become the basic auth username and the - // basicAuthPassword will be used as the password value. - BasicAuthPassword *SecretSource `json:"basicAuthPassword,omitempty"` -} diff --git a/pkg/apis/options/legacy_options.go b/pkg/apis/options/legacy_options.go index bc48e631b0..05fb4fd437 100644 --- a/pkg/apis/options/legacy_options.go +++ b/pkg/apis/options/legacy_options.go @@ -2,92 +2,23 @@ package options import ( "fmt" - "net/url" - "reflect" - "strconv" - "strings" - "time" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - "github.com/spf13/pflag" ) type LegacyOptions struct { - // Legacy options related to upstream servers - LegacyUpstreams LegacyUpstreams `cfg:",squash"` - - // Legacy options for injecting request/response headers - LegacyHeaders LegacyHeaders `cfg:",squash"` - - // Legacy options for the server address and TLS - LegacyServer LegacyServer `cfg:",squash"` - // Legacy options for single provider - LegacyProvider LegacyProvider `cfg:",squash"` + LegacyProvider LegacyProvider `mapstructure:",squash"` - Options Options `cfg:",squash"` + Options Options `mapstructure:",squash"` } func NewLegacyOptions() *LegacyOptions { return &LegacyOptions{ - LegacyUpstreams: LegacyUpstreams{ - PassHostHeader: true, - ProxyWebSockets: true, - FlushInterval: DefaultUpstreamFlushInterval, - Timeout: DefaultUpstreamTimeout, - }, - - LegacyHeaders: LegacyHeaders{ - PassBasicAuth: true, - PassUserHeaders: true, - SkipAuthStripHeaders: true, - }, - - LegacyServer: LegacyServer{ - HTTPAddress: "127.0.0.1:4180", - HTTPSAddress: ":443", - }, - - LegacyProvider: LegacyProvider{ - ProviderType: "google", - AzureTenant: "common", - ApprovalPrompt: "force", - UserIDClaim: "email", - OIDCEmailClaim: "email", - OIDCGroupsClaim: "groups", - OIDCAudienceClaims: []string{"aud"}, - OIDCExtraAudiences: []string{}, - InsecureOIDCSkipNonce: true, - }, - - Options: *NewOptions(), + LegacyProvider: legacyProviderDefaults(), + Options: *NewOptions(), } } -func NewLegacyFlagSet() *pflag.FlagSet { - flagSet := NewFlagSet() - - flagSet.AddFlagSet(legacyUpstreamsFlagSet()) - flagSet.AddFlagSet(legacyHeadersFlagSet()) - flagSet.AddFlagSet(legacyServerFlagset()) - flagSet.AddFlagSet(legacyProviderFlagSet()) - flagSet.AddFlagSet(legacyGoogleFlagSet()) - - return flagSet -} - func (l *LegacyOptions) ToOptions() (*Options, error) { - upstreams, err := l.LegacyUpstreams.convert() - if err != nil { - return nil, fmt.Errorf("error converting upstreams: %v", err) - } - l.Options.UpstreamServers = upstreams - - l.Options.InjectRequestHeaders, l.Options.InjectResponseHeaders = l.LegacyHeaders.convert() - - l.Options.Server, l.Options.MetricsServer = l.LegacyServer.convert() - - l.Options.LegacyPreferEmailToUser = l.LegacyHeaders.PreferEmailToUser providers, err := l.LegacyProvider.convert() if err != nil { @@ -98,564 +29,63 @@ func (l *LegacyOptions) ToOptions() (*Options, error) { return &l.Options, nil } -type LegacyUpstreams struct { - FlushInterval time.Duration `flag:"flush-interval" cfg:"flush_interval"` - PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"` - ProxyWebSockets bool `flag:"proxy-websockets" cfg:"proxy_websockets"` - SSLUpstreamInsecureSkipVerify bool `flag:"ssl-upstream-insecure-skip-verify" cfg:"ssl_upstream_insecure_skip_verify"` - Upstreams []string `flag:"upstream" cfg:"upstreams"` - Timeout time.Duration `flag:"upstream-timeout" cfg:"upstream_timeout"` -} - -func legacyUpstreamsFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("upstreams", pflag.ExitOnError) - - flagSet.Duration("flush-interval", DefaultUpstreamFlushInterval, "period between response flushing when streaming responses") - flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream") - flagSet.Bool("proxy-websockets", true, "enables WebSocket proxying") - flagSet.Bool("ssl-upstream-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS upstreams") - flagSet.StringSlice("upstream", []string{}, "the http url(s) of the upstream endpoint, file:// paths for static files or static:// for static response. Routing is based on the path") - flagSet.Duration("upstream-timeout", DefaultUpstreamTimeout, "maximum amount of time the server will wait for a response from the upstream") - - return flagSet -} - -func (l *LegacyUpstreams) convert() (UpstreamConfig, error) { - upstreams := UpstreamConfig{} - - for _, upstreamString := range l.Upstreams { - u, err := url.Parse(upstreamString) - if err != nil { - return UpstreamConfig{}, fmt.Errorf("could not parse upstream %q: %v", upstreamString, err) - } - - if u.Path == "" { - u.Path = "/" - } - - flushInterval := Duration(l.FlushInterval) - timeout := Duration(l.Timeout) - upstream := Upstream{ - ID: u.Path, - Path: u.Path, - URI: upstreamString, - InsecureSkipTLSVerify: l.SSLUpstreamInsecureSkipVerify, - PassHostHeader: &l.PassHostHeader, - ProxyWebSockets: &l.ProxyWebSockets, - FlushInterval: &flushInterval, - Timeout: &timeout, - } - - switch u.Scheme { - case "file": - if u.Fragment != "" { - upstream.ID = u.Fragment - upstream.Path = u.Fragment - // Trim the fragment from the end of the URI - upstream.URI = strings.SplitN(upstreamString, "#", 2)[0] - } - case "static": - responseCode, err := strconv.Atoi(u.Host) - if err != nil { - logger.Errorf("unable to convert %q to int, use default \"200\"", u.Host) - responseCode = 200 - } - upstream.Static = true - upstream.StaticCode = &responseCode - - // This is not allowed to be empty and must be unique - upstream.ID = upstreamString - - // We only support the root path in the legacy config - upstream.Path = "/" - - // Force defaults compatible with static responses - upstream.URI = "" - upstream.InsecureSkipTLSVerify = false - upstream.PassHostHeader = nil - upstream.ProxyWebSockets = nil - upstream.FlushInterval = nil - upstream.Timeout = nil - case "unix": - upstream.Path = "/" - } - - upstreams.Upstreams = append(upstreams.Upstreams, upstream) - } - - return upstreams, nil -} - -type LegacyHeaders struct { - PassBasicAuth bool `flag:"pass-basic-auth" cfg:"pass_basic_auth"` - PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"` - PassUserHeaders bool `flag:"pass-user-headers" cfg:"pass_user_headers"` - PassAuthorization bool `flag:"pass-authorization-header" cfg:"pass_authorization_header"` - - SetBasicAuth bool `flag:"set-basic-auth" cfg:"set_basic_auth"` - SetXAuthRequest bool `flag:"set-xauthrequest" cfg:"set_xauthrequest"` - SetAuthorization bool `flag:"set-authorization-header" cfg:"set_authorization_header"` - - PreferEmailToUser bool `flag:"prefer-email-to-user" cfg:"prefer_email_to_user"` - BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"` - SkipAuthStripHeaders bool `flag:"skip-auth-strip-headers" cfg:"skip_auth_strip_headers"` -} - -func legacyHeadersFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("headers", pflag.ExitOnError) - - flagSet.Bool("pass-basic-auth", true, "pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream") - flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header") - flagSet.Bool("pass-user-headers", true, "pass X-Forwarded-User and X-Forwarded-Email information to upstream") - flagSet.Bool("pass-authorization-header", false, "pass the Authorization Header to upstream") - - flagSet.Bool("set-basic-auth", false, "set HTTP Basic Auth information in response (useful in Nginx auth_request mode)") - flagSet.Bool("set-xauthrequest", false, "set X-Auth-Request-User and X-Auth-Request-Email response headers (useful in Nginx auth_request mode)") - flagSet.Bool("set-authorization-header", false, "set Authorization response headers (useful in Nginx auth_request mode)") - - flagSet.Bool("prefer-email-to-user", false, "Prefer to use the Email address as the Username when passing information to upstream. Will only use Username if Email is unavailable, eg. htaccess authentication. Used in conjunction with -pass-basic-auth and -pass-user-headers") - flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header") - flagSet.Bool("skip-auth-strip-headers", true, "strips X-Forwarded-* style authentication headers & Authorization header if they would be set by oauth2-proxy") - - return flagSet -} - -// convert takes the legacy request/response headers and converts them to -// the new format for InjectRequestHeaders and InjectResponseHeaders -func (l *LegacyHeaders) convert() ([]Header, []Header) { - return l.getRequestHeaders(), l.getResponseHeaders() -} - -func (l *LegacyHeaders) getRequestHeaders() []Header { - requestHeaders := []Header{} - - if l.PassBasicAuth && l.BasicAuthPassword != "" { - requestHeaders = append(requestHeaders, getBasicAuthHeader(l.PreferEmailToUser, l.BasicAuthPassword)) - } - - // In the old implementation, PassUserHeaders is a subset of PassBasicAuth - if l.PassBasicAuth || l.PassUserHeaders { - requestHeaders = append(requestHeaders, getPassUserHeaders(l.PreferEmailToUser)...) - requestHeaders = append(requestHeaders, getPreferredUsernameHeader()) - } - - if l.PassAccessToken { - requestHeaders = append(requestHeaders, getPassAccessTokenHeader()) - } - - if l.PassAuthorization { - requestHeaders = append(requestHeaders, getAuthorizationHeader()) - } - - for i := range requestHeaders { - requestHeaders[i].PreserveRequestValue = !l.SkipAuthStripHeaders - } - - return requestHeaders -} - -func (l *LegacyHeaders) getResponseHeaders() []Header { - responseHeaders := []Header{} - - if l.SetXAuthRequest { - responseHeaders = append(responseHeaders, getXAuthRequestHeaders()...) - if l.PassAccessToken { - responseHeaders = append(responseHeaders, getXAuthRequestAccessTokenHeader()) - } - } - - if l.SetBasicAuth { - responseHeaders = append(responseHeaders, getBasicAuthHeader(l.PreferEmailToUser, l.BasicAuthPassword)) - } - - if l.SetAuthorization { - responseHeaders = append(responseHeaders, getAuthorizationHeader()) - } - - return responseHeaders -} - -func getBasicAuthHeader(preferEmailToUser bool, basicAuthPassword string) Header { - claim := "user" - if preferEmailToUser { - claim = "email" - } - - return Header{ - Name: "Authorization", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: claim, - Prefix: "Basic ", - BasicAuthPassword: &SecretSource{ - Value: []byte(basicAuthPassword), - }, - }, - }, - }, - } -} - -func getPassUserHeaders(preferEmailToUser bool) []Header { - headers := []Header{ - { - Name: "X-Forwarded-Groups", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - } - - if preferEmailToUser { - return append(headers, - Header{ - Name: "X-Forwarded-User", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - }, - ) - } - - return append(headers, - Header{ - Name: "X-Forwarded-User", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - }, - }, - }, - }, - Header{ - Name: "X-Forwarded-Email", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - }, - ) -} - -func getPassAccessTokenHeader() Header { - return Header{ - Name: "X-Forwarded-Access-Token", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "access_token", - }, - }, - }, - } -} - -func getAuthorizationHeader() Header { - return Header{ - Name: "Authorization", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "id_token", - Prefix: "Bearer ", - }, - }, - }, - } -} - -func getPreferredUsernameHeader() Header { - return Header{ - Name: "X-Forwarded-Preferred-Username", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - } -} - -func getXAuthRequestHeaders() []Header { - headers := []Header{ - { - Name: "X-Auth-Request-User", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Email", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Preferred-Username", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - }, - { - Name: "X-Auth-Request-Groups", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - } - - return headers -} - -func getXAuthRequestAccessTokenHeader() Header { - return Header{ - Name: "X-Auth-Request-Access-Token", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "access_token", - }, - }, - }, - } -} - -type LegacyServer struct { - MetricsAddress string `flag:"metrics-address" cfg:"metrics_address"` - MetricsSecureAddress string `flag:"metrics-secure-address" cfg:"metrics_secure_address"` - MetricsTLSCertFile string `flag:"metrics-tls-cert-file" cfg:"metrics_tls_cert_file"` - MetricsTLSKeyFile string `flag:"metrics-tls-key-file" cfg:"metrics_tls_key_file"` - HTTPAddress string `flag:"http-address" cfg:"http_address"` - HTTPSAddress string `flag:"https-address" cfg:"https_address"` - TLSCertFile string `flag:"tls-cert-file" cfg:"tls_cert_file"` - TLSKeyFile string `flag:"tls-key-file" cfg:"tls_key_file"` - TLSMinVersion string `flag:"tls-min-version" cfg:"tls_min_version"` - TLSCipherSuites []string `flag:"tls-cipher-suite" cfg:"tls_cipher_suites"` -} - -func legacyServerFlagset() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("server", pflag.ExitOnError) - - flagSet.String("metrics-address", "", "the address /metrics will be served on (e.g. \":9100\")") - flagSet.String("metrics-secure-address", "", "the address /metrics will be served on for HTTPS clients (e.g. \":9100\")") - flagSet.String("metrics-tls-cert-file", "", "path to certificate file for secure metrics server") - flagSet.String("metrics-tls-key-file", "", "path to private key file for secure metrics server") - flagSet.String("http-address", "127.0.0.1:4180", "[http://]: or unix:// to listen on for HTTP clients") - flagSet.String("https-address", ":443", ": to listen on for HTTPS clients") - flagSet.String("tls-cert-file", "", "path to certificate file") - flagSet.String("tls-key-file", "", "path to private key file") - flagSet.String("tls-min-version", "", "minimal TLS version for HTTPS clients (either \"TLS1.2\" or \"TLS1.3\")") - flagSet.StringSlice("tls-cipher-suite", []string{}, "restricts TLS cipher suites to those listed (e.g. TLS_RSA_WITH_RC4_128_SHA) (may be given multiple times)") - - return flagSet -} - type LegacyProvider struct { - ClientID string `flag:"client-id" cfg:"client_id"` - ClientSecret string `flag:"client-secret" cfg:"client_secret"` - ClientSecretFile string `flag:"client-secret-file" cfg:"client_secret_file"` - - KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"` - AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"` - AzureGraphGroupField string `flag:"azure-graph-group-field" cfg:"azure_graph_group_field"` - BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"` - BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"` - GitHubOrg string `flag:"github-org" cfg:"github_org"` - GitHubTeam string `flag:"github-team" cfg:"github_team"` - GitHubRepo string `flag:"github-repo" cfg:"github_repo"` - GitHubToken string `flag:"github-token" cfg:"github_token"` - GitHubUsers []string `flag:"github-user" cfg:"github_users"` - GitLabGroup []string `flag:"gitlab-group" cfg:"gitlab_groups"` - GitLabProjects []string `flag:"gitlab-project" cfg:"gitlab_projects"` - GoogleGroupsLegacy []string `flag:"google-group" cfg:"google_group"` - GoogleGroups []string `flag:"google-group" cfg:"google_groups"` - GoogleAdminEmail string `flag:"google-admin-email" cfg:"google_admin_email"` - GoogleServiceAccountJSON string `flag:"google-service-account-json" cfg:"google_service_account_json"` - GoogleUseApplicationDefaultCredentials bool `flag:"google-use-application-default-credentials" cfg:"google_use_application_default_credentials"` - GoogleTargetPrincipal string `flag:"google-target-principal" cfg:"google_target_principal"` - - // These options allow for other providers besides Google, with - // potential overrides. - ProviderType string `flag:"provider" cfg:"provider"` - ProviderName string `flag:"provider-display-name" cfg:"provider_display_name"` - ProviderCAFiles []string `flag:"provider-ca-file" cfg:"provider_ca_files"` - UseSystemTrustStore bool `flag:"use-system-trust-store" cfg:"use_system_trust_store"` - OIDCIssuerURL string `flag:"oidc-issuer-url" cfg:"oidc_issuer_url"` - InsecureOIDCAllowUnverifiedEmail bool `flag:"insecure-oidc-allow-unverified-email" cfg:"insecure_oidc_allow_unverified_email"` - InsecureOIDCSkipIssuerVerification bool `flag:"insecure-oidc-skip-issuer-verification" cfg:"insecure_oidc_skip_issuer_verification"` - InsecureOIDCSkipNonce bool `flag:"insecure-oidc-skip-nonce" cfg:"insecure_oidc_skip_nonce"` - SkipOIDCDiscovery bool `flag:"skip-oidc-discovery" cfg:"skip_oidc_discovery"` - OIDCJwksURL string `flag:"oidc-jwks-url" cfg:"oidc_jwks_url"` - OIDCEmailClaim string `flag:"oidc-email-claim" cfg:"oidc_email_claim"` - OIDCGroupsClaim string `flag:"oidc-groups-claim" cfg:"oidc_groups_claim"` - OIDCAudienceClaims []string `flag:"oidc-audience-claim" cfg:"oidc_audience_claims"` - OIDCExtraAudiences []string `flag:"oidc-extra-audience" cfg:"oidc_extra_audiences"` - LoginURL string `flag:"login-url" cfg:"login_url"` - RedeemURL string `flag:"redeem-url" cfg:"redeem_url"` - ProfileURL string `flag:"profile-url" cfg:"profile_url"` - SkipClaimsFromProfileURL bool `flag:"skip-claims-from-profile-url" cfg:"skip_claims_from_profile_url"` - ProtectedResource string `flag:"resource" cfg:"resource"` - ValidateURL string `flag:"validate-url" cfg:"validate_url"` - Scope string `flag:"scope" cfg:"scope"` - Prompt string `flag:"prompt" cfg:"prompt"` - ApprovalPrompt string `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0 - UserIDClaim string `flag:"user-id-claim" cfg:"user_id_claim"` - AllowedGroups []string `flag:"allowed-group" cfg:"allowed_groups"` - AllowedRoles []string `flag:"allowed-role" cfg:"allowed_roles"` - BackendLogoutURL string `flag:"backend-logout-url" cfg:"backend_logout_url"` - - AcrValues string `flag:"acr-values" cfg:"acr_values"` - JWTKey string `flag:"jwt-key" cfg:"jwt_key"` - JWTKeyFile string `flag:"jwt-key-file" cfg:"jwt_key_file"` - PubJWKURL string `flag:"pubjwk-url" cfg:"pubjwk_url"` - // PKCE Code Challenge method to use (either S256 or plain) - CodeChallengeMethod string `flag:"code-challenge-method" cfg:"code_challenge_method"` - // Provided for legacy reasons, to be dropped in newer version see #1667 - ForceCodeChallengeMethod string `flag:"force-code-challenge-method" cfg:"force_code_challenge_method"` -} - -func legacyProviderFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("provider", pflag.ExitOnError) - - flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)") - flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") - flagSet.String("azure-graph-group-field", "", "configures the group field to be used when building the groups list(`id` or `displayName`. Default is `id`) from Microsoft Graph(available only for v2.0 oidc url). Based on this value, the `allowed-group` config values should be adjusted accordingly. If using `id` as group field, `allowed-group` should contains groups IDs, if using `displayName` as group field, `allowed-group` should contains groups name") - flagSet.String("bitbucket-team", "", "restrict logins to members of this team") - flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") - flagSet.String("github-org", "", "restrict logins to members of this organisation") - flagSet.String("github-team", "", "restrict logins to members of this team") - flagSet.String("github-repo", "", "restrict logins to collaborators of this repository") - flagSet.String("github-token", "", "the token to use when verifying repository collaborators (must have push access to the repository)") - flagSet.StringSlice("github-user", []string{}, "allow users with these usernames to login even if they do not belong to the specified org and team or collaborators (may be given multiple times)") - flagSet.StringSlice("gitlab-group", []string{}, "restrict logins to members of this group (may be given multiple times)") - flagSet.StringSlice("gitlab-project", []string{}, "restrict logins to members of this project (may be given multiple times) (eg `group/project=accesslevel`). Access level should be a value matching Gitlab access levels (see https://docs.gitlab.com/ee/api/members.html#valid-access-levels), defaulted to 20 if absent") - flagSet.String("client-id", "", "the OAuth Client ID: ie: \"123456.apps.googleusercontent.com\"") - flagSet.String("client-secret", "", "the OAuth Client Secret") - flagSet.String("client-secret-file", "", "the file with OAuth Client Secret") - - flagSet.String("provider", "google", "OAuth provider") - flagSet.String("provider-display-name", "", "Provider display name") - flagSet.StringSlice("provider-ca-file", []string{}, "One or more paths to CA certificates that should be used when connecting to the provider. If not specified, the default Go trust sources are used instead.") - flagSet.Bool("use-system-trust-store", false, "Determines if 'provider-ca-file' files and the system trust store are used. If set to true, your custom CA files and the system trust store are used otherwise only your custom CA files.") - flagSet.String("oidc-issuer-url", "", "OpenID Connect issuer URL (ie: https://accounts.google.com)") - flagSet.Bool("insecure-oidc-allow-unverified-email", false, "Don't fail if an email address in an id_token is not verified") - flagSet.Bool("insecure-oidc-skip-issuer-verification", false, "Do not verify if issuer matches OIDC discovery URL") - flagSet.Bool("insecure-oidc-skip-nonce", true, "skip verifying the OIDC ID Token's nonce claim") - flagSet.Bool("skip-oidc-discovery", false, "Skip OIDC discovery and use manually supplied Endpoints") - flagSet.String("oidc-jwks-url", "", "OpenID Connect JWKS URL (ie: https://www.googleapis.com/oauth2/v3/certs)") - flagSet.String("oidc-groups-claim", OIDCGroupsClaim, "which OIDC claim contains the user groups") - flagSet.String("oidc-email-claim", OIDCEmailClaim, "which OIDC claim contains the user's email") - flagSet.StringSlice("oidc-audience-claim", OIDCAudienceClaims, "which OIDC claims are used as audience to verify against client id") - flagSet.StringSlice("oidc-extra-audience", []string{}, "additional audiences allowed to pass audience verification") - flagSet.String("login-url", "", "Authentication endpoint") - flagSet.String("redeem-url", "", "Token redemption endpoint") - flagSet.String("profile-url", "", "Profile access endpoint") - flagSet.Bool("skip-claims-from-profile-url", false, "Skip loading missing claims from profile URL") - flagSet.String("resource", "", "The resource that is protected (Azure AD only)") - flagSet.String("validate-url", "", "Access token validation endpoint") - flagSet.String("scope", "", "OAuth scope specification") - flagSet.String("prompt", "", "OIDC prompt") - flagSet.String("approval-prompt", "force", "OAuth approval_prompt") - flagSet.String("code-challenge-method", "", "use PKCE code challenges with the specified method. Either 'plain' or 'S256'") - flagSet.String("force-code-challenge-method", "", "Deprecated - use --code-challenge-method") - - flagSet.String("acr-values", "", "acr values string: optional") - flagSet.String("jwt-key", "", "private key in PEM format used to sign JWT, so that you can say something like -jwt-key=\"${OAUTH2_PROXY_JWT_KEY}\": required by login.gov") - flagSet.String("jwt-key-file", "", "path to the private key file in PEM format used to sign the JWT so that you can say something like -jwt-key-file=/etc/ssl/private/jwt_signing_key.pem: required by login.gov") - flagSet.String("pubjwk-url", "", "JWK pubkey access endpoint: required by login.gov") - - flagSet.String("user-id-claim", OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") - flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") - flagSet.StringSlice("allowed-role", []string{}, "(keycloak-oidc) restrict logins to members of these roles (may be given multiple times)") - flagSet.String("backend-logout-url", "", "url to perform a backend logout, {id_token} can be used as placeholder for the id_token") - - return flagSet -} - -func legacyGoogleFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("google", pflag.ExitOnError) - - flagSet.StringSlice("google-group", []string{}, "restrict logins to members of this google group (may be given multiple times).") - flagSet.String("google-admin-email", "", "the google admin to impersonate for api calls") - flagSet.String("google-service-account-json", "", "the path to the service account json credentials") - flagSet.String("google-use-application-default-credentials", "", "use application default credentials instead of service account json (i.e. GKE Workload Identity)") - flagSet.String("google-target-principal", "", "the target principal to impersonate when using ADC") - - return flagSet -} - -func (l LegacyServer) convert() (Server, Server) { - appServer := Server{ - BindAddress: l.HTTPAddress, - SecureBindAddress: l.HTTPSAddress, - } - if l.TLSKeyFile != "" || l.TLSCertFile != "" { - appServer.TLS = &TLS{ - Key: &SecretSource{ - FromFile: l.TLSKeyFile, - }, - Cert: &SecretSource{ - FromFile: l.TLSCertFile, - }, - MinVersion: l.TLSMinVersion, - } - if len(l.TLSCipherSuites) != 0 { - appServer.TLS.CipherSuites = l.TLSCipherSuites - } - // Preserve backwards compatibility, only run one server - appServer.BindAddress = "" - } else { - // Disable the HTTPS server if there's no certificates. - // This preserves backwards compatibility. - appServer.SecureBindAddress = "" - } - - metricsServer := Server{ - BindAddress: l.MetricsAddress, - SecureBindAddress: l.MetricsSecureAddress, - } - if l.MetricsTLSKeyFile != "" || l.MetricsTLSCertFile != "" { - metricsServer.TLS = &TLS{ - Key: &SecretSource{ - FromFile: l.MetricsTLSKeyFile, - }, - Cert: &SecretSource{ - FromFile: l.MetricsTLSCertFile, - }, - } + ClientID string `mapstructure:"client_id"` + ClientSecret string `mapstructure:"client_secret"` + ProviderType string `mapstructure:"provider"` + OIDCIssuerURL string `mapstructure:"oidc_issuer_url"` + InsecureOIDCSkipIssuerVerification bool `mapstructure:"insecure_oidc_skip_issuer_verification"` + InsecureOIDCSkipNonce bool `mapstructure:"insecure_oidc_skip_nonce"` + SkipOIDCDiscovery bool `mapstructure:"skip_oidc_discovery"` + OIDCJwksURL string `mapstructure:"oidc_jwks_url"` + OIDCEmailClaim string `mapstructure:"oidc_email_claim"` + OIDCGroupsClaim string `mapstructure:"oidc_groups_claim"` + OIDCAudienceClaims []string `mapstructure:"oidc_audience_claims"` + OIDCExtraAudiences []string `mapstructure:"oidc_extra_audiences"` + OIDCVerifierRequestTimeout uint32 `mapstructure:"oidc_verifier_request_timeout"` + LoginURL string `mapstructure:"login_url"` + RedeemURL string `mapstructure:"redeem_url"` + RedeemTimeout uint32 `mapstructure:"redeem_timeout"` + ProfileURL string `mapstructure:"profile_url"` + SkipClaimsFromProfileURL bool `mapstructure:"skip_claims_from_profile_url"` + ValidateURL string `mapstructure:"validate_url"` + Scope string `mapstructure:"scope"` + Prompt string `mapstructure:"prompt"` + ApprovalPrompt string `mapstructure:"approval_prompt"` + UserIDClaim string `mapstructure:"user_id_claim"` + AllowedGroups []string `mapstructure:"allowed_groups"` + AcrValues string `mapstructure:"acr_values"` + CodeChallengeMethod string `mapstructure:"code_challenge_method"` +} + +func legacyProviderDefaults() LegacyProvider { + return LegacyProvider{ + ClientID: "", + ClientSecret: "", + ProviderType: "oidc", + OIDCIssuerURL: "", + InsecureOIDCSkipIssuerVerification: false, + InsecureOIDCSkipNonce: true, + SkipOIDCDiscovery: false, + OIDCJwksURL: "", + OIDCEmailClaim: OIDCEmailClaim, + OIDCGroupsClaim: OIDCGroupsClaim, + OIDCAudienceClaims: []string{"aud"}, + OIDCExtraAudiences: nil, + OIDCVerifierRequestTimeout: 2000, + LoginURL: "", + RedeemURL: "", + ProfileURL: "", + SkipClaimsFromProfileURL: false, + ValidateURL: "", + Scope: "", + Prompt: "", + ApprovalPrompt: "", + UserIDClaim: OIDCEmailClaim, + AllowedGroups: nil, + AcrValues: "", + CodeChallengeMethod: "", } - - return appServer, metricsServer } func (l *LegacyProvider) convert() (Providers, error) { @@ -664,26 +94,21 @@ func (l *LegacyProvider) convert() (Providers, error) { provider := Provider{ ClientID: l.ClientID, ClientSecret: l.ClientSecret, - ClientSecretFile: l.ClientSecretFile, Type: ProviderType(l.ProviderType), - CAFiles: l.ProviderCAFiles, - UseSystemTrustStore: l.UseSystemTrustStore, LoginURL: l.LoginURL, RedeemURL: l.RedeemURL, ProfileURL: l.ProfileURL, SkipClaimsFromProfileURL: l.SkipClaimsFromProfileURL, - ProtectedResource: l.ProtectedResource, ValidateURL: l.ValidateURL, Scope: l.Scope, AllowedGroups: l.AllowedGroups, CodeChallengeMethod: l.CodeChallengeMethod, - BackendLogoutURL: l.BackendLogoutURL, + RedeemTimeout: l.RedeemTimeout, } // This part is out of the switch section for all providers that support OIDC provider.OIDCConfig = OIDCOptions{ IssuerURL: l.OIDCIssuerURL, - InsecureAllowUnverifiedEmail: l.InsecureOIDCAllowUnverifiedEmail, InsecureSkipIssuerVerification: l.InsecureOIDCSkipIssuerVerification, InsecureSkipNonce: l.InsecureOIDCSkipNonce, SkipDiscovery: l.SkipOIDCDiscovery, @@ -693,77 +118,10 @@ func (l *LegacyProvider) convert() (Providers, error) { GroupsClaim: l.OIDCGroupsClaim, AudienceClaims: l.OIDCAudienceClaims, ExtraAudiences: l.OIDCExtraAudiences, + VerifierRequestTimeout: l.OIDCVerifierRequestTimeout, } - // Support for legacy configuration option - if l.ForceCodeChallengeMethod != "" && l.CodeChallengeMethod == "" { - provider.CodeChallengeMethod = l.ForceCodeChallengeMethod - } - - // This part is out of the switch section because azure has a default tenant - // that needs to be added from legacy options - provider.AzureConfig = AzureOptions{ - Tenant: l.AzureTenant, - GraphGroupField: l.AzureGraphGroupField, - } - - switch provider.Type { - case "github": - provider.GitHubConfig = GitHubOptions{ - Org: l.GitHubOrg, - Team: l.GitHubTeam, - Repo: l.GitHubRepo, - Token: l.GitHubToken, - Users: l.GitHubUsers, - } - case "keycloak-oidc": - provider.KeycloakConfig = KeycloakOptions{ - Groups: l.KeycloakGroups, - Roles: l.AllowedRoles, - } - case "keycloak": - provider.KeycloakConfig = KeycloakOptions{ - Groups: l.KeycloakGroups, - } - case "gitlab": - provider.GitLabConfig = GitLabOptions{ - Group: l.GitLabGroup, - Projects: l.GitLabProjects, - } - case "login.gov": - provider.LoginGovConfig = LoginGovOptions{ - JWTKey: l.JWTKey, - JWTKeyFile: l.JWTKeyFile, - PubJWKURL: l.PubJWKURL, - } - case "bitbucket": - provider.BitbucketConfig = BitbucketOptions{ - Team: l.BitbucketTeam, - Repository: l.BitbucketRepository, - } - case "google": - if len(l.GoogleGroupsLegacy) != 0 && !reflect.DeepEqual(l.GoogleGroupsLegacy, l.GoogleGroups) { - // Log the deprecation notice - logger.Error( - "WARNING: The 'OAUTH2_PROXY_GOOGLE_GROUP' environment variable is deprecated and will likely be removed in the next major release. Use 'OAUTH2_PROXY_GOOGLE_GROUPS' instead.", - ) - l.GoogleGroups = l.GoogleGroupsLegacy - } - provider.GoogleConfig = GoogleOptions{ - Groups: l.GoogleGroups, - AdminEmail: l.GoogleAdminEmail, - ServiceAccountJSON: l.GoogleServiceAccountJSON, - UseApplicationDefaultCredentials: l.GoogleUseApplicationDefaultCredentials, - TargetPrincipal: l.GoogleTargetPrincipal, - } - } - - if l.ProviderName != "" { - provider.ID = l.ProviderName - provider.Name = l.ProviderName - } else { - provider.ID = l.ProviderType + "=" + l.ClientID - } + provider.ID = l.ProviderType + "=" + l.ClientID // handle AcrValues, Prompt and ApprovalPrompt var urlParams []LoginURLParameter @@ -775,10 +133,6 @@ func (l *LegacyProvider) convert() (Providers, error) { urlParams = append(urlParams, LoginURLParameter{Name: "prompt", Default: []string{l.Prompt}}) case l.ApprovalPrompt != "": urlParams = append(urlParams, LoginURLParameter{Name: "approval_prompt", Default: []string{l.ApprovalPrompt}}) - default: - // match legacy behaviour by default - if neither prompt nor approval_prompt - // specified, use approval_prompt=force - urlParams = append(urlParams, LoginURLParameter{Name: "approval_prompt", Default: []string{"force"}}) } provider.LoginURLParameters = urlParams diff --git a/pkg/apis/options/legacy_options_test.go b/pkg/apis/options/legacy_options_test.go deleted file mode 100644 index 4ad7d9fcd1..0000000000 --- a/pkg/apis/options/legacy_options_test.go +++ /dev/null @@ -1,1042 +0,0 @@ -package options - -import ( - "time" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("Legacy Options", func() { - Context("ToOptions", func() { - It("converts the options as expected", func() { - opts := NewOptions() - - legacyOpts := NewLegacyOptions() - - // Set upstreams and related options to test their conversion - flushInterval := Duration(5 * time.Second) - timeout := Duration(5 * time.Second) - legacyOpts.LegacyUpstreams.FlushInterval = time.Duration(flushInterval) - legacyOpts.LegacyUpstreams.Timeout = time.Duration(timeout) - legacyOpts.LegacyUpstreams.PassHostHeader = true - legacyOpts.LegacyUpstreams.ProxyWebSockets = true - legacyOpts.LegacyUpstreams.SSLUpstreamInsecureSkipVerify = true - legacyOpts.LegacyUpstreams.Upstreams = []string{"http://foo.bar/baz", "file:///var/lib/website#/bar", "static://204"} - legacyOpts.LegacyProvider.ClientID = "oauth-proxy" - - truth := true - staticCode := 204 - opts.UpstreamServers = UpstreamConfig{ - Upstreams: []Upstream{ - { - ID: "/baz", - Path: "/baz", - URI: "http://foo.bar/baz", - FlushInterval: &flushInterval, - InsecureSkipTLSVerify: true, - PassHostHeader: &truth, - ProxyWebSockets: &truth, - Timeout: &timeout, - }, - { - ID: "/bar", - Path: "/bar", - URI: "file:///var/lib/website", - FlushInterval: &flushInterval, - InsecureSkipTLSVerify: true, - PassHostHeader: &truth, - ProxyWebSockets: &truth, - Timeout: &timeout, - }, - { - ID: "static://204", - Path: "/", - URI: "", - Static: true, - StaticCode: &staticCode, - FlushInterval: nil, - InsecureSkipTLSVerify: false, - PassHostHeader: nil, - ProxyWebSockets: nil, - Timeout: nil, - }, - }, - } - - opts.InjectRequestHeaders = []Header{ - { - Name: "X-Forwarded-Groups", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "groups", - }, - }, - }, - }, - { - Name: "X-Forwarded-User", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - }, - }, - }, - }, - { - Name: "X-Forwarded-Email", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - }, - { - Name: "X-Forwarded-Preferred-Username", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - }, - } - - opts.InjectResponseHeaders = []Header{} - - opts.Server = Server{ - BindAddress: "127.0.0.1:4180", - } - - opts.Providers[0].ClientID = "oauth-proxy" - opts.Providers[0].ID = "google=oauth-proxy" - opts.Providers[0].OIDCConfig.InsecureSkipNonce = true - opts.Providers[0].OIDCConfig.AudienceClaims = []string{"aud"} - opts.Providers[0].OIDCConfig.ExtraAudiences = []string{} - opts.Providers[0].LoginURLParameters = []LoginURLParameter{ - {Name: "approval_prompt", Default: []string{"force"}}, - } - - converted, err := legacyOpts.ToOptions() - Expect(err).ToNot(HaveOccurred()) - Expect(converted).To(Equal(opts)) - }) - }) - - Context("Legacy Upstreams", func() { - type convertUpstreamsTableInput struct { - upstreamStrings []string - expectedUpstreams []Upstream - errMsg string - } - - // Non defaults for these options - skipVerify := true - passHostHeader := false - proxyWebSockets := true - flushInterval := Duration(5 * time.Second) - timeout := Duration(5 * time.Second) - - // Test cases and expected outcomes - validHTTP := "http://foo.bar/baz" - validHTTPUpstream := Upstream{ - ID: "/baz", - Path: "/baz", - URI: validHTTP, - InsecureSkipTLSVerify: skipVerify, - PassHostHeader: &passHostHeader, - ProxyWebSockets: &proxyWebSockets, - FlushInterval: &flushInterval, - Timeout: &timeout, - } - - // Test cases and expected outcomes - emptyPathHTTP := "http://foo.bar" - emptyPathHTTPUpstream := Upstream{ - ID: "/", - Path: "/", - URI: emptyPathHTTP, - InsecureSkipTLSVerify: skipVerify, - PassHostHeader: &passHostHeader, - ProxyWebSockets: &proxyWebSockets, - FlushInterval: &flushInterval, - Timeout: &timeout, - } - - validFileWithFragment := "file:///var/lib/website#/bar" - validFileWithFragmentUpstream := Upstream{ - ID: "/bar", - Path: "/bar", - URI: "file:///var/lib/website", - InsecureSkipTLSVerify: skipVerify, - PassHostHeader: &passHostHeader, - ProxyWebSockets: &proxyWebSockets, - FlushInterval: &flushInterval, - Timeout: &timeout, - } - - validStatic := "static://204" - validStaticCode := 204 - validStaticUpstream := Upstream{ - ID: validStatic, - Path: "/", - URI: "", - Static: true, - StaticCode: &validStaticCode, - InsecureSkipTLSVerify: false, - PassHostHeader: nil, - ProxyWebSockets: nil, - FlushInterval: nil, - Timeout: nil, - } - - invalidStatic := "static://abc" - invalidStaticCode := 200 - invalidStaticUpstream := Upstream{ - ID: invalidStatic, - Path: "/", - URI: "", - Static: true, - StaticCode: &invalidStaticCode, - InsecureSkipTLSVerify: false, - PassHostHeader: nil, - ProxyWebSockets: nil, - FlushInterval: nil, - Timeout: nil, - } - - invalidHTTP := ":foo" - invalidHTTPErrMsg := "could not parse upstream \":foo\": parse \":foo\": missing protocol scheme" - - DescribeTable("convertLegacyUpstreams", - func(in *convertUpstreamsTableInput) { - legacyUpstreams := LegacyUpstreams{ - Upstreams: in.upstreamStrings, - SSLUpstreamInsecureSkipVerify: skipVerify, - PassHostHeader: passHostHeader, - ProxyWebSockets: proxyWebSockets, - FlushInterval: time.Duration(flushInterval), - Timeout: time.Duration(timeout), - } - - upstreams, err := legacyUpstreams.convert() - - if in.errMsg != "" { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal(in.errMsg)) - } else { - Expect(err).ToNot(HaveOccurred()) - } - - Expect(upstreams.Upstreams).To(ConsistOf(in.expectedUpstreams)) - }, - Entry("with no upstreams", &convertUpstreamsTableInput{ - upstreamStrings: []string{}, - expectedUpstreams: []Upstream{}, - errMsg: "", - }), - Entry("with a valid HTTP upstream", &convertUpstreamsTableInput{ - upstreamStrings: []string{validHTTP}, - expectedUpstreams: []Upstream{validHTTPUpstream}, - errMsg: "", - }), - Entry("with a HTTP upstream with an empty path", &convertUpstreamsTableInput{ - upstreamStrings: []string{emptyPathHTTP}, - expectedUpstreams: []Upstream{emptyPathHTTPUpstream}, - errMsg: "", - }), - Entry("with a valid File upstream with a fragment", &convertUpstreamsTableInput{ - upstreamStrings: []string{validFileWithFragment}, - expectedUpstreams: []Upstream{validFileWithFragmentUpstream}, - errMsg: "", - }), - Entry("with a valid static upstream", &convertUpstreamsTableInput{ - upstreamStrings: []string{validStatic}, - expectedUpstreams: []Upstream{validStaticUpstream}, - errMsg: "", - }), - Entry("with an invalid static upstream, code is 200", &convertUpstreamsTableInput{ - upstreamStrings: []string{invalidStatic}, - expectedUpstreams: []Upstream{invalidStaticUpstream}, - errMsg: "", - }), - Entry("with an invalid HTTP upstream", &convertUpstreamsTableInput{ - upstreamStrings: []string{invalidHTTP}, - expectedUpstreams: []Upstream{}, - errMsg: invalidHTTPErrMsg, - }), - Entry("with an invalid HTTP upstream and other upstreams", &convertUpstreamsTableInput{ - upstreamStrings: []string{validHTTP, invalidHTTP}, - expectedUpstreams: []Upstream{}, - errMsg: invalidHTTPErrMsg, - }), - Entry("with multiple valid upstreams", &convertUpstreamsTableInput{ - upstreamStrings: []string{validHTTP, validFileWithFragment, validStatic}, - expectedUpstreams: []Upstream{validHTTPUpstream, validFileWithFragmentUpstream, validStaticUpstream}, - errMsg: "", - }), - ) - }) - - Context("Legacy Headers", func() { - const basicAuthSecret = "super-secret-password" - - type legacyHeadersTableInput struct { - legacyHeaders *LegacyHeaders - expectedRequestHeaders []Header - expectedResponseHeaders []Header - } - - withPreserveRequestValue := func(h Header, preserve bool) Header { - h.PreserveRequestValue = preserve - return h - } - - xForwardedUser := Header{ - Name: "X-Forwarded-User", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - }, - }, - }, - } - - xForwardedEmail := Header{ - Name: "X-Forwarded-Email", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - } - - xForwardedGroups := Header{ - Name: "X-Forwarded-Groups", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "groups", - }, - }, - }, - } - - xForwardedPreferredUsername := Header{ - Name: "X-Forwarded-Preferred-Username", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - } - - basicAuthHeader := Header{ - Name: "Authorization", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - Prefix: "Basic ", - BasicAuthPassword: &SecretSource{ - Value: []byte(basicAuthSecret), - }, - }, - }, - }, - } - - xForwardedUserWithEmail := Header{ - Name: "X-Forwarded-User", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - } - - xForwardedAccessToken := Header{ - Name: "X-Forwarded-Access-Token", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "access_token", - }, - }, - }, - } - - basicAuthHeaderWithEmail := Header{ - Name: "Authorization", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - Prefix: "Basic ", - BasicAuthPassword: &SecretSource{ - Value: []byte(basicAuthSecret), - }, - }, - }, - }, - } - - xAuthRequestUser := Header{ - Name: "X-Auth-Request-User", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - }, - }, - }, - } - - xAuthRequestEmail := Header{ - Name: "X-Auth-Request-Email", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "email", - }, - }, - }, - } - - xAuthRequestGroups := Header{ - Name: "X-Auth-Request-Groups", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "groups", - }, - }, - }, - } - - xAuthRequestPreferredUsername := Header{ - Name: "X-Auth-Request-Preferred-Username", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "preferred_username", - }, - }, - }, - } - - xAuthRequestAccessToken := Header{ - Name: "X-Auth-Request-Access-Token", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "access_token", - }, - }, - }, - } - - authorizationHeader := Header{ - Name: "Authorization", - PreserveRequestValue: false, - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "id_token", - Prefix: "Bearer ", - }, - }, - }, - } - - DescribeTable("should convert to injectRequestHeaders", - func(in legacyHeadersTableInput) { - requestHeaders, responseHeaders := in.legacyHeaders.convert() - Expect(requestHeaders).To(ConsistOf(in.expectedRequestHeaders)) - Expect(responseHeaders).To(ConsistOf(in.expectedResponseHeaders)) - }, - Entry("with all header options off", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{}, - expectedResponseHeaders: []Header{}, - }), - Entry("with basic auth enabled", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: true, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: true, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: basicAuthSecret, - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - xForwardedUser, - xForwardedEmail, - xForwardedGroups, - xForwardedPreferredUsername, - basicAuthHeader, - }, - expectedResponseHeaders: []Header{ - basicAuthHeader, - }, - }), - Entry("with basic auth enabled and skipAuthStripHeaders disabled", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: true, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: true, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: basicAuthSecret, - SkipAuthStripHeaders: false, - }, - expectedRequestHeaders: []Header{ - withPreserveRequestValue(xForwardedUser, true), - withPreserveRequestValue(xForwardedEmail, true), - withPreserveRequestValue(xForwardedGroups, true), - withPreserveRequestValue(xForwardedPreferredUsername, true), - withPreserveRequestValue(basicAuthHeader, true), - }, - expectedResponseHeaders: []Header{ - basicAuthHeader, - }, - }), - Entry("with basic auth enabled and preferEmailToUser", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: true, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: true, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: true, - BasicAuthPassword: basicAuthSecret, - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - xForwardedUserWithEmail, - xForwardedGroups, - xForwardedPreferredUsername, - basicAuthHeaderWithEmail, - }, - expectedResponseHeaders: []Header{ - basicAuthHeaderWithEmail, - }, - }), - Entry("with basic auth enabled and passUserHeaders", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: true, - PassAccessToken: false, - PassUserHeaders: true, - PassAuthorization: false, - - SetBasicAuth: true, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: basicAuthSecret, - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - xForwardedUser, - xForwardedEmail, - xForwardedGroups, - xForwardedPreferredUsername, - basicAuthHeader, - }, - expectedResponseHeaders: []Header{ - basicAuthHeader, - }, - }), - Entry("with passUserHeaders", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: false, - PassUserHeaders: true, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - xForwardedUser, - xForwardedEmail, - xForwardedGroups, - xForwardedPreferredUsername, - }, - expectedResponseHeaders: []Header{}, - }), - Entry("with passUserHeaders and SkipAuthStripHeaders disabled", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: false, - PassUserHeaders: true, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: false, - }, - expectedRequestHeaders: []Header{ - withPreserveRequestValue(xForwardedUser, true), - withPreserveRequestValue(xForwardedEmail, true), - withPreserveRequestValue(xForwardedGroups, true), - withPreserveRequestValue(xForwardedPreferredUsername, true), - }, - expectedResponseHeaders: []Header{}, - }), - Entry("with setXAuthRequest", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: true, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{}, - expectedResponseHeaders: []Header{ - xAuthRequestUser, - xAuthRequestEmail, - xAuthRequestGroups, - xAuthRequestPreferredUsername, - }, - }), - Entry("with passAccessToken", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: true, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - xForwardedAccessToken, - }, - expectedResponseHeaders: []Header{}, - }), - Entry("with passAcessToken and setXAuthRequest", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: true, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: true, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - xForwardedAccessToken, - }, - expectedResponseHeaders: []Header{ - xAuthRequestUser, - xAuthRequestEmail, - xAuthRequestGroups, - xAuthRequestPreferredUsername, - xAuthRequestAccessToken, - }, - }), - Entry("with passAcessToken and SkipAuthStripHeaders disabled", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: true, - PassUserHeaders: false, - PassAuthorization: false, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: false, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: false, - }, - expectedRequestHeaders: []Header{ - withPreserveRequestValue(xForwardedAccessToken, true), - }, - expectedResponseHeaders: []Header{}, - }), - Entry("with authorization headers", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: true, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: true, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: true, - }, - expectedRequestHeaders: []Header{ - authorizationHeader, - }, - expectedResponseHeaders: []Header{ - authorizationHeader, - }, - }), - Entry("with authorization headers and SkipAuthStripHeaders disabled", legacyHeadersTableInput{ - legacyHeaders: &LegacyHeaders{ - PassBasicAuth: false, - PassAccessToken: false, - PassUserHeaders: false, - PassAuthorization: true, - - SetBasicAuth: false, - SetXAuthRequest: false, - SetAuthorization: true, - - PreferEmailToUser: false, - BasicAuthPassword: "", - SkipAuthStripHeaders: false, - }, - expectedRequestHeaders: []Header{ - withPreserveRequestValue(authorizationHeader, true), - }, - expectedResponseHeaders: []Header{ - authorizationHeader, - }, - }), - ) - }) - - Context("Legacy Servers", func() { - type legacyServersTableInput struct { - legacyServer LegacyServer - expectedAppServer Server - expectedMetricsServer Server - } - - const ( - insecureAddr = "127.0.0.1:8080" - insecureMetricsAddr = ":9090" - secureAddr = ":443" - secureMetricsAddr = ":9443" - crtPath = "tls.crt" - keyPath = "tls.key" - minVersion = "TLS1.3" - ) - cipherSuites := []string{"TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_256_GCM_SHA384"} - - var tlsConfig = &TLS{ - Cert: &SecretSource{ - FromFile: crtPath, - }, - Key: &SecretSource{ - FromFile: keyPath, - }, - } - - var tlsConfigMinVersion = &TLS{ - Cert: tlsConfig.Cert, - Key: tlsConfig.Key, - MinVersion: minVersion, - } - - var tlsConfigCipherSuites = &TLS{ - Cert: tlsConfig.Cert, - Key: tlsConfig.Key, - CipherSuites: []string{ - "TLS_RSA_WITH_AES_128_GCM_SHA256", - "TLS_RSA_WITH_AES_256_GCM_SHA384", - }, - } - - DescribeTable("should convert to app and metrics servers", - func(in legacyServersTableInput) { - appServer, metricsServer := in.legacyServer.convert() - Expect(appServer).To(Equal(in.expectedAppServer)) - Expect(metricsServer).To(Equal(in.expectedMetricsServer)) - }, - Entry("with default options only starts app HTTP server", legacyServersTableInput{ - legacyServer: LegacyServer{ - HTTPAddress: insecureAddr, - HTTPSAddress: secureAddr, - }, - expectedAppServer: Server{ - BindAddress: insecureAddr, - }, - }), - Entry("with TLS options specified only starts app HTTPS server", legacyServersTableInput{ - legacyServer: LegacyServer{ - HTTPAddress: insecureAddr, - HTTPSAddress: secureAddr, - TLSKeyFile: keyPath, - TLSCertFile: crtPath, - }, - expectedAppServer: Server{ - SecureBindAddress: secureAddr, - TLS: tlsConfig, - }, - }), - Entry("with TLS options specified with MinVersion", legacyServersTableInput{ - legacyServer: LegacyServer{ - HTTPAddress: insecureAddr, - HTTPSAddress: secureAddr, - TLSKeyFile: keyPath, - TLSCertFile: crtPath, - TLSMinVersion: minVersion, - }, - expectedAppServer: Server{ - SecureBindAddress: secureAddr, - TLS: tlsConfigMinVersion, - }, - }), - Entry("with TLS options specified with CipherSuites", legacyServersTableInput{ - legacyServer: LegacyServer{ - HTTPAddress: insecureAddr, - HTTPSAddress: secureAddr, - TLSKeyFile: keyPath, - TLSCertFile: crtPath, - TLSCipherSuites: cipherSuites, - }, - expectedAppServer: Server{ - SecureBindAddress: secureAddr, - TLS: tlsConfigCipherSuites, - }, - }), - Entry("with metrics HTTP and HTTPS addresses", legacyServersTableInput{ - legacyServer: LegacyServer{ - HTTPAddress: insecureAddr, - HTTPSAddress: secureAddr, - MetricsAddress: insecureMetricsAddr, - MetricsSecureAddress: secureMetricsAddr, - }, - expectedAppServer: Server{ - BindAddress: insecureAddr, - }, - expectedMetricsServer: Server{ - BindAddress: insecureMetricsAddr, - SecureBindAddress: secureMetricsAddr, - }, - }), - Entry("with metrics HTTPS and tls cert/key", legacyServersTableInput{ - legacyServer: LegacyServer{ - HTTPAddress: insecureAddr, - HTTPSAddress: secureAddr, - MetricsAddress: insecureMetricsAddr, - MetricsSecureAddress: secureMetricsAddr, - MetricsTLSKeyFile: keyPath, - MetricsTLSCertFile: crtPath, - }, - expectedAppServer: Server{ - BindAddress: insecureAddr, - }, - expectedMetricsServer: Server{ - BindAddress: insecureMetricsAddr, - SecureBindAddress: secureMetricsAddr, - TLS: tlsConfig, - }, - }), - ) - }) - - Context("Legacy Providers", func() { - type convertProvidersTableInput struct { - legacyProvider LegacyProvider - expectedProviders Providers - errMsg string - } - - // Non defaults for these options - clientID := "abcd" - - defaultURLParams := []LoginURLParameter{ - {Name: "approval_prompt", Default: []string{"force"}}, - } - - defaultProvider := Provider{ - ID: "google=" + clientID, - ClientID: clientID, - Type: "google", - LoginURLParameters: defaultURLParams, - } - defaultLegacyProvider := LegacyProvider{ - ClientID: clientID, - ProviderType: "google", - } - - defaultProviderWithPrompt := Provider{ - ID: "google=" + clientID, - ClientID: clientID, - Type: "google", - LoginURLParameters: []LoginURLParameter{ - {Name: "prompt", Default: []string{"switch_user"}}, - }, - } - defaultLegacyProviderWithPrompt := LegacyProvider{ - ClientID: clientID, - ProviderType: "google", - Prompt: "switch_user", - } - - displayNameProvider := Provider{ - ID: "displayName", - Name: "displayName", - ClientID: clientID, - Type: "google", - LoginURLParameters: defaultURLParams, - } - - displayNameLegacyProvider := LegacyProvider{ - ClientID: clientID, - ProviderName: "displayName", - ProviderType: "google", - } - - internalConfigProvider := Provider{ - ID: "google=" + clientID, - ClientID: clientID, - Type: "google", - GoogleConfig: GoogleOptions{ - AdminEmail: "email@email.com", - ServiceAccountJSON: "test.json", - Groups: []string{"1", "2"}, - }, - LoginURLParameters: defaultURLParams, - } - - internalConfigLegacyProvider := LegacyProvider{ - ClientID: clientID, - ProviderType: "google", - GoogleAdminEmail: "email@email.com", - GoogleServiceAccountJSON: "test.json", - GoogleGroups: []string{"1", "2"}, - } - - legacyConfigLegacyProvider := LegacyProvider{ - ClientID: clientID, - ProviderType: "google", - GoogleAdminEmail: "email@email.com", - GoogleServiceAccountJSON: "test.json", - GoogleGroupsLegacy: []string{"1", "2"}, - } - DescribeTable("convertLegacyProviders", - func(in *convertProvidersTableInput) { - providers, err := in.legacyProvider.convert() - - if in.errMsg != "" { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(Equal(in.errMsg)) - } else { - Expect(err).ToNot(HaveOccurred()) - } - - Expect(providers).To(ConsistOf(in.expectedProviders)) - }, - Entry("with default provider", &convertProvidersTableInput{ - legacyProvider: defaultLegacyProvider, - expectedProviders: Providers{defaultProvider}, - errMsg: "", - }), - Entry("with prompt setting", &convertProvidersTableInput{ - legacyProvider: defaultLegacyProviderWithPrompt, - expectedProviders: Providers{defaultProviderWithPrompt}, - errMsg: "", - }), - Entry("with provider display name", &convertProvidersTableInput{ - legacyProvider: displayNameLegacyProvider, - expectedProviders: Providers{displayNameProvider}, - errMsg: "", - }), - Entry("with internal provider config", &convertProvidersTableInput{ - legacyProvider: internalConfigLegacyProvider, - expectedProviders: Providers{internalConfigProvider}, - errMsg: "", - }), - Entry("with legacy provider config", &convertProvidersTableInput{ - legacyProvider: legacyConfigLegacyProvider, - expectedProviders: Providers{internalConfigProvider}, - errMsg: "", - }), - ) - }) -}) diff --git a/pkg/apis/options/load.go b/pkg/apis/options/load.go deleted file mode 100644 index 1f22dfbc9f..0000000000 --- a/pkg/apis/options/load.go +++ /dev/null @@ -1,177 +0,0 @@ -package options - -import ( - "errors" - "fmt" - "os" - "reflect" - "strings" - - "github.com/a8m/envsubst" - "github.com/ghodss/yaml" - "github.com/mitchellh/mapstructure" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -// Load reads in the config file at the path given, then merges in environment -// variables (prefixed with `OAUTH2_PROXY`) and finally merges in flags from the flagSet. -// If a config value is unset and the flag has a non-zero value default, this default will be used. -// Eg. A field defined: -// -// FooBar `cfg:"foo_bar" flag:"foo-bar"` -// -// Can be set in the config file as `foo_bar="baz"`, in the environment as `OAUTH2_PROXY_FOO_BAR=baz`, -// or via the command line flag `--foo-bar=baz`. -func Load(configFileName string, flagSet *pflag.FlagSet, into interface{}) error { - v := viper.New() - v.SetConfigFile(configFileName) - v.SetConfigType("toml") // Config is in toml format - v.SetEnvPrefix("OAUTH2_PROXY") - v.AutomaticEnv() - v.SetTypeByDefaultValue(true) - - if configFileName != "" { - err := v.ReadInConfig() - if err != nil { - return fmt.Errorf("unable to load config file: %w", err) - } - } - - err := registerFlags(v, "", flagSet, into) - if err != nil { - // This should only happen if there is a programming error - return fmt.Errorf("unable to register flags: %w", err) - } - - // UnmarshalExact will return an error if the config includes options that are - // not mapped to fields of the into struct - err = v.UnmarshalExact(into, decodeFromCfgTag) - if err != nil { - return fmt.Errorf("error unmarshalling config: %w", err) - } - - return nil -} - -// registerFlags uses `cfg` and `flag` tags to associate flags in the flagSet -// to the fields in the options interface provided. -// Each exported field in the options must have a `cfg` tag otherwise an error will occur. -// - For fields, set `cfg` and `flag` so that `flag` is the name of the flag associated to this config option -// - For exported fields that are not user facing, set the `cfg` to `,internal` -// - For structs containing user facing fields, set the `cfg` to `,squash` -func registerFlags(v *viper.Viper, prefix string, flagSet *pflag.FlagSet, options interface{}) error { - val := reflect.ValueOf(options) - var typ reflect.Type - if val.Kind() == reflect.Ptr { - typ = val.Elem().Type() - } else { - typ = val.Type() - } - - for i := 0; i < typ.NumField(); i++ { - // pull out the struct tags: - // flag - the name of the command line flag - // cfg - the name of the config file option - field := typ.Field(i) - fieldV := reflect.Indirect(val).Field(i) - fieldName := strings.Join([]string{prefix, field.Name}, ".") - - cfgName := field.Tag.Get("cfg") - if cfgName == ",internal" { - // Public but internal types that should not be exposed to users, skip them - continue - } - - if isUnexported(field.Name) { - // Unexported fields cannot be set by a user, so won't have tags or flags, skip them - continue - } - - if field.Type.Kind() == reflect.Struct { - if cfgName != ",squash" { - return fmt.Errorf("field %q does not have required cfg tag: `,squash`", fieldName) - } - err := registerFlags(v, fieldName, flagSet, fieldV.Interface()) - if err != nil { - return err - } - continue - } - - flagName := field.Tag.Get("flag") - if flagName == "" || cfgName == "" { - return fmt.Errorf("field %q does not have required tags (cfg, flag)", fieldName) - } - - if flagSet == nil { - return fmt.Errorf("flagset cannot be nil") - } - - f := flagSet.Lookup(flagName) - if f == nil { - return fmt.Errorf("field %q does not have a registered flag", flagName) - } - err := v.BindPFlag(cfgName, f) - if err != nil { - return fmt.Errorf("error binding flag for field %q: %w", fieldName, err) - } - } - - return nil -} - -// decodeFromCfgTag sets the Viper decoder to read the names from the `cfg` tag -// on each struct entry. -func decodeFromCfgTag(c *mapstructure.DecoderConfig) { - c.TagName = "cfg" -} - -// isUnexported checks if a field name starts with a lowercase letter and therefore -// if it is unexported. -func isUnexported(name string) bool { - if len(name) == 0 { - // This should never happen - panic("field name has len 0") - } - - first := string(name[0]) - return first == strings.ToLower(first) -} - -// LoadYAML will load a YAML based configuration file into the options interface provided. -func LoadYAML(configFileName string, into interface{}) error { - buffer, err := loadAndParseYaml(configFileName) - if err != nil { - return err - } - - // UnmarshalStrict will return an error if the config includes options that are - // not mapped to fields of the into struct - if err := yaml.UnmarshalStrict(buffer, into, yaml.DisallowUnknownFields); err != nil { - return fmt.Errorf("error unmarshalling config: %w", err) - } - - return nil -} - -// Performs the heavy lifting of the LoadYaml function -func loadAndParseYaml(configFileName string) ([]byte, error) { - if configFileName == "" { - return nil, errors.New("no configuration file provided") - } - - unparsedBuffer, err := os.ReadFile(configFileName) - if err != nil { - return nil, fmt.Errorf("unable to load config file: %w", err) - } - - // We now parse over the yaml with env substring, and fill in the ENV's - buffer, err := envsubst.Bytes(unparsedBuffer) - if err != nil { - return nil, fmt.Errorf("error in substituting env variables : %w", err) - } - - return buffer, nil - -} diff --git a/pkg/apis/options/load_test.go b/pkg/apis/options/load_test.go deleted file mode 100644 index 3cf1dad44e..0000000000 --- a/pkg/apis/options/load_test.go +++ /dev/null @@ -1,565 +0,0 @@ -package options - -import ( - "errors" - "fmt" - "os" - "time" - - . "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/testutil" - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - "github.com/spf13/pflag" -) - -var _ = Describe("Load", func() { - optionsWithNilProvider := NewOptions() - optionsWithNilProvider.Providers = nil - - legacyOptionsWithNilProvider := &LegacyOptions{ - LegacyUpstreams: LegacyUpstreams{ - PassHostHeader: true, - ProxyWebSockets: true, - FlushInterval: DefaultUpstreamFlushInterval, - Timeout: DefaultUpstreamTimeout, - }, - - LegacyHeaders: LegacyHeaders{ - PassBasicAuth: true, - PassUserHeaders: true, - SkipAuthStripHeaders: true, - }, - - LegacyServer: LegacyServer{ - HTTPAddress: "127.0.0.1:4180", - HTTPSAddress: ":443", - }, - - LegacyProvider: LegacyProvider{ - ProviderType: "google", - AzureTenant: "common", - ApprovalPrompt: "force", - UserIDClaim: "email", - OIDCEmailClaim: "email", - OIDCGroupsClaim: "groups", - OIDCAudienceClaims: []string{"aud"}, - InsecureOIDCSkipNonce: true, - }, - - Options: Options{ - ProxyPrefix: "/oauth2", - PingPath: "/ping", - ReadyPath: "/ready", - RealClientIPHeader: "X-Real-IP", - ForceHTTPS: false, - Cookie: cookieDefaults(), - Session: sessionOptionsDefaults(), - Templates: templatesDefaults(), - SkipAuthPreflight: false, - Logging: loggingDefaults(), - }, - } - - Context("with a testOptions structure", func() { - type TestOptionSubStruct struct { - StringSliceOption []string `flag:"string-slice-option" cfg:"string_slice_option"` - } - - type TestOptions struct { - StringOption string `flag:"string-option" cfg:"string_option"` - Sub TestOptionSubStruct `cfg:",squash"` - // Check exported but internal fields do not break loading - Internal *string `cfg:",internal"` - // Check unexported fields do not break loading - unexported string - } - - type MissingSquashTestOptions struct { - StringOption string `flag:"string-option" cfg:"string_option"` - Sub TestOptionSubStruct - } - - type MissingCfgTestOptions struct { - StringOption string `flag:"string-option"` - Sub TestOptionSubStruct `cfg:",squash"` - } - - type MissingFlagTestOptions struct { - StringOption string `cfg:"string_option"` - Sub TestOptionSubStruct `cfg:",squash"` - } - - var testOptionsConfigBytes = []byte(` - string_option="foo" - string_slice_option="a,b,c,d" - `) - - var testOptionsFlagSet *pflag.FlagSet - - type testOptionsTableInput struct { - env map[string]string - args []string - configFile []byte - flagSet func() *pflag.FlagSet - expectedErr error - input interface{} - expectedOutput interface{} - } - - BeforeEach(func() { - testOptionsFlagSet = pflag.NewFlagSet("testFlagSet", pflag.ExitOnError) - testOptionsFlagSet.String("string-option", "default", "") - testOptionsFlagSet.StringSlice("string-slice-option", []string{"a", "b"}, "") - }) - - DescribeTable("Load", - func(o *testOptionsTableInput) { - var configFileName string - - if o.configFile != nil { - By("Creating a config file") - configFile, err := os.CreateTemp("", "oauth2-proxy-test-legacy-config-file") - Expect(err).ToNot(HaveOccurred()) - defer configFile.Close() - - _, err = configFile.Write(o.configFile) - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(configFile.Name()) - - configFileName = configFile.Name() - } - - if len(o.env) > 0 { - By("Setting environment variables") - for k, v := range o.env { - os.Setenv(k, v) - defer os.Unsetenv(k) - } - } - - Expect(o.flagSet).ToNot(BeNil()) - flagSet := o.flagSet() - Expect(flagSet).ToNot(BeNil()) - - if len(o.args) > 0 { - By("Parsing flag arguments") - Expect(flagSet.Parse(o.args)).To(Succeed()) - } - - var input interface{} - if o.input != nil { - input = o.input - } else { - input = &TestOptions{} - } - err := Load(configFileName, flagSet, input) - if o.expectedErr != nil { - Expect(err).To(MatchError(o.expectedErr.Error())) - } else { - Expect(err).ToNot(HaveOccurred()) - } - Expect(input).To(EqualOpts(o.expectedOutput)) - }, - Entry("with just a config file", &testOptionsTableInput{ - configFile: testOptionsConfigBytes, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "foo", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c", "d"}, - }, - }, - }), - Entry("when setting env variables", &testOptionsTableInput{ - configFile: testOptionsConfigBytes, - env: map[string]string{ - "OAUTH2_PROXY_STRING_OPTION": "bar", - "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", - }, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "bar", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c"}, - }, - }, - }), - Entry("when setting flags", &testOptionsTableInput{ - configFile: testOptionsConfigBytes, - env: map[string]string{ - "OAUTH2_PROXY_STRING_OPTION": "bar", - "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", - }, - args: []string{ - "--string-option", "baz", - "--string-slice-option", "a,b,c,d,e", - }, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "baz", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c", "d", "e"}, - }, - }, - }), - Entry("when setting flags multiple times", &testOptionsTableInput{ - configFile: testOptionsConfigBytes, - env: map[string]string{ - "OAUTH2_PROXY_STRING_OPTION": "bar", - "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", - }, - args: []string{ - "--string-option", "baz", - "--string-slice-option", "x", - "--string-slice-option", "y", - "--string-slice-option", "z", - }, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "baz", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"x", "y", "z"}, - }, - }, - }), - Entry("when setting env variables without a config file", &testOptionsTableInput{ - env: map[string]string{ - "OAUTH2_PROXY_STRING_OPTION": "bar", - "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", - }, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "bar", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c"}, - }, - }, - }), - Entry("when setting flags without a config file", &testOptionsTableInput{ - env: map[string]string{ - "OAUTH2_PROXY_STRING_OPTION": "bar", - "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", - }, - args: []string{ - "--string-option", "baz", - "--string-slice-option", "a,b,c,d,e", - }, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "baz", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c", "d", "e"}, - }, - }, - }), - Entry("when setting flags without a config file", &testOptionsTableInput{ - env: map[string]string{ - "OAUTH2_PROXY_STRING_OPTION": "bar", - "OAUTH2_PROXY_STRING_SLICE_OPTION": "a,b,c", - }, - args: []string{ - "--string-option", "baz", - "--string-slice-option", "a,b,c,d,e", - }, - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "baz", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c", "d", "e"}, - }, - }, - }), - Entry("when nothing is set it should use flag defaults", &testOptionsTableInput{ - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedOutput: &TestOptions{ - StringOption: "default", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b"}, - }, - }, - }), - Entry("with an invalid config file", &testOptionsTableInput{ - configFile: []byte(`slice_option = foo`), - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedErr: fmt.Errorf("unable to load config file: While parsing config: toml: expected 'false'"), - expectedOutput: &TestOptions{}, - }), - Entry("with an invalid flagset", &testOptionsTableInput{ - flagSet: func() *pflag.FlagSet { - // Missing a flag - f := pflag.NewFlagSet("testFlagSet", pflag.ExitOnError) - f.String("string-option", "default", "") - return f - }, - expectedErr: fmt.Errorf("unable to register flags: field \"string-slice-option\" does not have a registered flag"), - expectedOutput: &TestOptions{}, - }), - Entry("with an struct is missing the squash tag", &testOptionsTableInput{ - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedErr: fmt.Errorf("unable to register flags: field \".Sub\" does not have required cfg tag: `,squash`"), - input: &MissingSquashTestOptions{}, - expectedOutput: &MissingSquashTestOptions{}, - }), - Entry("with a field is missing the cfg tag", &testOptionsTableInput{ - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedErr: fmt.Errorf("unable to register flags: field \".StringOption\" does not have required tags (cfg, flag)"), - input: &MissingCfgTestOptions{}, - expectedOutput: &MissingCfgTestOptions{}, - }), - Entry("with a field is missing the flag tag", &testOptionsTableInput{ - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedErr: fmt.Errorf("unable to register flags: field \".StringOption\" does not have required tags (cfg, flag)"), - input: &MissingFlagTestOptions{}, - expectedOutput: &MissingFlagTestOptions{}, - }), - Entry("with existing unexported fields", &testOptionsTableInput{ - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - input: &TestOptions{ - unexported: "unexported", - }, - expectedOutput: &TestOptions{ - StringOption: "default", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b"}, - }, - unexported: "unexported", - }, - }), - Entry("with an unknown option in the config file", &testOptionsTableInput{ - configFile: []byte(`unknown_option="foo"`), - flagSet: func() *pflag.FlagSet { return testOptionsFlagSet }, - expectedErr: fmt.Errorf("error unmarshalling config: 1 error(s) decoding:\n\n* '' has invalid keys: unknown_option"), - // Viper will unmarshal before returning the error, so this is the default output - expectedOutput: &TestOptions{ - StringOption: "default", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b"}, - }, - }, - }), - Entry("with an empty Options struct, should return default values", &testOptionsTableInput{ - flagSet: NewFlagSet, - input: &Options{}, - expectedOutput: optionsWithNilProvider, - }), - Entry("with an empty LegacyOptions struct, should return default values", &testOptionsTableInput{ - flagSet: NewLegacyFlagSet, - input: &LegacyOptions{}, - expectedOutput: legacyOptionsWithNilProvider, - }), - ) - }) -}) - -var _ = Describe("LoadYAML", func() { - Context("with a testOptions structure", func() { - type TestOptionSubStruct struct { - StringSliceOption []string `yaml:"stringSliceOption,omitempty"` - } - - type TestOptions struct { - StringOption string `yaml:"stringOption,omitempty"` - Sub TestOptionSubStruct `yaml:"sub,omitempty"` - - // Check that embedded fields can be unmarshalled - TestOptionSubStruct `yaml:",inline,squash"` - } - - var testOptionsConfigBytesFull = []byte(` -stringOption: foo -stringSliceOption: -- a -- b -- c -sub: - stringSliceOption: - - d - - e -`) - - type loadYAMLTableInput struct { - configFile []byte - input interface{} - expectedErr error - expectedOutput interface{} - } - - DescribeTable("LoadYAML", - func(in loadYAMLTableInput) { - // Set the required environment variables before running the test - os.Setenv("TESTUSER", "Alice") - - // Unset the environment variables after running the test - defer os.Unsetenv("TESTUSER") - - var configFileName string - if in.configFile != nil { - By("Creating a config file") - configFile, err := os.CreateTemp("", "oauth2-proxy-test-config-file") - Expect(err).ToNot(HaveOccurred()) - defer configFile.Close() - - _, err = configFile.Write(in.configFile) - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(configFile.Name()) - - configFileName = configFile.Name() - } - - var input interface{} - if in.input != nil { - input = in.input - } else { - input = &TestOptions{} - } - - err := LoadYAML(configFileName, input) - if in.expectedErr != nil { - Expect(err).To(MatchError(in.expectedErr.Error())) - } else { - Expect(err).ToNot(HaveOccurred()) - } - Expect(input).To(EqualOpts(in.expectedOutput)) - }, - Entry("with a valid input", loadYAMLTableInput{ - configFile: testOptionsConfigBytesFull, - input: &TestOptions{}, - expectedOutput: &TestOptions{ - StringOption: "foo", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"d", "e"}, - }, - TestOptionSubStruct: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c"}, - }, - }, - }), - Entry("with no config file", loadYAMLTableInput{ - configFile: nil, - input: &TestOptions{}, - expectedOutput: &TestOptions{}, - expectedErr: errors.New("no configuration file provided"), - }), - Entry("with invalid YAML", loadYAMLTableInput{ - configFile: []byte("\tfoo: bar"), - input: &TestOptions{}, - expectedOutput: &TestOptions{}, - expectedErr: errors.New("error unmarshalling config: error converting YAML to JSON: yaml: found character that cannot start any token"), - }), - Entry("with extra fields in the YAML", loadYAMLTableInput{ - configFile: append(testOptionsConfigBytesFull, []byte("foo: bar\n")...), - input: &TestOptions{}, - expectedOutput: &TestOptions{ - StringOption: "foo", - Sub: TestOptionSubStruct{ - StringSliceOption: []string{"d", "e"}, - }, - TestOptionSubStruct: TestOptionSubStruct{ - StringSliceOption: []string{"a", "b", "c"}, - }, - }, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: unknown field \"foo\""), - }), - Entry("with an incorrect type for a string field", loadYAMLTableInput{ - configFile: []byte(`stringOption: ["a", "b"]`), - input: &TestOptions{}, - expectedOutput: &TestOptions{}, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal array into Go struct field TestOptions.StringOption of type string"), - }), - Entry("with an incorrect type for an array field", loadYAMLTableInput{ - configFile: []byte(`stringSliceOption: "a"`), - input: &TestOptions{}, - expectedOutput: &TestOptions{}, - expectedErr: errors.New("error unmarshalling config: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go struct field TestOptions.StringSliceOption of type []string"), - }), - Entry("with a config file containing environment variable references", loadYAMLTableInput{ - configFile: []byte("stringOption: ${TESTUSER}"), - input: &TestOptions{}, - expectedOutput: &TestOptions{ - StringOption: "Alice", - }, - }), - Entry("with a config file containing env variable references, with a fallback value", loadYAMLTableInput{ - configFile: []byte("stringOption: ${TESTUSER2=Bob}"), - input: &TestOptions{}, - expectedOutput: &TestOptions{ - StringOption: "Bob", - }, - }), - ) - }) - - It("should load a full example AlphaOptions", func() { - config := []byte(` -upstreamConfig: - upstreams: - - id: httpbin - path: / - uri: http://httpbin - flushInterval: 500ms -injectRequestHeaders: -- name: X-Forwarded-User - values: - - claim: user -injectResponseHeaders: -- name: X-Secret - values: - - value: c2VjcmV0 -`) - - By("Creating a config file") - configFile, err := os.CreateTemp("", "oauth2-proxy-test-alpha-config-file") - Expect(err).ToNot(HaveOccurred()) - defer configFile.Close() - - _, err = configFile.Write(config) - Expect(err).ToNot(HaveOccurred()) - defer os.Remove(configFile.Name()) - - configFileName := configFile.Name() - - By("Loading the example config") - into := &AlphaOptions{} - Expect(LoadYAML(configFileName, into)).To(Succeed()) - - flushInterval := Duration(500 * time.Millisecond) - - Expect(into).To(Equal(&AlphaOptions{ - UpstreamConfig: UpstreamConfig{ - Upstreams: []Upstream{ - { - ID: "httpbin", - Path: "/", - URI: "http://httpbin", - FlushInterval: &flushInterval, - }, - }, - }, - InjectRequestHeaders: []Header{ - { - Name: "X-Forwarded-User", - Values: []HeaderValue{ - { - ClaimSource: &ClaimSource{ - Claim: "user", - }, - }, - }, - }, - }, - InjectResponseHeaders: []Header{ - { - Name: "X-Secret", - Values: []HeaderValue{ - { - SecretSource: &SecretSource{ - Value: []byte("secret"), - }, - }, - }, - }, - }, - })) - }) -}) diff --git a/pkg/apis/options/logging.go b/pkg/apis/options/logging.go deleted file mode 100644 index dfffd0fa4b..0000000000 --- a/pkg/apis/options/logging.go +++ /dev/null @@ -1,80 +0,0 @@ -package options - -import ( - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - "github.com/spf13/pflag" -) - -// Logging contains all options required for configuring the logging -type Logging struct { - AuthEnabled bool `flag:"auth-logging" cfg:"auth_logging"` - AuthFormat string `flag:"auth-logging-format" cfg:"auth_logging_format"` - RequestEnabled bool `flag:"request-logging" cfg:"request_logging"` - RequestFormat string `flag:"request-logging-format" cfg:"request_logging_format"` - StandardEnabled bool `flag:"standard-logging" cfg:"standard_logging"` - StandardFormat string `flag:"standard-logging-format" cfg:"standard_logging_format"` - ErrToInfo bool `flag:"errors-to-info-log" cfg:"errors_to_info_log"` - ExcludePaths []string `flag:"exclude-logging-path" cfg:"exclude_logging_paths"` - LocalTime bool `flag:"logging-local-time" cfg:"logging_local_time"` - SilencePing bool `flag:"silence-ping-logging" cfg:"silence_ping_logging"` - RequestIDHeader string `flag:"request-id-header" cfg:"request_id_header"` - File LogFileOptions `cfg:",squash"` -} - -// LogFileOptions contains options for configuring logging to a file -type LogFileOptions struct { - Filename string `flag:"logging-filename" cfg:"logging_filename"` - MaxSize int `flag:"logging-max-size" cfg:"logging_max_size"` - MaxAge int `flag:"logging-max-age" cfg:"logging_max_age"` - MaxBackups int `flag:"logging-max-backups" cfg:"logging_max_backups"` - Compress bool `flag:"logging-compress" cfg:"logging_compress"` -} - -func loggingFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("logging", pflag.ExitOnError) - - flagSet.Bool("auth-logging", true, "Log authentication attempts") - flagSet.String("auth-logging-format", logger.DefaultAuthLoggingFormat, "Template for authentication log lines") - flagSet.Bool("standard-logging", true, "Log standard runtime information") - flagSet.String("standard-logging-format", logger.DefaultStandardLoggingFormat, "Template for standard log lines") - flagSet.Bool("request-logging", true, "Log HTTP requests") - flagSet.String("request-logging-format", logger.DefaultRequestLoggingFormat, "Template for HTTP request log lines") - flagSet.Bool("errors-to-info-log", false, "Log errors to the standard logging channel instead of stderr") - - flagSet.StringSlice("exclude-logging-path", []string{}, "Exclude logging requests to paths (eg: '/path1,/path2,/path3')") - flagSet.Bool("logging-local-time", true, "If the time in log files and backup filenames are local or UTC time") - flagSet.Bool("silence-ping-logging", false, "Disable logging of requests to ping & ready endpoints") - flagSet.String("request-id-header", "X-Request-Id", "Request header to use as the request ID") - - flagSet.String("logging-filename", "", "File to log requests to, empty for stdout") - flagSet.Int("logging-max-size", 100, "Maximum size in megabytes of the log file before rotation") - flagSet.Int("logging-max-age", 7, "Maximum number of days to retain old log files") - flagSet.Int("logging-max-backups", 0, "Maximum number of old log files to retain; 0 to disable") - flagSet.Bool("logging-compress", false, "Should rotated log files be compressed using gzip") - - return flagSet -} - -// loggingDefaults creates a Logging structure, populating each field with its default value -func loggingDefaults() Logging { - return Logging{ - ExcludePaths: nil, - LocalTime: true, - SilencePing: false, - RequestIDHeader: "X-Request-Id", - AuthEnabled: true, - AuthFormat: logger.DefaultAuthLoggingFormat, - RequestEnabled: true, - RequestFormat: logger.DefaultRequestLoggingFormat, - StandardEnabled: true, - StandardFormat: logger.DefaultStandardLoggingFormat, - ErrToInfo: false, - File: LogFileOptions{ - Filename: "", - MaxSize: 100, - MaxAge: 7, - MaxBackups: 0, - Compress: false, - }, - } -} diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index 15a2df7501..a3fc729c8d 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -3,10 +3,7 @@ package options import ( "crypto" "net/url" - - ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip" - internaloidc "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/providers/oidc" - "github.com/spf13/pflag" + "time" ) // SignatureData holds hmacauth signature hash and key @@ -18,150 +15,50 @@ type SignatureData struct { // Options holds Configuration Options that can be set by Command Line Flag, // or Config File type Options struct { - ProxyPrefix string `flag:"proxy-prefix" cfg:"proxy_prefix"` - PingPath string `flag:"ping-path" cfg:"ping_path"` - PingUserAgent string `flag:"ping-user-agent" cfg:"ping_user_agent"` - ReadyPath string `flag:"ready-path" cfg:"ready_path"` - ReverseProxy bool `flag:"reverse-proxy" cfg:"reverse_proxy"` - RealClientIPHeader string `flag:"real-client-ip-header" cfg:"real_client_ip_header"` - TrustedIPs []string `flag:"trusted-ip" cfg:"trusted_ips"` - ForceHTTPS bool `flag:"force-https" cfg:"force_https"` - RawRedirectURL string `flag:"redirect-url" cfg:"redirect_url"` - RelativeRedirectURL bool `flag:"relative-redirect-url" cfg:"relative_redirect_url"` - - AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` - EmailDomains []string `flag:"email-domain" cfg:"email_domains"` - WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains"` - HtpasswdFile string `flag:"htpasswd-file" cfg:"htpasswd_file"` - HtpasswdUserGroups []string `flag:"htpasswd-user-group" cfg:"htpasswd_user_groups"` - - Cookie Cookie `cfg:",squash"` - Session SessionOptions `cfg:",squash"` - Logging Logging `cfg:",squash"` - Templates Templates `cfg:",squash"` - - // Not used in the legacy config, name not allowed to match an external key (upstreams) - // TODO(JoelSpeed): Rename when legacy config is removed - UpstreamServers UpstreamConfig `cfg:",internal"` + ProxyPrefix string `mapstructure:"proxy_prefix"` + ReverseProxy bool `mapstructure:"reverse_proxy"` + RawRedirectURL string `mapstructure:"redirect_url"` + RelativeRedirectURL bool `mapstructure:"relative_redirect_url"` - InjectRequestHeaders []Header `cfg:",internal"` - InjectResponseHeaders []Header `cfg:",internal"` + WhitelistDomains []string `mapstructure:"whitelist_domains"` - Server Server `cfg:",internal"` - MetricsServer Server `cfg:",internal"` + Cookie Cookie `mapstructure:",squash"` + Session SessionOptions `mapstructure:",squash"` + Service Service `mapstructure:",squash"` + MatchRules MatchRules `mapstructure:",squash"` - Providers Providers `cfg:",internal"` + Providers Providers - APIRoutes []string `flag:"api-route" cfg:"api_routes"` - SkipAuthRegex []string `flag:"skip-auth-regex" cfg:"skip_auth_regex"` - SkipAuthRoutes []string `flag:"skip-auth-route" cfg:"skip_auth_routes"` - SkipJwtBearerTokens bool `flag:"skip-jwt-bearer-tokens" cfg:"skip_jwt_bearer_tokens"` - ExtraJwtIssuers []string `flag:"extra-jwt-issuers" cfg:"extra_jwt_issuers"` - SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"` - SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"` - SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"` - ForceJSONErrors bool `flag:"force-json-errors" cfg:"force_json_errors"` - EncodeState bool `flag:"encode-state" cfg:"encode_state"` - AllowQuerySemicolons bool `flag:"allow-query-semicolons" cfg:"allow_query_semicolons"` - - SignatureKey string `flag:"signature-key" cfg:"signature_key"` - GCPHealthChecks bool `flag:"gcp-healthchecks" cfg:"gcp_healthchecks"` - - // This is used for backwards compatibility for basic auth users - LegacyPreferEmailToUser bool `cfg:",internal"` + SkipAuthPreflight bool `mapstructure:"skip_auth_preflight"` + EncodeState bool `mapstructure:"encode_state"` + PassAuthorization bool `mapstructure:"pass_authorization_header"` + VerifierInterval time.Duration `mapstructure:"verifier_interval"` + UpdateKeysInterval time.Duration `mapstructure:"update_keys_interval"` // internal values that are set after config validation - redirectURL *url.URL - signatureData *SignatureData - oidcVerifier internaloidc.IDTokenVerifier - jwtBearerVerifiers []internaloidc.IDTokenVerifier - realClientIPParser ipapi.RealClientIPParser + redirectURL *url.URL // 私有字段通常不需要 mapstructure 标签 } // Options for Getting internal values -func (o *Options) GetRedirectURL() *url.URL { return o.redirectURL } -func (o *Options) GetSignatureData() *SignatureData { return o.signatureData } -func (o *Options) GetOIDCVerifier() internaloidc.IDTokenVerifier { return o.oidcVerifier } -func (o *Options) GetJWTBearerVerifiers() []internaloidc.IDTokenVerifier { - return o.jwtBearerVerifiers -} -func (o *Options) GetRealClientIPParser() ipapi.RealClientIPParser { return o.realClientIPParser } +func (o *Options) GetRedirectURL() *url.URL { return o.redirectURL } // Options for Setting internal values -func (o *Options) SetRedirectURL(s *url.URL) { o.redirectURL = s } -func (o *Options) SetSignatureData(s *SignatureData) { o.signatureData = s } -func (o *Options) SetOIDCVerifier(s internaloidc.IDTokenVerifier) { o.oidcVerifier = s } -func (o *Options) SetJWTBearerVerifiers(s []internaloidc.IDTokenVerifier) { o.jwtBearerVerifiers = s } -func (o *Options) SetRealClientIPParser(s ipapi.RealClientIPParser) { o.realClientIPParser = s } +func (o *Options) SetRedirectURL(s *url.URL) { + o.redirectURL = s + o.MatchRules.RedirectURL = s +} // NewOptions constructs a new Options with defaulted values func NewOptions() *Options { return &Options{ ProxyPrefix: "/oauth2", Providers: providerDefaults(), - PingPath: "/ping", - ReadyPath: "/ready", - RealClientIPHeader: "X-Real-IP", - ForceHTTPS: false, Cookie: cookieDefaults(), Session: sessionOptionsDefaults(), - Templates: templatesDefaults(), SkipAuthPreflight: false, - Logging: loggingDefaults(), + PassAuthorization: true, + VerifierInterval: 2 * time.Second, // 5 seconds + UpdateKeysInterval: 24 * time.Hour, // 24 hours + MatchRules: matchRulesDefaults(), } } - -// NewFlagSet creates a new FlagSet with all of the flags required by Options -func NewFlagSet() *pflag.FlagSet { - flagSet := pflag.NewFlagSet("oauth2-proxy", pflag.ExitOnError) - - flagSet.Bool("reverse-proxy", false, "are we running behind a reverse proxy, controls whether headers like X-Real-Ip are accepted") - flagSet.String("real-client-ip-header", "X-Real-IP", "Header used to determine the real IP of the client (one of: X-Forwarded-For, X-Real-IP, or X-ProxyUser-IP)") - flagSet.StringSlice("trusted-ip", []string{}, "list of IPs or CIDR ranges to allow to bypass authentication. WARNING: trusting by IP has inherent security flaws, read the configuration documentation for more information.") - flagSet.Bool("force-https", false, "force HTTPS redirect for HTTP requests") - flagSet.String("redirect-url", "", "the OAuth Redirect URL. ie: \"https://internalapp.yourcompany.com/oauth2/callback\"") - flagSet.Bool("relative-redirect-url", false, "allow relative OAuth Redirect URL.") - flagSet.StringSlice("skip-auth-regex", []string{}, "(DEPRECATED for --skip-auth-route) bypass authentication for requests path's that match (may be given multiple times)") - flagSet.StringSlice("skip-auth-route", []string{}, "bypass authentication for requests that match the method & path. Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex") - flagSet.StringSlice("api-route", []string{}, "return HTTP 401 instead of redirecting to authentication server if token is not valid. Format: path_regex") - flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start") - flagSet.Bool("skip-auth-preflight", false, "will skip authentication for OPTIONS requests") - flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers") - flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)") - flagSet.Bool("force-json-errors", false, "will force JSON errors instead of HTTP error pages or redirects") - flagSet.Bool("encode-state", false, "will encode oauth state with base64") - flagSet.Bool("allow-query-semicolons", false, "allow the use of semicolons in query args") - flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)") - - flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") - flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . or a *. to allow subdomains (eg .example.com, *.example.com)") - flagSet.String("authenticated-emails-file", "", "authenticate against emails via file (one per line)") - flagSet.String("htpasswd-file", "", "additionally authenticate against a htpasswd file. Entries must be created with \"htpasswd -B\" for bcrypt encryption") - flagSet.StringSlice("htpasswd-user-group", []string{}, "the groups to be set on sessions for htpasswd users (may be given multiple times)") - flagSet.String("proxy-prefix", "/oauth2", "the url root path that this proxy should be nested under (e.g. //sign_in)") - flagSet.String("ping-path", "/ping", "the ping endpoint that can be used for basic health checks") - flagSet.String("ping-user-agent", "", "special User-Agent that will be used for basic health checks") - flagSet.String("ready-path", "/ready", "the ready endpoint that can be used for deep health checks") - flagSet.String("session-store-type", "cookie", "the session storage provider to use") - flagSet.Bool("session-cookie-minimal", false, "strip OAuth tokens from cookie session stores if they aren't needed (cookie session store only)") - flagSet.String("redis-connection-url", "", "URL of redis server for redis session storage (eg: redis://[USER[:PASSWORD]@]HOST[:PORT])") - flagSet.String("redis-username", "", "Redis username. Applicable for Redis configurations where ACL has been configured. Will override any username set in `--redis-connection-url`") - flagSet.String("redis-password", "", "Redis password. Applicable for all Redis configurations. Will override any password set in `--redis-connection-url`") - flagSet.Bool("redis-use-sentinel", false, "Connect to redis via sentinels. Must set --redis-sentinel-master-name and --redis-sentinel-connection-urls to use this feature") - flagSet.String("redis-sentinel-password", "", "Redis sentinel password. Used only for sentinel connection; any redis node passwords need to use `--redis-password`") - flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel") - flagSet.String("redis-ca-path", "", "Redis custom CA path") - flagSet.Bool("redis-insecure-skip-tls-verify", false, "Use insecure TLS connection to redis") - flagSet.StringSlice("redis-sentinel-connection-urls", []string{}, "List of Redis sentinel connection URLs (eg redis://[USER[:PASSWORD]@]HOST[:PORT]). Used in conjunction with --redis-use-sentinel") - flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature") - flagSet.StringSlice("redis-cluster-connection-urls", []string{}, "List of Redis cluster connection URLs (eg redis://[USER[:PASSWORD]@]HOST[:PORT]). Used in conjunction with --redis-use-cluster") - flagSet.Int("redis-connection-idle-timeout", 0, "Redis connection idle timeout seconds, if Redis timeout option is non-zero, the --redis-connection-idle-timeout must be less then Redis timeout option") - flagSet.String("signature-key", "", "GAP-Signature request signature key (algorithm:secretkey)") - flagSet.Bool("gcp-healthchecks", false, "Enable GCP/GKE healthcheck endpoints") - - flagSet.AddFlagSet(cookieFlagSet()) - flagSet.AddFlagSet(loggingFlagSet()) - flagSet.AddFlagSet(templatesFlagSet()) - - return flagSet -} diff --git a/pkg/apis/options/options_suite_test.go b/pkg/apis/options/options_suite_test.go deleted file mode 100644 index 6f678b0351..0000000000 --- a/pkg/apis/options/options_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package options - -import ( - "testing" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestOptionsSuite(t *testing.T) { - logger.SetOutput(GinkgoWriter) - logger.SetErrOutput(GinkgoWriter) - - RegisterFailHandler(Fail) - RunSpecs(t, "Options Suite") -} diff --git a/pkg/apis/options/providers.go b/pkg/apis/options/providers.go index f4e2839d22..2e763256e1 100644 --- a/pkg/apis/options/providers.go +++ b/pkg/apis/options/providers.go @@ -22,30 +22,9 @@ type Provider struct { // ClientSecret is the OAuth Client Secret that is defined in the provider // This value is required for all providers. ClientSecret string `json:"clientSecret,omitempty"` - // ClientSecretFile is the name of the file - // containing the OAuth Client Secret, it will be used if ClientSecret is not set. - ClientSecretFile string `json:"clientSecretFile,omitempty"` - - // KeycloakConfig holds all configurations for Keycloak provider. - KeycloakConfig KeycloakOptions `json:"keycloakConfig,omitempty"` - // AzureConfig holds all configurations for Azure provider. - AzureConfig AzureOptions `json:"azureConfig,omitempty"` - // ADFSConfig holds all configurations for ADFS provider. - ADFSConfig ADFSOptions `json:"ADFSConfig,omitempty"` - // BitbucketConfig holds all configurations for Bitbucket provider. - BitbucketConfig BitbucketOptions `json:"bitbucketConfig,omitempty"` - // GitHubConfig holds all configurations for GitHubC provider. - GitHubConfig GitHubOptions `json:"githubConfig,omitempty"` - // GitLabConfig holds all configurations for GitLab provider. - GitLabConfig GitLabOptions `json:"gitlabConfig,omitempty"` - // GoogleConfig holds all configurations for Google provider. - GoogleConfig GoogleOptions `json:"googleConfig,omitempty"` // OIDCConfig holds all configurations for OIDC provider // or providers utilize OIDC configurations. OIDCConfig OIDCOptions `json:"oidcConfig,omitempty"` - // LoginGovConfig holds all configurations for LoginGov provider. - LoginGovConfig LoginGovOptions `json:"loginGovConfig,omitempty"` - // ID should be a unique identifier for the provider. // This value is required for all providers. ID string `json:"id,omitempty"` @@ -56,12 +35,7 @@ type Provider struct { // Name is the providers display name // if set, it will be shown to the users in the login page. Name string `json:"name,omitempty"` - // CAFiles is a list of paths to CA certificates that should be used when connecting to the provider. - // If not specified, the default Go trust sources are used instead - CAFiles []string `json:"caFiles,omitempty"` - // UseSystemTrustStore determines if your custom CA files and the system trust store are used - // If set to true, your custom CA files and the system trust store are used otherwise only your custom CA files. - UseSystemTrustStore bool `json:"useSystemTrustStore,omitempty"` + // LoginURL is the authentication endpoint LoginURL string `json:"loginURL,omitempty"` // LoginURLParameters defines the parameters that can be passed from the start URL to the IdP login URL @@ -73,8 +47,6 @@ type Provider struct { // SkipClaimsFromProfileURL allows to skip request to Profile URL for resolving claims not present in id_token // default set to 'false' SkipClaimsFromProfileURL bool `json:"skipClaimsFromProfileURL,omitempty"` - // ProtectedResource is the resource that is protected (Azure AD and ADFS only) - ProtectedResource string `json:"resource,omitempty"` // ValidateURL is the access token validation endpoint ValidateURL string `json:"validateURL,omitempty"` // Scope is the OAuth scope specification @@ -83,9 +55,8 @@ type Provider struct { AllowedGroups []string `json:"allowedGroups,omitempty"` // The code challenge method CodeChallengeMethod string `json:"code_challenge_method,omitempty"` - - // URL to call to perform backend logout, `{id_token}` would be replaced by the actual `id_token` if available in the session - BackendLogoutURL string `json:"backendLogoutURL"` + // Client redeem request timeout + RedeemTimeout uint32 `json:"redeemTimeout"` } // ProviderType is used to enumerate the different provider type options @@ -95,121 +66,16 @@ type Provider struct { type ProviderType string const ( - // ADFSProvider is the provider type for ADFS - ADFSProvider ProviderType = "adfs" - - // AzureProvider is the provider type for Azure - AzureProvider ProviderType = "azure" - - // BitbucketProvider is the provider type for Bitbucket - BitbucketProvider ProviderType = "bitbucket" - - // DigitalOceanProvider is the provider type for DigitalOcean - DigitalOceanProvider ProviderType = "digitalocean" - - // FacebookProvider is the provider type for Facebook - FacebookProvider ProviderType = "facebook" - - // GitHubProvider is the provider type for GitHub - GitHubProvider ProviderType = "github" - - // GitLabProvider is the provider type for GitLab - GitLabProvider ProviderType = "gitlab" - - // GoogleProvider is the provider type for GoogleProvider - GoogleProvider ProviderType = "google" - - // KeycloakProvider is the provider type for Keycloak - KeycloakProvider ProviderType = "keycloak" - - // KeycloakOIDCProvider is the provider type for Keycloak OIDC - KeycloakOIDCProvider ProviderType = "keycloak-oidc" - - // LinkedInProvider is the provider type for LinkedIn - LinkedInProvider ProviderType = "linkedin" - - // LoginGovProvider is the provider type for LoginGov - LoginGovProvider ProviderType = "login.gov" - - // NextCloudProvider is the provider type for NextCloud - NextCloudProvider ProviderType = "nextcloud" - // OIDCProvider is the provider type for OIDC OIDCProvider ProviderType = "oidc" -) -type KeycloakOptions struct { - // Group enables to restrict login to members of indicated group - Groups []string `json:"groups,omitempty"` - - // Role enables to restrict login to users with role (only available when using the keycloak-oidc provider) - Roles []string `json:"roles,omitempty"` -} - -type AzureOptions struct { - // Tenant directs to a tenant-specific or common (tenant-independent) endpoint - // Default value is 'common' - Tenant string `json:"tenant,omitempty"` - // GraphGroupField configures the group field to be used when building the groups list from Microsoft Graph - // Default value is 'id' - GraphGroupField string `json:"graphGroupField,omitempty"` -} - -type ADFSOptions struct { - // Skip adding the scope parameter in login request - // Default value is 'false' - SkipScope bool `json:"skipScope,omitempty"` -} - -type BitbucketOptions struct { - // Team sets restrict logins to members of this team - Team string `json:"team,omitempty"` - // Repository sets restrict logins to user with access to this repository - Repository string `json:"repository,omitempty"` -} - -type GitHubOptions struct { - // Org sets restrict logins to members of this organisation - Org string `json:"org,omitempty"` - // Team sets restrict logins to members of this team - Team string `json:"team,omitempty"` - // Repo sets restrict logins to collaborators of this repository - Repo string `json:"repo,omitempty"` - // Token is the token to use when verifying repository collaborators - // it must have push access to the repository - Token string `json:"token,omitempty"` - // Users allows users with these usernames to login - // even if they do not belong to the specified org and team or collaborators - Users []string `json:"users,omitempty"` -} - -type GitLabOptions struct { - // Group sets restrict logins to members of this group - Group []string `json:"group,omitempty"` - // Projects restricts logins to members of these projects - Projects []string `json:"projects,omitempty"` -} - -type GoogleOptions struct { - // Groups sets restrict logins to members of this Google group - Groups []string `json:"group,omitempty"` - // AdminEmail is the Google admin to impersonate for api calls - AdminEmail string `json:"adminEmail,omitempty"` - // ServiceAccountJSON is the path to the service account json credentials - ServiceAccountJSON string `json:"serviceAccountJson,omitempty"` - // UseApplicationDefaultCredentials is a boolean whether to use Application Default Credentials instead of a ServiceAccountJSON - UseApplicationDefaultCredentials bool `json:"useApplicationDefaultCredentials,omitempty"` - // TargetPrincipal is the Google Service Account used for Application Default Credentials - TargetPrincipal string `json:"targetPrincipal,omitempty"` -} + AliyunProvider ProviderType = "aliyun" +) type OIDCOptions struct { // IssuerURL is the OpenID Connect issuer URL // eg: https://accounts.google.com IssuerURL string `json:"issuerURL,omitempty"` - // InsecureAllowUnverifiedEmail prevents failures if an email address in an id_token is not verified - // default set to 'false' - InsecureAllowUnverifiedEmail bool `json:"insecureAllowUnverifiedEmail,omitempty"` // InsecureSkipIssuerVerification skips verification of ID token issuers. When false, ID Token Issuers must match the OIDC discovery URL // default set to 'false' InsecureSkipIssuerVerification bool `json:"insecureSkipIssuerVerification,omitempty"` @@ -240,33 +106,22 @@ type OIDCOptions struct { // ExtraAudiences is a list of additional audiences that are allowed // to pass verification in addition to the client id. ExtraAudiences []string `json:"extraAudiences,omitempty"` -} -type LoginGovOptions struct { - // JWTKey is a private key in PEM format used to sign JWT, - JWTKey string `json:"jwtKey,omitempty"` - // JWTKeyFile is a path to the private key file in PEM format used to sign the JWT - JWTKeyFile string `json:"jwtKeyFile,omitempty"` - // PubJWKURL is the JWK pubkey access endpoint - PubJWKURL string `json:"pubjwkURL,omitempty"` + VerifierRequestTimeout uint32 `json:"verifierTimeout,omitempty"` } func providerDefaults() Providers { providers := Providers{ { - Type: "google", - AzureConfig: AzureOptions{ - Tenant: "common", - }, + Type: "oidc", OIDCConfig: OIDCOptions{ - InsecureAllowUnverifiedEmail: false, - InsecureSkipNonce: true, - SkipDiscovery: false, - UserIDClaim: OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim - EmailClaim: OIDCEmailClaim, - GroupsClaim: OIDCGroupsClaim, - AudienceClaims: OIDCAudienceClaims, - ExtraAudiences: []string{}, + InsecureSkipNonce: true, + SkipDiscovery: false, + UserIDClaim: OIDCEmailClaim, // Deprecated: Use OIDCEmailClaim + EmailClaim: OIDCEmailClaim, + GroupsClaim: OIDCGroupsClaim, + AudienceClaims: OIDCAudienceClaims, + ExtraAudiences: []string{}, }, }, } diff --git a/pkg/apis/options/rule.go b/pkg/apis/options/rule.go new file mode 100644 index 0000000000..4d8816418c --- /dev/null +++ b/pkg/apis/options/rule.go @@ -0,0 +1,92 @@ +package options + +import ( + "net/url" + "strings" + + regexp "github.com/wasilibs/go-re2" +) + +type RuleType string + +const ( + ExactMatch RuleType = "exact" + PrefixMatch RuleType = "prefix" + RegexMatch RuleType = "regex" +) + +type Rule struct { + Domain string `mapstructure:"match_rule_domain"` + Path string `mapstructure:"match_rule_path"` + Rule RuleType `mapstructure:"match_rule_type"` +} + +type MatchRules struct { + Mode string `mapstructure:"match_type"` + RuleList []Rule `mapstructure:"match_list"` + RedirectURL *url.URL +} + +func matchRulesDefaults() MatchRules { + return MatchRules{ + Mode: "whitelist", + RuleList: []Rule{}, + RedirectURL: &url.URL{}, + } +} + +// 将通配符模式转换为正则表达式模式 +func convertWildcardToRegex(pattern string) string { + pattern = regexp.QuoteMeta(pattern) + pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$" + return pattern +} + +func matchPattern(pattern string, target string, rule RuleType) bool { + switch rule { + case ExactMatch: + return pattern == target + case PrefixMatch: + return strings.HasPrefix(target, pattern) + case RegexMatch: + matched, _ := regexp.MatchString(pattern, target) + return matched + default: + return false + } +} + +func matchDomain(domain string, pattern string) bool { + // 将通配符模式转换为正则模式 + regexPattern := convertWildcardToRegex(pattern) + matched, _ := regexp.MatchString(regexPattern, domain) + return matched +} + +func matchDomainAndPath(domain, path string, rule Rule) bool { + return matchDomain(domain, rule.Domain) && matchPattern(rule.Path, path, rule.Rule) +} + +func IsAllowedByMode(domain, path string, config MatchRules, proxyPrefix string) bool { + if domain == config.RedirectURL.Host && strings.HasPrefix(path, proxyPrefix) { + return false + } + switch config.Mode { + case "whitelist": + for _, rule := range config.RuleList { + if matchDomainAndPath(domain, path, rule) { + return true + } + } + return false + case "blacklist": + for _, rule := range config.RuleList { + if matchDomainAndPath(domain, path, rule) { + return false + } + } + return true + default: + return false + } +} diff --git a/pkg/apis/options/server.go b/pkg/apis/options/server.go deleted file mode 100644 index f423ef2c97..0000000000 --- a/pkg/apis/options/server.go +++ /dev/null @@ -1,40 +0,0 @@ -package options - -// Server represents the configuration for an HTTP(S) server -type Server struct { - // BindAddress is the address on which to serve traffic. - // Leave blank or set to "-" to disable. - BindAddress string - - // SecureBindAddress is the address on which to serve secure traffic. - // Leave blank or set to "-" to disable. - SecureBindAddress string - - // TLS contains the information for loading the certificate and key for the - // secure traffic and further configuration for the TLS server. - TLS *TLS -} - -// TLS contains the information for loading a TLS certificate and key -// as well as an optional minimal TLS version that is acceptable. -type TLS struct { - // Key is the TLS key data to use. - // Typically this will come from a file. - Key *SecretSource - - // Cert is the TLS certificate data to use. - // Typically this will come from a file. - Cert *SecretSource - - // MinVersion is the minimal TLS version that is acceptable. - // E.g. Set to "TLS1.3" to select TLS version 1.3 - MinVersion string - - // CipherSuites is a list of TLS cipher suites that are allowed. - // E.g.: - // - TLS_RSA_WITH_RC4_128_SHA - // - TLS_RSA_WITH_AES_256_GCM_SHA384 - // If not specified, the default Go safe cipher list is used. - // List of valid cipher suites can be found in the [crypto/tls documentation](https://pkg.go.dev/crypto/tls#pkg-constants). - CipherSuites []string -} diff --git a/pkg/apis/options/service.go b/pkg/apis/options/service.go new file mode 100644 index 0000000000..06cc771886 --- /dev/null +++ b/pkg/apis/options/service.go @@ -0,0 +1,27 @@ +package options + +import ( + "errors" + + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" +) + +// Cookie contains configuration options relating to Service configuration +type Service struct { + // 带服务类型的完整 FQDN 名称,例如 keycloak.static, auth.dns + ServiceName string `mapstructure:"service_name"` + ServicePort int64 `mapstructure:"service_port"` + ServiceHost string `mapstructure:"service_host"` +} + +func (s *Service) NewService() (wrapper.HttpClient, error) { + if s.ServiceName == "" || s.ServicePort == 0 { + return nil, errors.New("invalid service config") + } + client := wrapper.NewClusterClient(&wrapper.FQDNCluster{ + FQDN: s.ServiceName, + Host: s.ServiceHost, + Port: s.ServicePort, + }) + return client, nil +} diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index c90c0ac273..c59527b084 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -2,38 +2,17 @@ package options // SessionOptions contains configuration options for the SessionStore providers. type SessionOptions struct { - Type string `flag:"session-store-type" cfg:"session_store_type"` - Cookie CookieStoreOptions `cfg:",squash"` - Redis RedisStoreOptions `cfg:",squash"` + Type string `mapstructure:"session_store_type"` + Cookie CookieStoreOptions `mapstructure:",squash"` } // CookieSessionStoreType is used to indicate the CookieSessionStore should be // used for storing sessions. var CookieSessionStoreType = "cookie" -// RedisSessionStoreType is used to indicate the RedisSessionStore should be -// used for storing sessions. -var RedisSessionStoreType = "redis" - // CookieStoreOptions contains configuration options for the CookieSessionStore. type CookieStoreOptions struct { - Minimal bool `flag:"session-cookie-minimal" cfg:"session_cookie_minimal"` -} - -// RedisStoreOptions contains configuration options for the RedisSessionStore. -type RedisStoreOptions struct { - ConnectionURL string `flag:"redis-connection-url" cfg:"redis_connection_url"` - Username string `flag:"redis-username" cfg:"redis_username"` - Password string `flag:"redis-password" cfg:"redis_password"` - UseSentinel bool `flag:"redis-use-sentinel" cfg:"redis_use_sentinel"` - SentinelPassword string `flag:"redis-sentinel-password" cfg:"redis_sentinel_password"` - SentinelMasterName string `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name"` - SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls"` - UseCluster bool `flag:"redis-use-cluster" cfg:"redis_use_cluster"` - ClusterConnectionURLs []string `flag:"redis-cluster-connection-urls" cfg:"redis_cluster_connection_urls"` - CAPath string `flag:"redis-ca-path" cfg:"redis_ca_path"` - InsecureSkipTLSVerify bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify"` - IdleTimeout int `flag:"redis-connection-idle-timeout" cfg:"redis_connection_idle_timeout"` + Minimal bool `mapstructure:"session_cookie_minimal"` } func sessionOptionsDefaults() SessionOptions { diff --git a/pkg/apis/options/testutil/options_matcher.go b/pkg/apis/options/testutil/options_matcher.go deleted file mode 100644 index 0aebd4372e..0000000000 --- a/pkg/apis/options/testutil/options_matcher.go +++ /dev/null @@ -1,62 +0,0 @@ -package testutil - -import ( - "errors" - "fmt" - "unicode" - "unicode/utf8" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/onsi/gomega/format" - "github.com/onsi/gomega/types" -) - -type optionsMatcher struct { - Expected interface{} - CompareOptions []cmp.Option -} - -func EqualOpts(expected interface{}) types.GomegaMatcher { - ignoreUnexported := cmp.FilterPath(func(p cmp.Path) bool { - sf, ok := p.Index(-1).(cmp.StructField) - if !ok { - return false - } - r, _ := utf8.DecodeRuneInString(sf.Name()) - return !unicode.IsUpper(r) - }, cmp.Ignore()) - - return &optionsMatcher{ - Expected: expected, - CompareOptions: []cmp.Option{ignoreUnexported, cmpopts.EquateEmpty()}, - } -} - -func (matcher *optionsMatcher) Match(actual interface{}) (success bool, err error) { - if actual == nil && matcher.Expected == nil { - return false, errors.New("trying to compare to ") - } - return cmp.Equal(actual, matcher.Expected, matcher.CompareOptions...), nil -} - -func (matcher *optionsMatcher) FailureMessage(actual interface{}) (message string) { - actualString, actualOK := actual.(string) - expectedString, expectedOK := fmt.Sprintf("%v", matcher.Expected), true - if actualOK && expectedOK { - return format.MessageWithDiff(actualString, "to equal", expectedString) - } - - return format.Message(actual, "to equal", matcher.Expected) + - "\n\nDiff:\n" + format.IndentString(matcher.getDiff(actual), 1) -} - -func (matcher *optionsMatcher) NegatedFailureMessage(actual interface{}) (message string) { - - return format.Message(actual, "not to equal", matcher.Expected) + - "\n\nDiff:\n" + format.IndentString(matcher.getDiff(actual), 1) -} - -func (matcher *optionsMatcher) getDiff(actual interface{}) string { - return cmp.Diff(actual, matcher.Expected, matcher.CompareOptions...) -} diff --git a/pkg/apis/options/testutil/options_matcher_test.go b/pkg/apis/options/testutil/options_matcher_test.go deleted file mode 100644 index 1bdc65bd75..0000000000 --- a/pkg/apis/options/testutil/options_matcher_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package testutil - -import ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Options Gomega Matcher", func() { - type TestOptions struct { - Foo string - Bar int - List []string - - // unexported fields should be ignored - unexported string - another string - } - - Context("two empty option structs are equal", func() { - Expect(EqualOpts(TestOptions{}).Match(TestOptions{})).To(BeTrue()) - }) - - Context("two options with the same content should be equal", func() { - opt1 := TestOptions{Foo: "foo", Bar: 1} - opt2 := TestOptions{Foo: "foo", Bar: 1} - Expect(EqualOpts(opt1).Match(opt2)).To(BeTrue()) - }) - - Context("when two options have different content", func() { - opt1 := TestOptions{Foo: "foo", Bar: 1} - opt2 := TestOptions{Foo: "foo", Bar: 2} - Expect(EqualOpts(opt1).Match(opt2)).To(BeFalse()) - }) - - Context("when two options have different types they are not equal", func() { - opt1 := TestOptions{Foo: "foo", Bar: 1} - opt2 := struct { - Foo string - Bar int - }{ - Foo: "foo", - Bar: 1, - } - Expect(EqualOpts(opt1).Match(opt2)).To(BeFalse()) - }) - - Context("when two options have different unexported fields they are equal", func() { - opts1 := TestOptions{Foo: "foo", Bar: 1, unexported: "unexported", another: "another"} - opts2 := TestOptions{Foo: "foo", Bar: 1, unexported: "unexported2"} - Expect(EqualOpts(opts1).Match(opts2)).To(BeTrue()) - }) - - Context("when two options have different list content they are not equal", func() { - opt1 := TestOptions{List: []string{"foo", "bar"}} - opt2 := TestOptions{List: []string{"foo", "baz"}} - Expect(EqualOpts(opt1).Match(opt2)).To(BeFalse()) - }) - - Context("when two options have different list lengths they are not equal", func() { - opt1 := TestOptions{List: []string{"foo", "bar"}} - opt2 := TestOptions{List: []string{"foo", "bar", "baz"}} - Expect(EqualOpts(opt1).Match(opt2)).To(BeFalse()) - }) - - Context("when one options has a list of length 0 and the other is nil they are equal", func() { - otp1 := TestOptions{List: []string{}} - opt2 := TestOptions{} - Expect(EqualOpts(otp1).Match(opt2)).To(BeTrue()) - }) -}) diff --git a/pkg/apis/options/upstreams.go b/pkg/apis/options/upstreams.go deleted file mode 100644 index dab4f62b77..0000000000 --- a/pkg/apis/options/upstreams.go +++ /dev/null @@ -1,94 +0,0 @@ -package options - -import "time" - -const ( - // DefaultUpstreamFlushInterval is the default value for the Upstream FlushInterval. - DefaultUpstreamFlushInterval = 1 * time.Second - - // DefaultUpstreamTimeout is the maximum duration a network dial to a upstream server for a response. - DefaultUpstreamTimeout = 30 * time.Second -) - -// UpstreamConfig is a collection of definitions for upstream servers. -type UpstreamConfig struct { - // ProxyRawPath will pass the raw url path to upstream allowing for urls - // like: "/%2F/" which would otherwise be redirected to "/" - ProxyRawPath bool `json:"proxyRawPath,omitempty"` - - // Upstreams represents the configuration for the upstream servers. - // Requests will be proxied to this upstream if the path matches the request path. - Upstreams []Upstream `json:"upstreams,omitempty"` -} - -// Upstream represents the configuration for an upstream server. -// Requests will be proxied to this upstream if the path matches the request path. -type Upstream struct { - // ID should be a unique identifier for the upstream. - // This value is required for all upstreams. - ID string `json:"id,omitempty"` - - // Path is used to map requests to the upstream server. - // The closest match will take precedence and all Paths must be unique. - // Path can also take a pattern when used with RewriteTarget. - // Path segments can be captured and matched using regular experessions. - // Eg: - // - `^/foo$`: Match only the explicit path `/foo` - // - `^/bar/$`: Match any path prefixed with `/bar/` - // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget - Path string `json:"path,omitempty"` - - // RewriteTarget allows users to rewrite the request path before it is sent to - // the upstream server. - // Use the Path to capture segments for reuse within the rewrite target. - // Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite - // the request `/baz/abc/123` to `/foo/abc/123` before proxying to the - // upstream server. - RewriteTarget string `json:"rewriteTarget,omitempty"` - - // The URI of the upstream server. This may be an HTTP(S) server of a File - // based URL. It may include a path, in which case all requests will be served - // under that path. - // Eg: - // - http://localhost:8080 - // - https://service.localhost - // - https://service.localhost/path - // - file://host/path - // If the URI's path is "/base" and the incoming request was for "/dir", - // the upstream request will be for "/base/dir". - URI string `json:"uri,omitempty"` - - // InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts. - // This option is insecure and will allow potential Man-In-The-Middle attacks - // between OAuth2 Proxy and the upstream server. - // Defaults to false. - InsecureSkipTLSVerify bool `json:"insecureSkipTLSVerify,omitempty"` - - // Static will make all requests to this upstream have a static response. - // The response will have a body of "Authenticated" and a response code - // matching StaticCode. - // If StaticCode is not set, the response will return a 200 response. - Static bool `json:"static,omitempty"` - - // StaticCode determines the response code for the Static response. - // This option can only be used with Static enabled. - StaticCode *int `json:"staticCode,omitempty"` - - // FlushInterval is the period between flushing the response buffer when - // streaming response from the upstream. - // Defaults to 1 second. - FlushInterval *Duration `json:"flushInterval,omitempty"` - - // PassHostHeader determines whether the request host header should be proxied - // to the upstream server. - // Defaults to true. - PassHostHeader *bool `json:"passHostHeader,omitempty"` - - // ProxyWebSockets enables proxying of websockets to upstream servers - // Defaults to true. - ProxyWebSockets *bool `json:"proxyWebSockets,omitempty"` - - // Timeout is the maximum duration the server will wait for a response from the upstream server. - // Defaults to 30 seconds. - Timeout *Duration `json:"timeout,omitempty"` -} diff --git a/pkg/apis/options/util/util.go b/pkg/apis/options/util/util.go deleted file mode 100644 index 03f0a13469..0000000000 --- a/pkg/apis/options/util/util.go +++ /dev/null @@ -1,22 +0,0 @@ -package util - -import ( - "errors" - "os" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" -) - -// GetSecretValue returns the value of the Secret from its source -func GetSecretValue(source *options.SecretSource) ([]byte, error) { - switch { - case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "": - return source.Value, nil - case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "": - return []byte(os.Getenv(source.FromEnv)), nil - case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "": - return os.ReadFile(source.FromFile) - default: - return nil, errors.New("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile") - } -} diff --git a/pkg/apis/options/util/util_suite_test.go b/pkg/apis/options/util/util_suite_test.go deleted file mode 100644 index ecd5bc215c..0000000000 --- a/pkg/apis/options/util/util_suite_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package util - -import ( - "testing" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestUtilSuite(t *testing.T) { - logger.SetOutput(GinkgoWriter) - logger.SetErrOutput(GinkgoWriter) - - RegisterFailHandler(Fail) - RunSpecs(t, "Options Util Suite") -} diff --git a/pkg/apis/options/util/util_test.go b/pkg/apis/options/util/util_test.go deleted file mode 100644 index b96c14532d..0000000000 --- a/pkg/apis/options/util/util_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package util - -import ( - "os" - "path" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("GetSecretValue", func() { - var fileDir string - const secretEnvKey = "SECRET_ENV_KEY" - const secretEnvValue = "secret-env-value" - var secretFileValue = []byte("secret-file-value") - - BeforeEach(func() { - os.Setenv(secretEnvKey, secretEnvValue) - - var err error - fileDir, err = os.MkdirTemp("", "oauth2-proxy-util-get-secret-value") - Expect(err).ToNot(HaveOccurred()) - Expect(os.WriteFile(path.Join(fileDir, "secret-file"), secretFileValue, 0600)).To(Succeed()) - }) - - AfterEach(func() { - os.Unsetenv(secretEnvKey) - os.RemoveAll(fileDir) - }) - - It("returns the correct value from the string value", func() { - value, err := GetSecretValue(&options.SecretSource{ - Value: []byte("secret-value-1"), - }) - Expect(err).ToNot(HaveOccurred()) - Expect(string(value)).To(Equal("secret-value-1")) - }) - - It("returns the correct value from the environment", func() { - value, err := GetSecretValue(&options.SecretSource{ - FromEnv: secretEnvKey, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(value).To(BeEquivalentTo(secretEnvValue)) - }) - - It("returns the correct value from a file", func() { - value, err := GetSecretValue(&options.SecretSource{ - FromFile: path.Join(fileDir, "secret-file"), - }) - Expect(err).ToNot(HaveOccurred()) - Expect(value).To(Equal(secretFileValue)) - }) - - It("when the file does not exist", func() { - value, err := GetSecretValue(&options.SecretSource{ - FromFile: path.Join(fileDir, "not-exist"), - }) - Expect(err).To(HaveOccurred()) - Expect(value).To(BeEmpty()) - }) - - It("with no source set", func() { - value, err := GetSecretValue(&options.SecretSource{}) - Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")) - Expect(value).To(BeEmpty()) - }) - - It("with multiple sources set", func() { - value, err := GetSecretValue(&options.SecretSource{ - FromEnv: secretEnvKey, - FromFile: path.Join(fileDir, "secret-file"), - }) - Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")) - Expect(value).To(BeEmpty()) - }) -}) diff --git a/pkg/apis/sessions/interfaces.go b/pkg/apis/sessions/interfaces.go index 97c364cf70..637b4184bf 100644 --- a/pkg/apis/sessions/interfaces.go +++ b/pkg/apis/sessions/interfaces.go @@ -16,7 +16,6 @@ type SessionStore interface { } var ErrLockNotObtained = errors.New("lock: not obtained") -var ErrNotLocked = errors.New("tried to release not existing lock") // Lock is an interface for controlling session locks type Lock interface { diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index b5e4fc8348..5905401430 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -1,37 +1,35 @@ package sessions import ( - "bytes" "context" "fmt" - "io" "time" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/clock" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption" - "github.com/pierrec/lz4/v4" - "github.com/vmihailenco/msgpack/v5" + "encoding/json" + + "github.com/higress-group/oauth2-proxy/pkg/clock" + "github.com/higress-group/oauth2-proxy/pkg/encryption" ) -// SessionState is used to store information about the currently authenticated user session +// // SessionState is used to store information about the currently authenticated user session type SessionState struct { - CreatedAt *time.Time `msgpack:"ca,omitempty"` - ExpiresOn *time.Time `msgpack:"eo,omitempty"` + CreatedAt *time.Time `json:"ca,omitempty"` + ExpiresOn *time.Time `json:"eo,omitempty"` - AccessToken string `msgpack:"at,omitempty"` - IDToken string `msgpack:"it,omitempty"` - RefreshToken string `msgpack:"rt,omitempty"` + AccessToken string `json:"at,omitempty"` + IDToken string `json:"it,omitempty"` + RefreshToken string `json:"rt,omitempty"` - Nonce []byte `msgpack:"n,omitempty"` + Nonce []byte `json:"n,omitempty"` - Email string `msgpack:"e,omitempty"` - User string `msgpack:"u,omitempty"` - Groups []string `msgpack:"g,omitempty"` - PreferredUsername string `msgpack:"pu,omitempty"` + Email string `json:"e,omitempty"` + User string `json:"u,omitempty"` + Groups []string `json:"g,omitempty"` + PreferredUsername string `json:"pu,omitempty"` // Internal helpers, not serialized - Clock clock.Clock `msgpack:"-"` - Lock Lock `msgpack:"-"` + Clock clock.Clock `json:"-"` + Lock Lock `json:"-"` } func (s *SessionState) ObtainLock(ctx context.Context, expiration time.Duration) error { @@ -160,20 +158,12 @@ func (s *SessionState) CheckNonce(hashed string) bool { // EncodeSessionState returns an encrypted, lz4 compressed, MessagePack encoded session func (s *SessionState) EncodeSessionState(c encryption.Cipher, compress bool) ([]byte, error) { - packed, err := msgpack.Marshal(s) + packed, err := json.Marshal(s) if err != nil { return nil, fmt.Errorf("error marshalling session state to msgpack: %w", err) } - if !compress { - return c.Encrypt(packed) - } - - compressed, err := lz4Compress(packed) - if err != nil { - return nil, err - } - return c.Encrypt(compressed) + return c.Encrypt(packed) } // DecodeSessionState decodes a LZ4 compressed MessagePack into a Session State @@ -184,69 +174,12 @@ func DecodeSessionState(data []byte, c encryption.Cipher, compressed bool) (*Ses } packed := decrypted - if compressed { - packed, err = lz4Decompress(decrypted) - if err != nil { - return nil, err - } - } var ss SessionState - err = msgpack.Unmarshal(packed, &ss) + err = json.Unmarshal(packed, &ss) if err != nil { return nil, fmt.Errorf("error unmarshalling data to session state: %w", err) } return &ss, nil } - -// lz4Compress compresses with LZ4 -// -// The Compress:Decompress ratio is 1:Many. LZ4 gives fastest decompress speeds -// at the expense of greater compression compared to other compression -// algorithms. -func lz4Compress(payload []byte) ([]byte, error) { - buf := new(bytes.Buffer) - zw := lz4.NewWriter(nil) - zw.Apply( - lz4.BlockSizeOption(lz4.BlockSize(65536)), - lz4.CompressionLevelOption(lz4.Fast), - ) - zw.Reset(buf) - - reader := bytes.NewReader(payload) - _, err := io.Copy(zw, reader) - if err != nil { - return nil, fmt.Errorf("error copying lz4 stream to buffer: %w", err) - } - err = zw.Close() - if err != nil { - return nil, fmt.Errorf("error closing lz4 writer: %w", err) - } - - compressed, err := io.ReadAll(buf) - if err != nil { - return nil, fmt.Errorf("error reading lz4 buffer: %w", err) - } - - return compressed, nil -} - -// lz4Decompress decompresses with LZ4 -func lz4Decompress(compressed []byte) ([]byte, error) { - reader := bytes.NewReader(compressed) - buf := new(bytes.Buffer) - zr := lz4.NewReader(nil) - zr.Reset(reader) - _, err := io.Copy(buf, zr) - if err != nil { - return nil, fmt.Errorf("error copying lz4 stream to buffer: %w", err) - } - - payload, err := io.ReadAll(buf) - if err != nil { - return nil, fmt.Errorf("error reading lz4 buffer: %w", err) - } - - return payload, nil -} diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go deleted file mode 100644 index e12c277636..0000000000 --- a/pkg/apis/sessions/session_state_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package sessions - -import ( - "crypto/rand" - "fmt" - "io" - "testing" - "time" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption" - . "github.com/onsi/gomega" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func timePtr(t time.Time) *time.Time { - return &t -} - -func TestCreatedAtNow(t *testing.T) { - g := NewWithT(t) - ss := &SessionState{} - - now := time.Unix(1234567890, 0) - ss.Clock.Set(now) - - ss.CreatedAtNow() - g.Expect(*ss.CreatedAt).To(Equal(now)) -} - -func TestExpiresIn(t *testing.T) { - g := NewWithT(t) - ss := &SessionState{} - - now := time.Unix(1234567890, 0) - ss.Clock.Set(now) - - ttl := time.Duration(743) * time.Second - ss.ExpiresIn(ttl) - - g.Expect(*ss.ExpiresOn).To(Equal(ss.CreatedAt.Add(ttl))) -} - -func TestString(t *testing.T) { - g := NewWithT(t) - created, err := time.Parse(time.RFC3339, "2000-01-01T00:00:00Z") - g.Expect(err).ToNot(HaveOccurred()) - expires, err := time.Parse(time.RFC3339, "2000-01-01T01:00:00Z") - g.Expect(err).ToNot(HaveOccurred()) - - testCases := []struct { - name string - sessionState *SessionState - expected string - }{ - { - name: "Minimal Session", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user}", - }, - { - name: "Full Session", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - CreatedAt: &created, - ExpiresOn: &expires, - AccessToken: "access.token", - IDToken: "id.token", - RefreshToken: "refresh.token", - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user token:true id_token:true created:2000-01-01 00:00:00 +0000 UTC expires:2000-01-01 01:00:00 +0000 UTC refresh_token:true}", - }, - { - name: "With a CreatedAt", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - CreatedAt: &created, - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user created:2000-01-01 00:00:00 +0000 UTC}", - }, - { - name: "With an ExpiresOn", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - ExpiresOn: &expires, - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user expires:2000-01-01 01:00:00 +0000 UTC}", - }, - { - name: "With an AccessToken", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - AccessToken: "access.token", - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user token:true}", - }, - { - name: "With an IDToken", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - IDToken: "id.token", - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user id_token:true}", - }, - { - name: "With a RefreshToken", - sessionState: &SessionState{ - Email: "email@email.email", - User: "some.user", - PreferredUsername: "preferred.user", - RefreshToken: "refresh.token", - }, - expected: "Session{email:email@email.email user:some.user PreferredUsername:preferred.user refresh_token:true}", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - gs := NewWithT(t) - gs.Expect(tc.sessionState.String()).To(Equal(tc.expected)) - }) - } -} - -func TestIsExpired(t *testing.T) { - s := &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(-1) * time.Minute))} - assert.Equal(t, true, s.IsExpired()) - - s = &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Minute))} - assert.Equal(t, false, s.IsExpired()) - - s = &SessionState{} - assert.Equal(t, false, s.IsExpired()) -} - -func TestAge(t *testing.T) { - ss := &SessionState{} - - // Created at unset so should be 0 - assert.Equal(t, time.Duration(0), ss.Age()) - - // Set CreatedAt to 1 hour ago - ss.CreatedAt = timePtr(time.Now().Add(-1 * time.Hour)) - assert.Equal(t, time.Hour, ss.Age().Round(time.Minute)) -} - -// TestEncodeAndDecodeSessionState encodes & decodes various session states -// and confirms the operation is 1:1 -func TestEncodeAndDecodeSessionState(t *testing.T) { - created := time.Now() - expires := time.Now().Add(time.Duration(1) * time.Hour) - - // Tokens in the test table are purposefully redundant - // Otherwise compressing small payloads could result in a compressed value - // that is larger (compression dictionary + limited like strings to compress) - // which breaks the len(compressed) < len(uncompressed) assertion. - testCases := map[string]SessionState{ - "Full session": { - Email: "username@example.com", - User: "username", - PreferredUsername: "preferred.username", - AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - CreatedAt: &created, - ExpiresOn: &expires, - RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - Nonce: []byte("abcdef1234567890abcdef1234567890"), - }, - "No ExpiresOn": { - Email: "username@example.com", - User: "username", - PreferredUsername: "preferred.username", - AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - CreatedAt: &created, - RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - Nonce: []byte("abcdef1234567890abcdef1234567890"), - }, - "No PreferredUsername": { - Email: "username@example.com", - User: "username", - AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - CreatedAt: &created, - ExpiresOn: &expires, - RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - Nonce: []byte("abcdef1234567890abcdef1234567890"), - }, - "Minimal session": { - User: "username", - IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - CreatedAt: &created, - RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - }, - "Bearer authorization header created session": { - Email: "username", - User: "username", - AccessToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - ExpiresOn: &expires, - }, - "With groups": { - Email: "username@example.com", - User: "username", - PreferredUsername: "preferred.username", - AccessToken: "AccessToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - IDToken: "IDToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - CreatedAt: &created, - ExpiresOn: &expires, - RefreshToken: "RefreshToken.12349871293847fdsaihf9238h4f91h8fr.1349f831y98fd7", - Nonce: []byte("abcdef1234567890abcdef1234567890"), - Groups: []string{"group-a", "group-b"}, - }, - } - - for _, secretSize := range []int{16, 24, 32} { - t.Run(fmt.Sprintf("%d byte secret", secretSize), func(t *testing.T) { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.NoError(t, err) - - cfb, err := encryption.NewCFBCipher([]byte(secret)) - assert.NoError(t, err) - gcm, err := encryption.NewGCMCipher([]byte(secret)) - assert.NoError(t, err) - - ciphers := map[string]encryption.Cipher{ - "CFB cipher": cfb, - "GCM cipher": gcm, - } - - for cipherName, c := range ciphers { - t.Run(cipherName, func(t *testing.T) { - for testName, ss := range testCases { - t.Run(testName, func(t *testing.T) { - encoded, err := ss.EncodeSessionState(c, false) - require.NoError(t, err) - encodedCompressed, err := ss.EncodeSessionState(c, true) - require.NoError(t, err) - // Make sure compressed version is smaller than if not compressed - assert.Greater(t, len(encoded), len(encodedCompressed)) - - decoded, err := DecodeSessionState(encoded, c, false) - require.NoError(t, err) - decodedCompressed, err := DecodeSessionState(encodedCompressed, c, true) - require.NoError(t, err) - - compareSessionStates(t, decoded, decodedCompressed) - compareSessionStates(t, decoded, &ss) - }) - } - }) - } - }) - } -} - -func compareSessionStates(t *testing.T, expected *SessionState, actual *SessionState) { - if expected.CreatedAt != nil { - assert.NotNil(t, actual.CreatedAt) - assert.Equal(t, true, expected.CreatedAt.Equal(*actual.CreatedAt)) - } else { - assert.Nil(t, actual.CreatedAt) - } - if expected.ExpiresOn != nil { - assert.NotNil(t, actual.ExpiresOn) - assert.Equal(t, true, expected.ExpiresOn.Equal(*actual.ExpiresOn)) - } else { - assert.Nil(t, actual.ExpiresOn) - } - - // Compare sessions without *time.Time fields - exp := *expected - exp.CreatedAt = nil - exp.ExpiresOn = nil - act := *actual - act.CreatedAt = nil - act.ExpiresOn = nil - assert.Equal(t, exp, act) -} diff --git a/pkg/app/pagewriter/default_logo.svg b/pkg/app/pagewriter/default_logo.svg deleted file mode 100644 index 37851c2ae7..0000000000 --- a/pkg/app/pagewriter/default_logo.svg +++ /dev/null @@ -1 +0,0 @@ -OAuth2_Proxy_logo_v3 diff --git a/pkg/app/pagewriter/error.html b/pkg/app/pagewriter/error.html deleted file mode 100644 index 86df327284..0000000000 --- a/pkg/app/pagewriter/error.html +++ /dev/null @@ -1,107 +0,0 @@ -{{define "error.html"}} - - - - - - {{.StatusCode}} {{.Title}} - - - - - - - - -
-
-
{{.StatusCode}}
-
-

{{.Title}}

-
- - {{ if or .Message .RequestID }} -
-
-

More Info

- - - -
- -
- {{ end }} - - {{ if .Redirect }} -
- -
-
-
- -
-
-
-
- - -
-
-
- {{ end }} - -
-
- -
-
- {{ if eq .Footer "-" }} - {{ else if eq .Footer ""}} -

Secured with OAuth2 Proxy version {{.Version}}

- {{ else }} -

{{.Footer}}

- {{ end }} -
-
- - - -{{end}} diff --git a/pkg/app/pagewriter/error_page.go b/pkg/app/pagewriter/error_page.go deleted file mode 100644 index e62a9a52fe..0000000000 --- a/pkg/app/pagewriter/error_page.go +++ /dev/null @@ -1,119 +0,0 @@ -package pagewriter - -import ( - "fmt" - "html/template" - "net/http" - - middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" -) - -// errorMessages are default error messages for each of the different -// http status codes expected to be rendered in the error page. -var errorMessages = map[int]string{ - http.StatusInternalServerError: "Oops! Something went wrong. For more information contact your server administrator.", - http.StatusNotFound: "We could not find the resource you were looking for.", - http.StatusForbidden: "You do not have permission to access this resource.", - http.StatusUnauthorized: "You need to be logged in to access this resource.", -} - -// errorPageWriter is used to render error pages. -type errorPageWriter struct { - // template is the error page HTML template. - template *template.Template - - // proxyPrefix is the prefix under which OAuth2 Proxy pages are served. - proxyPrefix string - - // footer is the footer to be displayed at the bottom of the page. - // If not set, a default footer will be used. - footer string - - // version is the OAuth2 Proxy version to be used in the default footer. - version string - - // debug determines whether errors pages should be rendered with detailed - // errors. - debug bool -} - -// ErrorPageOpts bundles up all the content needed to write the Error Page -type ErrorPageOpts struct { - // HTTP status code - Status int - // Redirect URL for "Go back" and "Sign in" buttons - RedirectURL string - // The UUID of the request - RequestID string - // App Error shown in debug mode - AppError string - // Generic error messages shown in non-debug mode - Messages []interface{} -} - -// WriteErrorPage writes an error page to the given response writer. -// It uses the passed redirectURL to give users the option to go back to where -// they originally came from or try signing in again. -func (e *errorPageWriter) WriteErrorPage(rw http.ResponseWriter, opts ErrorPageOpts) { - rw.WriteHeader(opts.Status) - - data := struct { - Title string - Message string - ProxyPrefix string - StatusCode int - Redirect string - RequestID string - Footer template.HTML - Version string - }{ - Title: http.StatusText(opts.Status), - Message: e.getMessage(opts.Status, opts.AppError, opts.Messages...), - ProxyPrefix: e.proxyPrefix, - StatusCode: opts.Status, - Redirect: opts.RedirectURL, - RequestID: opts.RequestID, - Footer: template.HTML(e.footer), // #nosec G203 -- We allow unescaped template.HTML since it is user configured options - Version: e.version, - } - - if err := e.template.Execute(rw, data); err != nil { - logger.Printf("Error rendering error template: %v", err) - http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - } -} - -// ProxyErrorHandler is used by the upstream ReverseProxy to render error pages -// when there are issues with upstream servers. -// It is expected to always render a bad gateway error. -func (e *errorPageWriter) ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error) { - logger.Errorf("Error proxying to upstream server: %v", proxyErr) - scope := middlewareapi.GetRequestScope(req) - e.WriteErrorPage(rw, ErrorPageOpts{ - Status: http.StatusBadGateway, - RedirectURL: "", // The user is already logged in and has hit an upstream error. Makes no sense to redirect in this case. - RequestID: scope.RequestID, - AppError: proxyErr.Error(), - Messages: []interface{}{"There was a problem connecting to the upstream server."}, - }) -} - -// getMessage creates the message for the template parameters. -// If the errorPagewriter.Debug is enabled, the application error takes precedence. -// Otherwise, any messages will be used. -// The first message is expected to be a format string. -// If no messages are supplied, a default error message will be used. -func (e *errorPageWriter) getMessage(status int, appError string, messages ...interface{}) string { - if e.debug { - return appError - } - if len(messages) > 0 { - format := fmt.Sprintf("%v", messages[0]) - return fmt.Sprintf(format, messages[1:]...) - } - if msg, ok := errorMessages[status]; ok { - return msg - } - return "Unknown error" -} diff --git a/pkg/app/pagewriter/error_page_test.go b/pkg/app/pagewriter/error_page_test.go deleted file mode 100644 index 7c4f013673..0000000000 --- a/pkg/app/pagewriter/error_page_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package pagewriter - -import ( - "errors" - "html/template" - "io" - "net/http/httptest" - - middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("Error Page Writer", func() { - var errorPage *errorPageWriter - - BeforeEach(func() { - tmpl, err := template.New("").Parse("{{.Title}} {{.Message}} {{.ProxyPrefix}} {{.StatusCode}} {{.Redirect}} {{.RequestID}} {{.Footer}} {{.Version}}") - Expect(err).ToNot(HaveOccurred()) - - errorPage = &errorPageWriter{ - template: tmpl, - proxyPrefix: "/prefix/", - footer: "Custom Footer Text", - version: "v0.0.0-test", - } - }) - - Context("WriteErrorPage", func() { - It("Writes the template to the response writer", func() { - recorder := httptest.NewRecorder() - errorPage.WriteErrorPage(recorder, ErrorPageOpts{ - Status: 403, - RedirectURL: "/redirect", - RequestID: testRequestID, - AppError: "Access Denied", - }) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("Forbidden You do not have permission to access this resource. /prefix/ 403 /redirect 11111111-2222-4333-8444-555555555555 Custom Footer Text v0.0.0-test")) - }) - - It("With a different code, uses the stock message for the correct code", func() { - recorder := httptest.NewRecorder() - errorPage.WriteErrorPage(recorder, ErrorPageOpts{ - Status: 500, - RedirectURL: "/redirect", - RequestID: testRequestID, - AppError: "Access Denied", - }) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("Internal Server Error Oops! Something went wrong. For more information contact your server administrator. /prefix/ 500 /redirect 11111111-2222-4333-8444-555555555555 Custom Footer Text v0.0.0-test")) - }) - - It("With a message override, uses the message", func() { - recorder := httptest.NewRecorder() - errorPage.WriteErrorPage(recorder, ErrorPageOpts{ - Status: 403, - RedirectURL: "/redirect", - RequestID: testRequestID, - AppError: "Access Denied", - Messages: []interface{}{ - "An extra message: %s", - "with more context.", - }, - }) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("Forbidden An extra message: with more context. /prefix/ 403 /redirect 11111111-2222-4333-8444-555555555555 Custom Footer Text v0.0.0-test")) - }) - - It("Sanitizes malicious user input", func() { - recorder := httptest.NewRecorder() - errorPage.WriteErrorPage(recorder, ErrorPageOpts{ - Status: 403, - RedirectURL: "/redirect", - RequestID: "", - AppError: "Access Denied", - }) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("Forbidden You do not have permission to access this resource. /prefix/ 403 /redirect <script>alert(1)</script> Custom Footer Text v0.0.0-test")) - }) - }) - - Context("ProxyErrorHandler", func() { - It("Writes a bad gateway error the response writer", func() { - req := httptest.NewRequest("", "/bad-gateway", nil) - req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{ - RequestID: testRequestID, - }) - recorder := httptest.NewRecorder() - errorPage.ProxyErrorHandler(recorder, req, errors.New("some upstream error")) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("Bad Gateway There was a problem connecting to the upstream server. /prefix/ 502 11111111-2222-4333-8444-555555555555 Custom Footer Text v0.0.0-test")) - }) - }) - - Context("With Debug enabled", func() { - BeforeEach(func() { - tmpl, err := template.New("").Parse("{{.Message}}") - Expect(err).ToNot(HaveOccurred()) - - errorPage.template = tmpl - errorPage.debug = true - }) - - Context("WriteErrorPage", func() { - It("Writes the detailed error in place of the message", func() { - recorder := httptest.NewRecorder() - errorPage.WriteErrorPage(recorder, ErrorPageOpts{ - Status: 403, - RedirectURL: "/redirect", - AppError: "Debug error", - }) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("Debug error")) - }) - }) - - Context("ProxyErrorHandler", func() { - It("Writes a bad gateway error the response writer", func() { - req := httptest.NewRequest("", "/bad-gateway", nil) - req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{ - RequestID: testRequestID, - }) - recorder := httptest.NewRecorder() - errorPage.ProxyErrorHandler(recorder, req, errors.New("some upstream error")) - - body, err := io.ReadAll(recorder.Result().Body) - Expect(err).ToNot(HaveOccurred()) - Expect(string(body)).To(Equal("some upstream error")) - }) - }) - }) -}) diff --git a/pkg/app/pagewriter/pagewriter.go b/pkg/app/pagewriter/pagewriter.go deleted file mode 100644 index 8da82a8780..0000000000 --- a/pkg/app/pagewriter/pagewriter.go +++ /dev/null @@ -1,174 +0,0 @@ -package pagewriter - -import ( - "fmt" - "net/http" -) - -// Writer is an interface for rendering html templates for both sign-in and -// error pages. -// It can also be used to write errors for the http.ReverseProxy used in the -// upstream package. -type Writer interface { - WriteSignInPage(rw http.ResponseWriter, req *http.Request, redirectURL string, statusCode int) - WriteErrorPage(rw http.ResponseWriter, opts ErrorPageOpts) - ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error) - WriteRobotsTxt(rw http.ResponseWriter, req *http.Request) -} - -// pageWriter implements the Writer interface -type pageWriter struct { - *errorPageWriter - *signInPageWriter - *staticPageWriter -} - -// Opts contains all options required to configure the template -// rendering within OAuth2 Proxy. -type Opts struct { - // TemplatesPath is the path from which to load custom templates for the sign-in and error pages. - TemplatesPath string - - // ProxyPrefix is the prefix under which OAuth2 Proxy pages are served. - ProxyPrefix string - - // Footer is the footer to be displayed at the bottom of the page. - // If not set, a default footer will be used. - Footer string - - // Version is the OAuth2 Proxy version to be used in the default footer. - Version string - - // Debug determines whether errors pages should be rendered with detailed - // errors. - Debug bool - - // DisplayLoginForm determines whether or not the basic auth password form is displayed on the sign-in page. - DisplayLoginForm bool - - // ProviderName is the name of the provider that should be displayed on the login button. - ProviderName string - - // SignInMessage is the messge displayed above the login button. - SignInMessage string - - // CustomLogo is the path or URL to a logo to be displayed on the sign in page. - // The logo can be either PNG, JPG/JPEG or SVG. - // If a URL is used, image support depends on the browser. - CustomLogo string -} - -// NewWriter constructs a Writer from the options given to allow -// rendering of sign-in and error pages. -func NewWriter(opts Opts) (Writer, error) { - templates, err := loadTemplates(opts.TemplatesPath) - if err != nil { - return nil, fmt.Errorf("error loading templates: %v", err) - } - - logoData, err := loadCustomLogo(opts.CustomLogo) - if err != nil { - return nil, fmt.Errorf("error loading logo: %v", err) - } - - errorPage := &errorPageWriter{ - template: templates.Lookup("error.html"), - proxyPrefix: opts.ProxyPrefix, - footer: opts.Footer, - version: opts.Version, - debug: opts.Debug, - } - - signInPage := &signInPageWriter{ - template: templates.Lookup("sign_in.html"), - errorPageWriter: errorPage, - proxyPrefix: opts.ProxyPrefix, - providerName: opts.ProviderName, - signInMessage: opts.SignInMessage, - footer: opts.Footer, - version: opts.Version, - displayLoginForm: opts.DisplayLoginForm, - logoData: logoData, - } - - staticPages, err := newStaticPageWriter(opts.TemplatesPath, errorPage) - if err != nil { - return nil, fmt.Errorf("error loading static page writer: %v", err) - } - - return &pageWriter{ - errorPageWriter: errorPage, - signInPageWriter: signInPage, - staticPageWriter: staticPages, - }, nil -} - -// WriterFuncs is an implementation of the PageWriter interface based -// on override functions. -// If any of the funcs are not provided, a default implementation will be used. -// This is primarily for us in testing. -type WriterFuncs struct { - SignInPageFunc func(rw http.ResponseWriter, req *http.Request, redirectURL string, statusCode int) - ErrorPageFunc func(rw http.ResponseWriter, opts ErrorPageOpts) - ProxyErrorFunc func(rw http.ResponseWriter, req *http.Request, proxyErr error) - RobotsTxtfunc func(rw http.ResponseWriter, req *http.Request) -} - -// WriteSignInPage implements the Writer interface. -// If the SignInPageFunc is provided, this will be used, else a default -// implementation will be used. -func (w *WriterFuncs) WriteSignInPage(rw http.ResponseWriter, req *http.Request, redirectURL string, statusCode int) { - if w.SignInPageFunc != nil { - w.SignInPageFunc(rw, req, redirectURL, statusCode) - return - } - - if _, err := rw.Write([]byte("Sign In")); err != nil { - rw.WriteHeader(http.StatusInternalServerError) - } -} - -// WriteErrorPage implements the Writer interface. -// If the ErrorPageFunc is provided, this will be used, else a default -// implementation will be used. -func (w *WriterFuncs) WriteErrorPage(rw http.ResponseWriter, opts ErrorPageOpts) { - if w.ErrorPageFunc != nil { - w.ErrorPageFunc(rw, opts) - return - } - - rw.WriteHeader(opts.Status) - errMsg := fmt.Sprintf("%d - %v", opts.Status, opts.AppError) - if _, err := rw.Write([]byte(errMsg)); err != nil { - rw.WriteHeader(http.StatusInternalServerError) - } -} - -// ProxyErrorHandler implements the Writer interface. -// If the ProxyErrorFunc is provided, this will be used, else a default -// implementation will be used. -func (w *WriterFuncs) ProxyErrorHandler(rw http.ResponseWriter, req *http.Request, proxyErr error) { - if w.ProxyErrorFunc != nil { - w.ProxyErrorFunc(rw, req, proxyErr) - return - } - - w.WriteErrorPage(rw, ErrorPageOpts{ - Status: http.StatusBadGateway, - AppError: proxyErr.Error(), - }) -} - -// WriteRobotsTxt implements the Writer interface. -// If the RobotsTxtfunc is provided, this will be used, else a default -// implementation will be used. -func (w *WriterFuncs) WriteRobotsTxt(rw http.ResponseWriter, req *http.Request) { - if w.RobotsTxtfunc != nil { - w.RobotsTxtfunc(rw, req) - return - } - - if _, err := rw.Write([]byte("Allow: *")); err != nil { - rw.WriteHeader(http.StatusInternalServerError) - } -} diff --git a/pkg/app/pagewriter/pagewriter_suite_test.go b/pkg/app/pagewriter/pagewriter_suite_test.go deleted file mode 100644 index e27eee6878..0000000000 --- a/pkg/app/pagewriter/pagewriter_suite_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package pagewriter - -import ( - "testing" - - "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -const testRequestID = "11111111-2222-4333-8444-555555555555" - -func TestOptionsSuite(t *testing.T) { - logger.SetOutput(GinkgoWriter) - logger.SetErrOutput(GinkgoWriter) - - RegisterFailHandler(Fail) - RunSpecs(t, "App Suite") -} diff --git a/pkg/app/pagewriter/pagewriter_test.go b/pkg/app/pagewriter/pagewriter_test.go deleted file mode 100644 index eca0c760fc..0000000000 --- a/pkg/app/pagewriter/pagewriter_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package pagewriter - -import ( - "errors" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("Writer", func() { - Context("NewWriter", func() { - var writer Writer - var opts Opts - var request *http.Request - - BeforeEach(func() { - opts = Opts{ - TemplatesPath: "", - ProxyPrefix: "/prefix", - Footer: "