├── docs ├── static │ ├── .nojekyll │ └── img │ │ ├── architecture.png │ │ ├── sign-in-page.png │ │ └── logos │ │ ├── OAuth2_Proxy_icon.png │ │ ├── OAuth2_Proxy_vertical.png │ │ ├── OAuth2_Proxy_horizontal.png │ │ └── OAuth2_Proxy_icon.svg ├── versions.json ├── babel.config.js ├── .gitignore ├── src │ ├── pages │ │ ├── styles.module.css │ │ └── index.md │ └── css │ │ └── custom.css ├── sidebars.js ├── README.md ├── package.json ├── docs │ ├── behaviour.md │ ├── installation.md │ └── community │ │ └── security.md ├── versioned_docs │ ├── version-6.1.x │ │ ├── behaviour.md │ │ ├── features │ │ │ ├── request_signatures.md │ │ │ └── endpoints.md │ │ ├── installation.md │ │ └── community │ │ │ └── security.md │ ├── version-7.0.x │ │ ├── behaviour.md │ │ ├── features │ │ │ ├── request_signatures.md │ │ │ └── endpoints.md │ │ ├── installation.md │ │ └── community │ │ │ └── security.md │ ├── version-7.1.x │ │ ├── behaviour.md │ │ ├── installation.md │ │ ├── community │ │ │ └── security.md │ │ └── features │ │ │ └── endpoints.md │ ├── version-7.2.x │ │ ├── behaviour.md │ │ ├── installation.md │ │ ├── community │ │ │ └── security.md │ │ └── features │ │ │ └── endpoints.md │ └── version-7.3.x │ │ ├── behaviour.md │ │ ├── installation.md │ │ └── community │ │ └── security.md ├── versioned_sidebars │ ├── version-7.3.x-sidebars.json │ ├── version-6.1.x-sidebars.json │ ├── version-7.1.x-sidebars.json │ ├── version-7.2.x-sidebars.json │ └── version-7.0.x-sidebars.json └── docusaurus.config.js ├── nsswitch.conf ├── contrib ├── local-environment │ ├── kubernetes │ │ ├── .gitignore │ │ ├── kind-cluster.yaml │ │ ├── Chart.lock │ │ ├── README.md │ │ ├── Chart.yaml │ │ ├── custom-dns.yaml │ │ └── Makefile │ ├── traefik │ │ ├── traefik.yaml │ │ └── dynamic.yaml │ ├── README.md │ ├── oauth2-proxy-alpha-config.cfg │ ├── oauth2-proxy.cfg │ ├── oauth2-proxy-alpha-config.yaml │ ├── oauth2-proxy-nginx.cfg │ ├── oauth2-proxy-keycloak.cfg │ ├── docker-compose-alpha-config.yaml │ ├── keycloak │ │ └── master-users-0.json │ ├── oauth2-proxy-traefik.cfg │ ├── dex.yaml │ ├── Makefile │ ├── docker-compose-traefik.yaml │ ├── docker-compose-nginx.yaml │ ├── docker-compose.yaml │ └── docker-compose-keycloak.yaml ├── oauth2-proxy.service.example └── oauth2-proxy_autocomplete.sh ├── pkg ├── app │ ├── pagewriter │ │ ├── robots.txt │ │ ├── pagewriter_suite_test.go │ │ └── templates.go │ └── redirect │ │ ├── pagewriter_suite_test.go │ │ ├── validator.go │ │ └── getters.go ├── middleware │ ├── testdata │ │ └── metrics │ │ │ ├── notfoundrequest.txt │ │ │ └── successfulrequest.txt │ ├── middleware_suite_test.go │ ├── session_utils.go │ ├── scope.go │ ├── healthcheck.go │ ├── redirect_to_https.go │ └── metrics_test.go ├── authentication │ └── basic │ │ ├── validator.go │ │ ├── test │ │ ├── htpasswd-sha1.txt │ │ ├── htpasswd-mixed.txt │ │ └── htpasswd-bcrypt.txt │ │ └── basic_suite_test.go ├── validation │ ├── utils.go │ ├── validation_suite_test.go │ ├── common.go │ ├── header.go │ ├── cookie.go │ ├── logging.go │ ├── allowlist.go │ └── providers_test.go ├── apis │ ├── ip │ │ └── interfaces.go │ ├── options │ │ ├── doc.go │ │ ├── options_suite_test.go │ │ ├── util │ │ │ ├── util_suite_test.go │ │ │ ├── util.go │ │ │ └── util_test.go │ │ ├── server.go │ │ ├── header.go │ │ ├── common.go │ │ ├── sessions.go │ │ └── common_test.go │ ├── sessions │ │ ├── lock.go │ │ └── interfaces.go │ └── middleware │ │ ├── middleware_suite_test.go │ │ ├── scope_test.go │ │ ├── scope.go │ │ └── session.go ├── providers │ ├── oidc │ │ └── oidc_suite_test.go │ └── util │ │ └── util_suite_test.go ├── clock │ └── clock_suite_test.go ├── sessions │ ├── persistence │ │ ├── persistence_suite_test.go │ │ ├── interfaces.go │ │ └── manager_test.go │ ├── session_store.go │ ├── tests │ │ ├── mock_lock.go │ │ └── mock_store.go │ └── redis │ │ ├── client.go │ │ └── lock.go ├── requests │ ├── util │ │ ├── util_suite_test.go │ │ └── util.go │ ├── requests_suite_test.go │ └── result.go ├── ip │ └── parse_ip_net.go ├── http │ └── server_group.go ├── cookies │ ├── cookies_suite_test.go │ └── cookies.go ├── header │ └── header_suite_test.go ├── encryption │ └── nonce.go ├── upstream │ ├── static.go │ ├── file.go │ ├── file_test.go │ ├── rewrite_test.go │ └── static_test.go └── watcher │ └── watcher.go ├── .dockerignore ├── version.go ├── tools └── tools.go ├── SECURITY.md ├── MAINTAINERS ├── main_suite_test.go ├── providers ├── providers_suite_test.go ├── facebook_test.go ├── auth_test.go ├── nextcloud_test.go ├── util.go ├── internal_util.go └── nextcloud.go ├── .github ├── workflows │ ├── test.sh │ ├── stale.yml │ ├── ci.yaml │ ├── codeql.yml │ └── docs.yaml ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── .golangci.yml ├── LICENSE ├── dist.sh ├── RELEASE.md └── Dockerfile /docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nsswitch.conf: -------------------------------------------------------------------------------- 1 | hosts: files dns 2 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/.gitignore: -------------------------------------------------------------------------------- 1 | charts/ 2 | -------------------------------------------------------------------------------- /pkg/app/pagewriter/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile.dev 2 | Dockerfile 3 | docs 4 | vendor 5 | .git 6 | oauth2-proxy 7 | -------------------------------------------------------------------------------- /docs/versions.json: -------------------------------------------------------------------------------- 1 | [ 2 | "7.3.x", 3 | "7.2.x", 4 | "7.1.x", 5 | "7.0.x", 6 | "6.1.x" 7 | ] 8 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // VERSION contains version information 4 | var VERSION = "undefined" 5 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/static/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/oauth2-proxy/master/docs/static/img/architecture.png -------------------------------------------------------------------------------- /docs/static/img/sign-in-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/oauth2-proxy/master/docs/static/img/sign-in-page.png -------------------------------------------------------------------------------- /docs/static/img/logos/OAuth2_Proxy_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/oauth2-proxy/master/docs/static/img/logos/OAuth2_Proxy_icon.png -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // +build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/oauth2-proxy/tools/reference-gen/cmd/reference-gen" 7 | ) 8 | -------------------------------------------------------------------------------- /docs/static/img/logos/OAuth2_Proxy_vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/oauth2-proxy/master/docs/static/img/logos/OAuth2_Proxy_vertical.png -------------------------------------------------------------------------------- /docs/static/img/logos/OAuth2_Proxy_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/oauth2-proxy/master/docs/static/img/logos/OAuth2_Proxy_horizontal.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Disclosures 2 | 3 | Please see [our community docs](https://oauth2-proxy.github.io/oauth2-proxy/docs/community/security) for our security policy. 4 | -------------------------------------------------------------------------------- /contrib/local-environment/traefik/traefik.yaml: -------------------------------------------------------------------------------- 1 | api: 2 | insecure: true 3 | log: 4 | level: INFO 5 | providers: 6 | file: 7 | filename: /etc/traefik/dynamic.yaml 8 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Joel Speed (@JoelSpeed) 2 | Henry Jenkins (@steakunderscore) 3 | Nick Meves (@NickMeves) 4 | -------------------------------------------------------------------------------- /pkg/middleware/testdata/metrics/notfoundrequest.txt: -------------------------------------------------------------------------------- 1 | # HELP oauth2_proxy_requests_total Total number of requests by HTTP status code. 2 | # TYPE oauth2_proxy_requests_total counter 3 | oauth2_proxy_requests_total{code="404"} 1 4 | -------------------------------------------------------------------------------- /pkg/middleware/testdata/metrics/successfulrequest.txt: -------------------------------------------------------------------------------- 1 | # HELP oauth2_proxy_requests_total Total number of requests by HTTP status code. 2 | # TYPE oauth2_proxy_requests_total counter 3 | oauth2_proxy_requests_total{code="200"} 1 4 | -------------------------------------------------------------------------------- /contrib/local-environment/README.md: -------------------------------------------------------------------------------- 1 | # oauth2-proxy: local-environment 2 | 3 | Run `make up` to deploy local dex, etcd and oauth2-proxy instances in Docker containers. Review the [`Makefile`](Makefile) for additional deployment options. 4 | -------------------------------------------------------------------------------- /pkg/authentication/basic/validator.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | // Validator is a minimal interface for something that can validate a 4 | // username and password combination. 5 | type Validator interface { 6 | Validate(user, password string) bool 7 | } 8 | -------------------------------------------------------------------------------- /contrib/local-environment/oauth2-proxy-alpha-config.cfg: -------------------------------------------------------------------------------- 1 | http_address="0.0.0.0:4180" 2 | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" 3 | email_domains="example.com" 4 | cookie_secure="false" 5 | redirect_url="http://localhost:4180/oauth2/callback" 6 | -------------------------------------------------------------------------------- /pkg/authentication/basic/test/htpasswd-sha1.txt: -------------------------------------------------------------------------------- 1 | # admin:Adm1n1str$t0r 2 | admin:{SHA}gXQeRH0bcaCfhAk2gOLm1uaePMA= 3 | 4 | # user1:UsErOn3P455 5 | user1:{SHA}Dvs/L78raajL4jEAHPkwflQXJzI= 6 | 7 | # user2: us3r2P455W0Rd! 8 | user2:{SHA}MoN9/JCJEcYUb6GCQ+2buDvn9pI= 9 | -------------------------------------------------------------------------------- /pkg/validation/utils.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | func prefixValues(prefix string, values ...string) []string { 4 | msgs := []string{} 5 | for _, value := range values { 6 | if value != "" { 7 | msgs = append(msgs, prefix+value) 8 | } 9 | } 10 | return msgs 11 | } 12 | -------------------------------------------------------------------------------- /pkg/authentication/basic/test/htpasswd-mixed.txt: -------------------------------------------------------------------------------- 1 | # admin:Adm1n1str$t0r 2 | admin:$2y$05$SXWrNM7ldtbRzBvUC3VXyOvUeiUcP45XPwM93P5eeGOEPIiAZmJjC 3 | 4 | # user1:UsErOn3P455 5 | user1:{SHA}Dvs/L78raajL4jEAHPkwflQXJzI= 6 | 7 | # user2: us3r2P455W0Rd! 8 | user2:{SHA}MoN9/JCJEcYUb6GCQ+2buDvn9pI= 9 | -------------------------------------------------------------------------------- /pkg/apis/ip/interfaces.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | ) 7 | 8 | // RealClientIPParser is an interface for a getting the client's real IP to be used for logging. 9 | type RealClientIPParser interface { 10 | GetRealClientIP(http.Header) (net.IP, error) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/apis/options/doc.go: -------------------------------------------------------------------------------- 1 | //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 2 | package options 3 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /pkg/authentication/basic/test/htpasswd-bcrypt.txt: -------------------------------------------------------------------------------- 1 | # admin:Adm1n1str$t0r 2 | admin:$2y$05$SXWrNM7ldtbRzBvUC3VXyOvUeiUcP45XPwM93P5eeGOEPIiAZmJjC 3 | 4 | # user1:UsErOn3P455 5 | user1:$2y$05$/sZYJOk8.3Etg4V6fV7puuXfCJLmV5Q7u3xvKpjBSJUka.t2YtmmG 6 | 7 | # user2: us3r2P455W0Rd! 8 | user2:$2y$05$l22MubgKTZFTjTs8TNg5k.YKvcnM2.bA/.iwl0idef5CbekdvBxva 9 | -------------------------------------------------------------------------------- /pkg/providers/oidc/oidc_suite_test.go: -------------------------------------------------------------------------------- 1 | package oidc 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestOIDCSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "OIDC") 16 | } 17 | -------------------------------------------------------------------------------- /main_suite_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestMainSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Main Suite") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/clock/clock_suite_test.go: -------------------------------------------------------------------------------- 1 | package clock_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestClockSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Clock") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/authentication/basic/basic_suite_test.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestBasicSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Basic") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/apis/options/options_suite_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestOptionsSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Options Suite") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/apis/options/util/util_suite_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestUtilSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Options Util Suite") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/providers/util/util_suite_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestProviderUtilSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Provider Utils") 17 | } 18 | -------------------------------------------------------------------------------- /providers/providers_suite_test.go: -------------------------------------------------------------------------------- 1 | package providers_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestProviderSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Providers") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/validation/validation_suite_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestValidationSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Validation Suite") 17 | } 18 | -------------------------------------------------------------------------------- /pkg/sessions/persistence/persistence_suite_test.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestPersistenceSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Persistence") 17 | } 18 | -------------------------------------------------------------------------------- /contrib/local-environment/oauth2-proxy.cfg: -------------------------------------------------------------------------------- 1 | http_address="0.0.0.0:4180" 2 | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" 3 | provider="oidc" 4 | email_domains="example.com" 5 | oidc_issuer_url="http://dex.localhost:4190/dex" 6 | client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" 7 | client_id="oauth2-proxy" 8 | cookie_secure="false" 9 | 10 | redirect_url="http://localhost:4180/oauth2/callback" 11 | upstreams="http://httpbin" 12 | -------------------------------------------------------------------------------- /pkg/app/pagewriter/pagewriter_suite_test.go: -------------------------------------------------------------------------------- 1 | package pagewriter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | const testRequestID = "11111111-2222-4333-8444-555555555555" 12 | 13 | func TestOptionsSuite(t *testing.T) { 14 | logger.SetOutput(GinkgoWriter) 15 | logger.SetErrOutput(GinkgoWriter) 16 | 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "App Suite") 19 | } 20 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/kind-cluster.yaml: -------------------------------------------------------------------------------- 1 | kind: Cluster 2 | apiVersion: kind.x-k8s.io/v1alpha4 3 | nodes: 4 | - role: control-plane 5 | kubeadmConfigPatches: 6 | - | 7 | kind: InitConfiguration 8 | nodeRegistration: 9 | kubeletExtraArgs: 10 | node-labels: "ingress-ready=true" 11 | extraPortMappings: 12 | - containerPort: 80 13 | hostPort: 80 14 | protocol: TCP 15 | - containerPort: 443 16 | hostPort: 443 17 | protocol: TCP 18 | -------------------------------------------------------------------------------- /pkg/apis/sessions/lock.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type NoOpLock struct{} 9 | 10 | func (l *NoOpLock) Obtain(ctx context.Context, expiration time.Duration) error { 11 | return nil 12 | } 13 | 14 | func (l *NoOpLock) Peek(ctx context.Context) (bool, error) { 15 | return false, nil 16 | } 17 | 18 | func (l *NoOpLock) Refresh(ctx context.Context, expiration time.Duration) error { 19 | return nil 20 | } 21 | 22 | func (l *NoOpLock) Release(ctx context.Context) error { 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/Chart.lock: -------------------------------------------------------------------------------- 1 | dependencies: 2 | - name: dex 3 | repository: https://charts.helm.sh/stable 4 | version: 2.11.0 5 | - name: oauth2-proxy 6 | repository: https://charts.helm.sh/stable 7 | version: 3.1.0 8 | - name: httpbin 9 | repository: https://conservis.github.io/helm-charts 10 | version: 1.0.1 11 | - name: hello-world 12 | repository: https://conservis.github.io/helm-charts 13 | version: 1.0.1 14 | digest: sha256:e325948ece1706bd9d9e439568985db41e9a0d57623d0f9638249cb0d23821b8 15 | generated: "2020-11-23T11:45:07.908898-08:00" 16 | -------------------------------------------------------------------------------- /pkg/requests/util/util_suite_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | // TestRequestUtilSuite and related tests are in a *_test package 12 | // to prevent circular imports with the `logger` package which uses 13 | // this functionality 14 | func TestRequestUtilSuite(t *testing.T) { 15 | logger.SetOutput(GinkgoWriter) 16 | logger.SetErrOutput(GinkgoWriter) 17 | 18 | RegisterFailHandler(Fail) 19 | RunSpecs(t, "Request Utils") 20 | } 21 | -------------------------------------------------------------------------------- /contrib/oauth2-proxy.service.example: -------------------------------------------------------------------------------- 1 | # Systemd service file for oauth2-proxy daemon 2 | # 3 | # Date: Feb 9, 2016 4 | # Author: Srdjan Grubor 5 | 6 | [Unit] 7 | Description=oauth2-proxy daemon service 8 | After=syslog.target network.target 9 | 10 | [Service] 11 | # www-data group and user need to be created before using these lines 12 | User=www-data 13 | Group=www-data 14 | 15 | ExecStart=/usr/local/bin/oauth2-proxy --config=/etc/oauth2-proxy.cfg 16 | ExecReload=/bin/kill -HUP $MAINPID 17 | 18 | KillMode=process 19 | Restart=always 20 | 21 | [Install] 22 | WantedBy=multi-user.target 23 | -------------------------------------------------------------------------------- /pkg/apis/middleware/middleware_suite_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | // TestMiddlewareSuite and related tests are in a *_test package 12 | // to prevent circular imports with the `logger` package which uses 13 | // this functionality 14 | func TestMiddlewareSuite(t *testing.T) { 15 | logger.SetOutput(GinkgoWriter) 16 | logger.SetErrOutput(GinkgoWriter) 17 | 18 | RegisterFailHandler(Fail) 19 | RunSpecs(t, "Middleware API") 20 | } 21 | -------------------------------------------------------------------------------- /pkg/sessions/persistence/interfaces.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 8 | ) 9 | 10 | // Store is used for persistent session stores (IE not Cookie) 11 | // Implementing this interface allows it to easily use the persistence.Manager 12 | // for session ticket + encryption details. 13 | type Store interface { 14 | Save(context.Context, string, []byte, time.Duration) error 15 | Load(context.Context, string) ([]byte, error) 16 | Clear(context.Context, string) error 17 | Lock(key string) sessions.Lock 18 | } 19 | -------------------------------------------------------------------------------- /contrib/local-environment/oauth2-proxy-alpha-config.yaml: -------------------------------------------------------------------------------- 1 | upstreams: 2 | - id: httpbin 3 | path: / 4 | uri: http://httpbin 5 | injectRequestHeaders: 6 | - name: X-Forwarded-Groups 7 | values: 8 | - claim: groups 9 | - name: X-Forwarded-User 10 | values: 11 | - claim: user 12 | - name: X-Forwarded-Email 13 | values: 14 | - claim: email 15 | - name: X-Forwarded-Preferred-Username 16 | values: 17 | - claim: preferred_username 18 | providers: 19 | - provider: oidc 20 | clientSecret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK 21 | clientID: oauth2-proxy 22 | oidcConfig: 23 | oidcIssuerURL: http://dex.localhost:4190/dex 24 | -------------------------------------------------------------------------------- /contrib/local-environment/oauth2-proxy-nginx.cfg: -------------------------------------------------------------------------------- 1 | http_address="0.0.0.0:4180" 2 | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" 3 | provider="oidc" 4 | email_domains="example.com" 5 | oidc_issuer_url="http://dex.localhost:4190/dex" 6 | client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" 7 | client_id="oauth2-proxy" 8 | cookie_secure="false" 9 | 10 | redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback" 11 | cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains. 12 | whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target. 13 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes example 2 | Based on [kind](https://kind.sigs.k8s.io) as a local Kubernetes cluster. 3 | 4 | ## Quick start 5 | 6 | Before you start: 7 | 8 | _Required_ 9 | * install [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) 10 | * install [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) 11 | 12 | _Optional_ 13 | * install [helm 3](https://helm.sh/docs/intro/quickstart/#install-helm). 14 | 15 | Then: 16 | 17 | * `make create-cluster` 18 | * `make deploy` OR `make helm-deploy` for helm 19 | 20 | Visit http://httpbin.localtest.me or http://hello-world.localtest.me/ 21 | 22 | ## Uninstall 23 | 24 | * `make delete-cluster` 25 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | description: K8S example based on https://kind.sigs.k8s.io 3 | name: kubernetes 4 | version: 5.1.1 5 | appVersion: 5.1.1 6 | dependencies: 7 | - name: dex 8 | version: 2.11.0 9 | repository: https://charts.helm.sh/stable 10 | - name: oauth2-proxy 11 | version: 3.1.0 12 | repository: https://charts.helm.sh/stable 13 | # https://github.com/postmanlabs/httpbin/issues/549 is still in progress, for now using a non-official chart 14 | - name: httpbin 15 | version: 1.0.1 16 | repository: https://conservis.github.io/helm-charts 17 | - name: hello-world 18 | version: 1.0.1 19 | repository: https://conservis.github.io/helm-charts 20 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | -------------------------------------------------------------------------------- /pkg/ip/parse_ip_net.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func ParseIPNet(s string) *net.IPNet { 9 | if !strings.ContainsRune(s, '/') { 10 | ip := net.ParseIP(s) 11 | if ip == nil { 12 | return nil 13 | } 14 | 15 | var mask net.IPMask 16 | switch { 17 | case ip.To4() != nil: 18 | mask = net.CIDRMask(32, 32) 19 | case ip.To16() != nil: 20 | mask = net.CIDRMask(128, 128) 21 | default: 22 | return nil 23 | } 24 | 25 | return &net.IPNet{ 26 | IP: ip, 27 | Mask: mask, 28 | } 29 | } 30 | 31 | switch ip, ipNet, err := net.ParseCIDR(s); { 32 | case err != nil: 33 | return nil 34 | case !ipNet.IP.Equal(ip): 35 | return nil 36 | default: 37 | return ipNet 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # manually exiting from script, because after-build needs to run always 3 | set +e 4 | 5 | if [ -z $CC_TEST_REPORTER_ID ]; then 6 | echo "1. CC_TEST_REPORTER_ID is unset, skipping" 7 | else 8 | echo "1. Running before-build" 9 | ./cc-test-reporter before-build 10 | fi 11 | 12 | echo "2. Running test" 13 | make test 14 | TEST_STATUS=$? 15 | 16 | if [ -z $CC_TEST_REPORTER_ID ]; then 17 | echo "3. CC_TEST_REPORTER_ID is unset, skipping" 18 | else 19 | echo "3. Running after-build" 20 | ./cc-test-reporter after-build --exit-code $TEST_STATUS -t gocov --prefix $(go list -m) 21 | fi 22 | 23 | if [ "$TEST_STATUS" -ne 0 ]; then 24 | echo "Test failed, status code: $TEST_STATUS" 25 | exit $TEST_STATUS 26 | fi 27 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.' 17 | stale-pr-message: 'This pull request has been inactive for 60 days. If the pull request is still relevant please comment to re-activate the pull request. If no action is taken within 7 days, the pull request will be marked closed.' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | oauth2-proxy 2 | vendor 3 | dist 4 | release 5 | .godeps 6 | *.exe 7 | .env 8 | .bundle 9 | c.out 10 | 11 | # Go.gitignore 12 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 13 | *.o 14 | *.a 15 | *.so 16 | 17 | # Folders 18 | _obj 19 | _test 20 | .idea/ 21 | .vscode/ 22 | 23 | # Architecture specific extensions/prefixes 24 | *.[568vq] 25 | [568vq].out 26 | 27 | *.cgo1.go 28 | *.cgo2.c 29 | _cgo_defun.c 30 | _cgo_gotypes.go 31 | _cgo_export.* 32 | 33 | _testmain.go 34 | 35 | # Editor swap/temp files 36 | .*.swp 37 | 38 | # Dockerfile.dev is ignored by both git and docker 39 | # for faster development cycle of docker build 40 | # cp Dockerfile Dockerfile.dev 41 | # vi Dockerfile.dev 42 | # docker build -f Dockerfile.dev . 43 | Dockerfile.dev 44 | -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | docs: [ 3 | { 4 | type: 'doc', 5 | id: 'installation', 6 | }, 7 | { 8 | type: 'doc', 9 | id: 'behaviour', 10 | }, 11 | { 12 | type: 'category', 13 | label: 'Configuration', 14 | collapsed: false, 15 | items: ['configuration/overview', 'configuration/oauth_provider', 'configuration/session_storage', 'configuration/tls', 'configuration/alpha-config'], 16 | }, 17 | { 18 | type: 'category', 19 | label: 'Features', 20 | collapsed: false, 21 | items: ['features/endpoints'], 22 | }, 23 | { 24 | type: 'category', 25 | label: 'Community', 26 | collapsed: false, 27 | items: ['community/security'], 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /pkg/sessions/persistence/manager_test.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 7 | sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/tests" 9 | . "github.com/onsi/ginkgo" 10 | ) 11 | 12 | var _ = Describe("Persistence Manager Tests", func() { 13 | var ms *tests.MockStore 14 | BeforeEach(func() { 15 | ms = tests.NewMockStore() 16 | }) 17 | tests.RunSessionStoreTests( 18 | func(_ *options.SessionOptions, cookieOpts *options.Cookie) (sessionsapi.SessionStore, error) { 19 | return NewManager(ms, cookieOpts), nil 20 | }, 21 | func(d time.Duration) error { 22 | ms.FastForward(d) 23 | return nil 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/custom-dns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | Corefile: | 4 | .:53 { 5 | errors 6 | health { 7 | lameduck 5s 8 | } 9 | ready 10 | kubernetes cluster.local in-addr.arpa ip6.arpa { 11 | pods insecure 12 | fallthrough in-addr.arpa ip6.arpa 13 | ttl 30 14 | } 15 | prometheus :9153 16 | forward . /etc/resolv.conf 17 | cache 30 18 | loop 19 | reload 20 | loadbalance 21 | hosts { 22 | 10.244.0.1 dex.localtest.me 23 | 10.244.0.1 oauth2-proxy.localtest.me 24 | fallthrough 25 | } 26 | } 27 | kind: ConfigMap 28 | metadata: 29 | name: coredns 30 | namespace: kube-system 31 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To develop on this project, please fork the repo and clone into your `$GOPATH`. 4 | 5 | Dependencies are **not** checked in so please download those separately. 6 | Download the dependencies using `go mod download`. 7 | 8 | ```bash 9 | cd $GOPATH/src/github.com # Create this directory if it doesn't exist 10 | git clone git@github.com:/oauth2-proxy oauth2-proxy/oauth2-proxy 11 | cd oauth2-proxy/oauth2-proxy 12 | go mod download 13 | ``` 14 | 15 | ## Pull Requests and Issues 16 | 17 | We track bugs and issues using Github. 18 | 19 | If you find a bug, please open an Issue. 20 | 21 | If you want to fix a bug, please fork, create a feature branch, fix the bug and 22 | open a PR back to this repo. 23 | Please mention the open bug issue number within your PR if applicable. 24 | -------------------------------------------------------------------------------- /pkg/http/server_group.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | 6 | "golang.org/x/sync/errgroup" 7 | ) 8 | 9 | // NewServerGroup creates a new Server to start and gracefully stop a collection 10 | // of Servers. 11 | func NewServerGroup(servers ...Server) Server { 12 | return &serverGroup{ 13 | servers: servers, 14 | } 15 | } 16 | 17 | // serverGroup manages the starting and graceful shutdown of a collection of 18 | // servers. 19 | type serverGroup struct { 20 | servers []Server 21 | } 22 | 23 | // Start runs the servers in the server group. 24 | func (s *serverGroup) Start(ctx context.Context) error { 25 | g, groupCtx := errgroup.WithContext(ctx) 26 | 27 | for _, server := range s.servers { 28 | srv := server 29 | g.Go(func() error { 30 | return srv.Start(groupCtx) 31 | }) 32 | } 33 | 34 | return g.Wait() 35 | } 36 | -------------------------------------------------------------------------------- /pkg/apis/options/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 9 | ) 10 | 11 | // GetSecretValue returns the value of the Secret from its source 12 | func GetSecretValue(source *options.SecretSource) ([]byte, error) { 13 | switch { 14 | case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "": 15 | return source.Value, nil 16 | case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "": 17 | return []byte(os.Getenv(source.FromEnv)), nil 18 | case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "": 19 | return ioutil.ReadFile(source.FromFile) 20 | default: 21 | return nil, errors.New("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pkg/sessions/session_store.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/cookie" 9 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions/redis" 10 | ) 11 | 12 | // NewSessionStore creates a SessionStore from the provided configuration 13 | func NewSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) { 14 | switch opts.Type { 15 | case options.CookieSessionStoreType: 16 | return cookie.NewCookieSessionStore(opts, cookieOpts) 17 | case options.RedisSessionStoreType: 18 | return redis.NewRedisSessionStore(opts, cookieOpts) 19 | default: 20 | return nil, fmt.Errorf("unknown session store type '%s'", opts.Type) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/src/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Welcome to OAuth2 Proxy 3 | hide_table_of_contents: true 4 | --- 5 | 6 | ![OAuth2 Proxy](../../static/img/logos/OAuth2_Proxy_horizontal.svg) 7 | 8 | A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) 9 | to validate accounts by email, domain or group. 10 | 11 | :::note 12 | This repository was forked from [bitly/OAuth2_Proxy](https://github.com/bitly/oauth2_proxy) on 27/11/2018. 13 | Versions v3.0.0 and up are from this fork and will have diverged from any changes in the original fork. 14 | A list of changes can be seen in the [CHANGELOG](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/CHANGELOG.md). 15 | ::: 16 | 17 | ![Sign In Page](../../static/img/sign-in-page.png) 18 | 19 | ## Architecture 20 | 21 | ![OAuth2 Proxy Architecture](../../static/img/architecture.png) 22 | -------------------------------------------------------------------------------- /providers/facebook_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | func TestNewFacebookProvider(t *testing.T) { 10 | g := NewWithT(t) 11 | 12 | // Test that defaults are set when calling for a new provider with nothing set 13 | providerData := NewFacebookProvider(&ProviderData{}).Data() 14 | g.Expect(providerData.ProviderName).To(Equal("Facebook")) 15 | g.Expect(providerData.LoginURL.String()).To(Equal("https://www.facebook.com/v2.5/dialog/oauth")) 16 | g.Expect(providerData.RedeemURL.String()).To(Equal("https://graph.facebook.com/v2.5/oauth/access_token")) 17 | g.Expect(providerData.ProfileURL.String()).To(Equal("https://graph.facebook.com/v2.5/me")) 18 | g.Expect(providerData.ValidateURL.String()).To(Equal("https://graph.facebook.com/v2.5/me")) 19 | g.Expect(providerData.Scope).To(Equal("public_profile email")) 20 | } 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docusaurus", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve" 12 | }, 13 | "dependencies": { 14 | "@docusaurus/core": "^2.0.0-beta.15", 15 | "@docusaurus/preset-classic": "^2.0.0-beta.15", 16 | "@mdx-js/react": "^1.6.22", 17 | "clsx": "^1.1.1", 18 | "react": "^16.14.0", 19 | "react-dom": "^16.14.0" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/cookies/cookies_suite_test.go: -------------------------------------------------------------------------------- 1 | package cookies 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | const ( 14 | csrfState = "1234asdf1234asdf1234asdf" 15 | csrfNonce = "0987lkjh0987lkjh0987lkjh" 16 | 17 | cookieName = "cookie_test_12345" 18 | cookieSecret = "3q48hmFH30FJ2HfJF0239UFJCVcl3kj3" 19 | cookieDomain = "o2p.cookies.test" 20 | cookiePath = "/cookie-tests" 21 | 22 | nowEpoch = 1609366421 23 | ) 24 | 25 | func TestProviderSuite(t *testing.T) { 26 | logger.SetOutput(GinkgoWriter) 27 | 28 | RegisterFailHandler(Fail) 29 | RunSpecs(t, "Cookies") 30 | } 31 | 32 | func testCookieExpires(exp time.Time) string { 33 | var buf [len(http.TimeFormat)]byte 34 | return string(exp.UTC().AppendFormat(buf[:0], http.TimeFormat)) 35 | } 36 | -------------------------------------------------------------------------------- /docs/docs/behaviour.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: behaviour 3 | title: Behaviour 4 | --- 5 | 6 | 1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). 7 | 2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) 8 | 3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set 9 | 4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) 10 | 11 | Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md). 12 | -------------------------------------------------------------------------------- /contrib/local-environment/oauth2-proxy-keycloak.cfg: -------------------------------------------------------------------------------- 1 | http_address="0.0.0.0:4180" 2 | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" 3 | email_domains=["example.com"] 4 | cookie_secure="false" 5 | upstreams="http://httpbin" 6 | cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains. 7 | whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target. 8 | 9 | # keycloak provider 10 | client_secret="72341b6d-7065-4518-a0e4-50ee15025608" 11 | client_id="oauth2-proxy" 12 | redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback" 13 | 14 | # in this case oauth2-proxy is going to visit 15 | # http://keycloak.localtest.me:9080/auth/realms/master/.well-known/openid-configuration for configuration 16 | oidc_issuer_url="http://keycloak.localtest.me:9080/auth/realms/master" 17 | provider="oidc" 18 | provider_display_name="Keycloak" 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-6.1.x/behaviour.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: behaviour 3 | title: Behaviour 4 | --- 5 | 6 | 1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). 7 | 2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) 8 | 3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set 9 | 4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) 10 | 11 | Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md). 12 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.0.x/behaviour.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: behaviour 3 | title: Behaviour 4 | --- 5 | 6 | 1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). 7 | 2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) 8 | 3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set 9 | 4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) 10 | 11 | Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md). 12 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.1.x/behaviour.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: behaviour 3 | title: Behaviour 4 | --- 5 | 6 | 1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). 7 | 2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) 8 | 3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set 9 | 4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) 10 | 11 | Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md). 12 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.2.x/behaviour.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: behaviour 3 | title: Behaviour 4 | --- 5 | 6 | 1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). 7 | 2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) 8 | 3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set 9 | 4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) 10 | 11 | Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md). 12 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.3.x/behaviour.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: behaviour 3 | title: Behaviour 4 | --- 5 | 6 | 1. Any request passing through the proxy (and not matched by `--skip-auth-regex`) is checked for the proxy's session cookie (`--cookie-name`) (or, if allowed, a JWT token - see `--skip-jwt-bearer-tokens`). 7 | 2. If authentication is required but missing then the user is asked to log in and redirected to the authentication provider (unless it is an Ajax request, i.e. one with `Accept: application/json`, in which case 401 Unauthorized is returned) 8 | 3. After returning from the authentication provider, the oauth tokens are stored in the configured session store (cookie, redis, ...) and a cookie is set 9 | 4. The request is forwarded to the upstream server with added user info and authentication headers (depending on the configuration) 10 | 11 | Notice that the proxy also provides a number of useful [endpoints](features/endpoints.md). 12 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 120s 3 | linters: 4 | enable: 5 | - govet 6 | - golint 7 | - ineffassign 8 | - goconst 9 | - deadcode 10 | - gofmt 11 | - goimports 12 | - gosec 13 | - gosimple 14 | - staticcheck 15 | - structcheck 16 | - typecheck 17 | - unused 18 | - varcheck 19 | - bodyclose 20 | - dogsled 21 | - goprintffuncname 22 | - misspell 23 | - prealloc 24 | - scopelint 25 | - stylecheck 26 | - unconvert 27 | - gocritic 28 | disable-all: true 29 | issues: 30 | exclude-rules: 31 | - path: _test\.go 32 | linters: 33 | - scopelint 34 | - bodyclose 35 | - unconvert 36 | - gocritic 37 | - gosec 38 | # If we have tests in shared test folders, these can be less strictly linted 39 | - path: tests/.*_tests\.go 40 | linters: 41 | - golint 42 | - bodyclose 43 | - stylecheck 44 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #25c2a0; 11 | --ifm-color-primary-dark: rgb(33, 175, 144); 12 | --ifm-color-primary-darker: rgb(31, 165, 136); 13 | --ifm-color-primary-darkest: rgb(26, 136, 112); 14 | --ifm-color-primary-light: rgb(70, 203, 174); 15 | --ifm-color-primary-lighter: rgb(102, 212, 189); 16 | --ifm-color-primary-lightest: rgb(146, 224, 208); 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | -------------------------------------------------------------------------------- /docs/versioned_sidebars/version-7.3.x-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": [ 3 | { 4 | "type": "doc", 5 | "id": "installation" 6 | }, 7 | { 8 | "type": "doc", 9 | "id": "behaviour" 10 | }, 11 | { 12 | "type": "category", 13 | "label": "Configuration", 14 | "collapsed": false, 15 | "items": [ 16 | "configuration/overview", 17 | "configuration/oauth_provider", 18 | "configuration/session_storage", 19 | "configuration/tls", 20 | "configuration/alpha-config" 21 | ] 22 | }, 23 | { 24 | "type": "category", 25 | "label": "Features", 26 | "collapsed": false, 27 | "items": [ 28 | "features/endpoints" 29 | ] 30 | }, 31 | { 32 | "type": "category", 33 | "label": "Community", 34 | "collapsed": false, 35 | "items": [ 36 | "community/security" 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /pkg/header/header_suite_test.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | "testing" 8 | 9 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var ( 15 | filesDir string 16 | ) 17 | 18 | func TestHeaderSuite(t *testing.T) { 19 | logger.SetOutput(GinkgoWriter) 20 | logger.SetErrOutput(GinkgoWriter) 21 | 22 | RegisterFailHandler(Fail) 23 | RunSpecs(t, "Header") 24 | } 25 | 26 | var _ = BeforeSuite(func() { 27 | os.Setenv("SECRET_ENV", "super-secret-env") 28 | 29 | dir, err := ioutil.TempDir("", "oauth2-proxy-header-suite") 30 | Expect(err).ToNot(HaveOccurred()) 31 | Expect(ioutil.WriteFile(path.Join(dir, "secret-file"), []byte("super-secret-file"), 0644)).To(Succeed()) 32 | filesDir = dir 33 | }) 34 | 35 | var _ = AfterSuite(func() { 36 | os.Unsetenv("SECRET_ENV") 37 | Expect(os.RemoveAll(filesDir)).To(Succeed()) 38 | }) 39 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Default owner should be a core org reviewers unless overridden by later rules in this file 2 | * @oauth2-proxy/reviewers 3 | 4 | # login.gov provider 5 | # Note: If @timothy-spencer terms out of his appointment, your best bet 6 | # for finding somebody who can test the oauth2-proxy would be to ask somebody 7 | # in the login.gov team (https://login.gov/developers/), the cloud.gov team 8 | # (https://cloud.gov/docs/help/), or the 18F org (https://18f.gsa.gov/contact/ 9 | # or the public devops channel at https://chat.18f.gov/). 10 | providers/logingov.go @timothy-spencer 11 | providers/logingov_test.go @timothy-spencer 12 | 13 | # Bitbucket provider 14 | providers/bitbucket.go @aledeganopix4d 15 | providers/bitbucket_test.go @aledeganopix4d 16 | 17 | # Nextcloud provider 18 | providers/nextcloud.go @Ramblurr 19 | providers/nextcloud_test.go @Ramblurr 20 | 21 | # DigitalOcean provider 22 | providers/digitalocean.go @kamaln7 23 | providers/digitalocean_test.go @kamaln7 24 | -------------------------------------------------------------------------------- /pkg/middleware/middleware_suite_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | func TestMiddlewareSuite(t *testing.T) { 14 | logger.SetOutput(GinkgoWriter) 15 | logger.SetErrOutput(GinkgoWriter) 16 | 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "Middleware") 19 | } 20 | 21 | func testHandler() http.Handler { 22 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 23 | rw.WriteHeader(200) 24 | rw.Write([]byte("test")) 25 | }) 26 | } 27 | 28 | func testUpstreamHandler(upstream string) http.Handler { 29 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 30 | scope := middlewareapi.GetRequestScope(req) 31 | scope.Upstream = upstream 32 | 33 | rw.WriteHeader(200) 34 | rw.Write([]byte("test")) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/middleware/session_utils.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // splitAuthHeader takes the auth header value and splits it into the token type 10 | // and the token value. 11 | func splitAuthHeader(header string) (string, string, error) { 12 | s := strings.Split(header, " ") 13 | if len(s) != 2 { 14 | return "", "", fmt.Errorf("invalid authorization header: %q", header) 15 | } 16 | return s[0], s[1], nil 17 | } 18 | 19 | // getBasicAuthCredentials decodes a basic auth token and extracts the user 20 | // and password pair. 21 | func getBasicAuthCredentials(token string) (string, string, error) { 22 | b, err := base64.StdEncoding.DecodeString(token) 23 | if err != nil { 24 | return "", "", fmt.Errorf("invalid basic auth token: %v", err) 25 | } 26 | 27 | pair := strings.SplitN(string(b), ":", 2) 28 | if len(pair) != 2 { 29 | return "", "", fmt.Errorf("invalid format: %q", b) 30 | } 31 | // user, password 32 | return pair[0], pair[1], nil 33 | } 34 | -------------------------------------------------------------------------------- /providers/auth_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 9 | ) 10 | 11 | var authorizedAccessToken = "imaginary_access_token" 12 | 13 | func CreateAuthorizedSession() *sessions.SessionState { 14 | return &sessions.SessionState{AccessToken: authorizedAccessToken} 15 | } 16 | 17 | func IsAuthorizedInHeader(reqHeader http.Header) bool { 18 | return IsAuthorizedInHeaderWithToken(reqHeader, authorizedAccessToken) 19 | } 20 | 21 | func IsAuthorizedInHeaderWithToken(reqHeader http.Header, token string) bool { 22 | return reqHeader.Get("Authorization") == fmt.Sprintf("Bearer %s", token) 23 | } 24 | 25 | func IsAuthorizedInURL(reqURL *url.URL) bool { 26 | return reqURL.Query().Get("access_token") == authorizedAccessToken 27 | } 28 | 29 | func isAuthorizedRefreshInURLWithToken(reqURL *url.URL, token string) bool { 30 | if token == "" { 31 | return false 32 | } 33 | return reqURL.Query().Get("refresh_token") == token 34 | } 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Motivation and Context 8 | 9 | 10 | 11 | 12 | ## How Has This Been Tested? 13 | 14 | 15 | 16 | 17 | 18 | ## Checklist: 19 | 20 | 21 | 22 | 23 | - [ ] My change requires a change to the documentation or CHANGELOG. 24 | - [ ] I have updated the documentation/CHANGELOG accordingly. 25 | - [ ] I have created a feature (non-master) branch for my PR. 26 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-6.1.x/features/request_signatures.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: request_signatures 3 | title: Request Signatures 4 | --- 5 | 6 | If `signature_key` is defined, proxied requests will be signed with the 7 | `GAP-Signature` header, which is a [Hash-based Message Authentication Code 8 | (HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) 9 | of selected request information and the request body [see `SIGNATURE_HEADERS` 10 | in `oauthproxy.go`](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/oauthproxy.go). 11 | 12 | `signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`) 13 | 14 | For more information about HMAC request signature validation, read the 15 | following: 16 | 17 | - [Amazon Web Services: Signing and Authenticating REST 18 | Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) 19 | - [rc3.org: Using HMAC to authenticate Web service 20 | requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/) 21 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.0.x/features/request_signatures.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: request_signatures 3 | title: Request Signatures 4 | --- 5 | 6 | If `signature_key` is defined, proxied requests will be signed with the 7 | `GAP-Signature` header, which is a [Hash-based Message Authentication Code 8 | (HMAC)](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) 9 | of selected request information and the request body [see `SIGNATURE_HEADERS` 10 | in `oauthproxy.go`](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/oauthproxy.go). 11 | 12 | `signature_key` must be of the form `algorithm:secretkey`, (ie: `signature_key = "sha1:secret0"`) 13 | 14 | For more information about HMAC request signature validation, read the 15 | following: 16 | 17 | - [Amazon Web Services: Signing and Authenticating REST 18 | Requests](https://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html) 19 | - [rc3.org: Using HMAC to authenticate Web service 20 | requests](http://rc3.org/2011/12/02/using-hmac-to-authenticate-web-service-requests/) 21 | -------------------------------------------------------------------------------- /contrib/local-environment/docker-compose-alpha-config.yaml: -------------------------------------------------------------------------------- 1 | # This docker-compose file can be used to bring up an example instance of oauth2-proxy 2 | # for manual testing and exploration of features. 3 | # Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider, 4 | # etcd for storage for Dex and HTTPBin as an example upstream. 5 | # This file also uses alpha configuration when configuring OAuth2 Proxy. 6 | # 7 | # This file is an extension of the main compose file and must be used with it 8 | # docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml 9 | # Alternatively: 10 | # make alpha-config- (eg make nginx-up, make nginx-down) 11 | # 12 | # Access http://localhost:4180 to initiate a login cycle 13 | version: '3.0' 14 | services: 15 | oauth2-proxy: 16 | command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml 17 | volumes: 18 | - "./oauth2-proxy-alpha-config.cfg:/oauth2-proxy.cfg" 19 | - "./oauth2-proxy-alpha-config.yaml:/oauth2-proxy-alpha-config.yaml" 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /pkg/middleware/scope.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/google/uuid" 7 | "github.com/justinas/alice" 8 | middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 9 | ) 10 | 11 | func NewScope(reverseProxy bool, idHeader string) alice.Constructor { 12 | return func(next http.Handler) http.Handler { 13 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 14 | scope := &middlewareapi.RequestScope{ 15 | ReverseProxy: reverseProxy, 16 | RequestID: genRequestID(req, idHeader), 17 | } 18 | req = middlewareapi.AddRequestScope(req, scope) 19 | next.ServeHTTP(rw, req) 20 | }) 21 | } 22 | } 23 | 24 | // genRequestID sets a request-wide ID for use in logging or error pages. 25 | // If a RequestID header is set, it uses that. Otherwise, it generates a random 26 | // UUID for the lifespan of the request. 27 | func genRequestID(req *http.Request, idHeader string) string { 28 | rid := req.Header.Get(idHeader) 29 | if rid != "" { 30 | return rid 31 | } 32 | return uuid.New().String() 33 | } 34 | -------------------------------------------------------------------------------- /contrib/local-environment/keycloak/master-users-0.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm" : "master", 3 | "users" : [ { 4 | "id" : "3356c0a0-d4d5-4436-9c5a-2299c71c08ec", 5 | "createdTimestamp" : 1591297959169, 6 | "username" : "admin@example.com", 7 | "email" : "admin@example.com", 8 | "enabled" : true, 9 | "totp" : false, 10 | "emailVerified" : true, 11 | "credentials" : [ { 12 | "id" : "a1a06ecd-fdc0-4e67-92cd-2da22d724e32", 13 | "type" : "password", 14 | "createdDate" : 1591297959315, 15 | "secretData" : "{\"value\":\"6rt5zuqHVHopvd0FTFE0CYadXTtzY0mDY2BrqnNQGS51/7DfMJeGgj0roNnGMGvDv30imErNmiSOYl+cL9jiIA==\",\"salt\":\"LI0kqr09JB7J9wvr2Hxzzg==\"}", 16 | "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" 17 | } ], 18 | "disableableCredentialTypes" : [ ], 19 | "requiredActions" : [ ], 20 | "realmRoles" : [ "offline_access", "admin", "uma_authorization" ], 21 | "clientRoles" : { 22 | "account" : [ "view-profile", "manage-account" ] 23 | }, 24 | "notBefore" : 0, 25 | "groups" : [ ] 26 | } ] 27 | } 28 | -------------------------------------------------------------------------------- /contrib/local-environment/oauth2-proxy-traefik.cfg: -------------------------------------------------------------------------------- 1 | http_address="0.0.0.0:4180" 2 | cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" 3 | provider="oidc" 4 | email_domains=["example.com"] 5 | oidc_issuer_url="http://dex.localhost:4190/dex" 6 | client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" 7 | client_id="oauth2-proxy" 8 | cookie_secure="false" 9 | 10 | redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback" 11 | cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains. 12 | whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target. 13 | 14 | # Mandatory option when using oauth2-proxy with traefik 15 | reverse_proxy="true" 16 | # Required for traefik with ForwardAuth and static upstream configuration 17 | upstreams="static://202" 18 | # The following option skip the page requesting the user 19 | # to click on a button to be redirected to the identity provider 20 | # It can be activated only when traefik is not configure with 21 | # the error redirection middleware as this example. 22 | skip_provider_button="true" 23 | -------------------------------------------------------------------------------- /pkg/encryption/nonce.go: -------------------------------------------------------------------------------- 1 | package encryption 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/rand" 6 | "encoding/base64" 7 | 8 | "golang.org/x/crypto/blake2b" 9 | ) 10 | 11 | // Nonce generates a random n-byte slice 12 | func Nonce(length int) ([]byte, error) { 13 | b := make([]byte, length) 14 | _, err := rand.Read(b) 15 | if err != nil { 16 | return nil, err 17 | } 18 | return b, nil 19 | } 20 | 21 | // HashNonce returns the BLAKE2b 256-bit hash of a nonce 22 | // NOTE: Error checking (G104) is purposefully skipped: 23 | // - `blake2b.New256` has no error path with a nil signing key 24 | // - `hash.Hash` interface's `Write` has an error signature, but 25 | // `blake2b.digest.Write` does not use it. 26 | /* #nosec G104 */ 27 | func HashNonce(nonce []byte) string { 28 | hasher, _ := blake2b.New256(nil) 29 | hasher.Write(nonce) 30 | sum := hasher.Sum(nil) 31 | return base64.RawURLEncoding.EncodeToString(sum) 32 | } 33 | 34 | // CheckNonce tests if a nonce matches the hashed version of it 35 | func CheckNonce(nonce []byte, hashed string) bool { 36 | return hmac.Equal([]byte(HashNonce(nonce)), []byte(hashed)) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/app/redirect/pagewriter_suite_test.go: -------------------------------------------------------------------------------- 1 | package redirect 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestOptionsSuite(t *testing.T) { 12 | logger.SetOutput(GinkgoWriter) 13 | logger.SetErrOutput(GinkgoWriter) 14 | 15 | RegisterFailHandler(Fail) 16 | RunSpecs(t, "Redirect Suite") 17 | } 18 | 19 | // testValidator creates a mock validator that will always return the given result. 20 | func testValidator(result bool, allowedRedirects ...string) Validator { 21 | return &mockValidator{result: result, allowedRedirects: allowedRedirects} 22 | } 23 | 24 | // mockValidator implements the Validator interface for use in testing. 25 | type mockValidator struct { 26 | result bool 27 | allowedRedirects []string 28 | } 29 | 30 | // IsValidRedirect implements the Validator interface. 31 | func (m *mockValidator) IsValidRedirect(redirect string) bool { 32 | for _, allowed := range m.allowedRedirects { 33 | if redirect == allowed { 34 | return true 35 | } 36 | } 37 | 38 | return m.result 39 | } 40 | -------------------------------------------------------------------------------- /contrib/local-environment/dex.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is intended to be used with the docker-compose testing 2 | # environment. 3 | # This should configure Dex to run on port 4190 and provides a static login 4 | issuer: http://dex.localhost:4190/dex 5 | storage: 6 | type: etcd 7 | config: 8 | endpoints: 9 | - http://etcd:2379 10 | namespace: dex/ 11 | web: 12 | http: 0.0.0.0:4190 13 | oauth2: 14 | skipApprovalScreen: true 15 | expiry: 16 | signingKeys: "4h" 17 | idTokens: "1h" 18 | staticClients: 19 | - id: oauth2-proxy 20 | redirectURIs: 21 | # These redirect URIs point to the `--redirect-url` for OAuth2 proxy. 22 | - 'http://localhost:4180/oauth2/callback' # For basic proxy example. 23 | - 'http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback' # For nginx and traefik example. 24 | name: 'OAuth2 Proxy' 25 | secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK 26 | enablePasswordDB: true 27 | staticPasswords: 28 | - email: "admin@example.com" 29 | # bcrypt hash of the string "password" 30 | hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" 31 | username: "admin" 32 | userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" 33 | -------------------------------------------------------------------------------- /docs/static/img/logos/OAuth2_Proxy_icon.svg: -------------------------------------------------------------------------------- 1 | OAuth2_Proxy_logo_v3 -------------------------------------------------------------------------------- /docs/versioned_docs/version-6.1.x/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | 1. Choose how to deploy: 8 | 9 | a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v6.1.1`) 10 | 11 | b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy` which will put the binary in `$GOPATH/bin` 12 | 13 | c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) 14 | 15 | Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. 16 | 17 | ``` 18 | $ sha256sum -c sha256sum.txt 2>&1 | grep OK 19 | oauth2-proxy-x.y.z.linux-amd64: OK 20 | ``` 21 | 22 | 2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md) 23 | 3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md) 24 | 4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx) 25 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.0.x/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | 1. Choose how to deploy: 8 | 9 | a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.0.1`) 10 | 11 | b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin` 12 | 13 | c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) 14 | 15 | Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. 16 | 17 | ``` 18 | $ sha256sum -c sha256sum.txt 2>&1 | grep OK 19 | oauth2-proxy-x.y.z.linux-amd64: OK 20 | ``` 21 | 22 | 2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md) 23 | 3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md) 24 | 4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx) 25 | -------------------------------------------------------------------------------- /pkg/sessions/tests/mock_lock.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 8 | ) 9 | 10 | type MockLock struct { 11 | expiration time.Duration 12 | elapsed time.Duration 13 | } 14 | 15 | func (l *MockLock) Obtain(ctx context.Context, expiration time.Duration) error { 16 | l.expiration = expiration 17 | return nil 18 | } 19 | 20 | func (l *MockLock) Peek(ctx context.Context) (bool, error) { 21 | if l.elapsed < l.expiration { 22 | return true, nil 23 | } 24 | return false, nil 25 | } 26 | 27 | func (l *MockLock) Refresh(ctx context.Context, expiration time.Duration) error { 28 | if l.expiration <= l.elapsed { 29 | return sessions.ErrNotLocked 30 | } 31 | l.expiration = expiration 32 | l.elapsed = time.Duration(0) 33 | return nil 34 | } 35 | 36 | func (l *MockLock) Release(ctx context.Context) error { 37 | if l.expiration <= l.elapsed { 38 | return sessions.ErrNotLocked 39 | } 40 | l.expiration = time.Duration(0) 41 | l.elapsed = time.Duration(0) 42 | return nil 43 | } 44 | 45 | // FastForward simulates the flow of time to test expirations 46 | func (l *MockLock) FastForward(duration time.Duration) { 47 | l.elapsed += duration 48 | } 49 | -------------------------------------------------------------------------------- /docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | 1. Choose how to deploy: 8 | 9 | a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.3.0`) 10 | 11 | b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin` 12 | 13 | c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) 14 | 15 | d. Using a [Kubernetes manifest](https://github.com/oauth2-proxy/manifests) (Helm) 16 | 17 | Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. 18 | 19 | ``` 20 | $ sha256sum -c sha256sum.txt 21 | oauth2-proxy-x.y.z.linux-amd64: OK 22 | ``` 23 | 24 | 2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md) 25 | 3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md) 26 | 4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx) 27 | -------------------------------------------------------------------------------- /pkg/validation/common.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 8 | ) 9 | 10 | const multipleValuesForSecretSource = "multiple values specified for secret source: specify either value, fromEnv of fromFile" 11 | 12 | func validateSecretSource(source options.SecretSource) string { 13 | switch { 14 | case len(source.Value) > 0 && source.FromEnv == "" && source.FromFile == "": 15 | return "" 16 | case len(source.Value) == 0 && source.FromEnv != "" && source.FromFile == "": 17 | return validateSecretSourceEnv(source.FromEnv) 18 | case len(source.Value) == 0 && source.FromEnv == "" && source.FromFile != "": 19 | return validateSecretSourceFile(source.FromFile) 20 | default: 21 | return multipleValuesForSecretSource 22 | } 23 | } 24 | 25 | func validateSecretSourceEnv(key string) string { 26 | if value := os.Getenv(key); value == "" { 27 | return fmt.Sprintf("error loading secret from environent: no value for for key %q", key) 28 | } 29 | return "" 30 | } 31 | 32 | func validateSecretSourceFile(path string) string { 33 | if _, err := os.Stat(path); err != nil { 34 | return fmt.Sprintf("error loadig secret from file: %v", err) 35 | } 36 | return "" 37 | } 38 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.2.x/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | 1. Choose how to deploy: 8 | 9 | a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.2.1`) 10 | 11 | b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin` 12 | 13 | c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) 14 | 15 | d. Using a [Kubernetes manifest](https://github.com/oauth2-proxy/manifests) (Helm) 16 | 17 | Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. 18 | 19 | ``` 20 | $ sha256sum -c sha256sum.txt 21 | oauth2-proxy-x.y.z.linux-amd64: OK 22 | ``` 23 | 24 | 2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md) 25 | 3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md) 26 | 4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx) 27 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.3.x/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | 1. Choose how to deploy: 8 | 9 | a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.3.0`) 10 | 11 | b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin` 12 | 13 | c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) 14 | 15 | d. Using a [Kubernetes manifest](https://github.com/oauth2-proxy/manifests) (Helm) 16 | 17 | Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. 18 | 19 | ``` 20 | $ sha256sum -c sha256sum.txt 21 | oauth2-proxy-x.y.z.linux-amd64: OK 22 | ``` 23 | 24 | 2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md) 25 | 3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md) 26 | 4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx) 27 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.1.x/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installation 4 | slug: / 5 | --- 6 | 7 | 1. Choose how to deploy: 8 | 9 | a. Download [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.1.3`) 10 | 11 | b. Build with `$ go get github.com/oauth2-proxy/oauth2-proxy/v7` which will put the binary in `$GOPATH/bin` 12 | 13 | c. Using the prebuilt docker image [quay.io/oauth2-proxy/oauth2-proxy](https://quay.io/oauth2-proxy/oauth2-proxy) (AMD64, ARMv6 and ARM64 tags available) 14 | 15 | d. Using a [Kubernetes manifest](https://github.com/oauth2-proxy/manifests) (Helm) 16 | 17 | Prebuilt binaries can be validated by extracting the file and verifying it against the `sha256sum.txt` checksum file provided for each release starting with version `v3.0.0`. 18 | 19 | ``` 20 | $ sha256sum -c sha256sum.txt 2>&1 | grep OK 21 | oauth2-proxy-x.y.z.linux-amd64: OK 22 | ``` 23 | 24 | 2. [Select a Provider and Register an OAuth Application with a Provider](configuration/auth.md) 25 | 3. [Configure OAuth2 Proxy using config file, command line options, or environment variables](configuration/overview.md) 26 | 4. [Configure SSL or Deploy behind a SSL endpoint](configuration/tls.md) (example provided for Nginx) 27 | -------------------------------------------------------------------------------- /contrib/local-environment/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: up 2 | up: 3 | docker-compose up -d 4 | 5 | .PHONY: % 6 | %: 7 | docker-compose $* 8 | 9 | .PHONY: alpha-config-up 10 | alpha-config-up: 11 | docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml up -d 12 | 13 | .PHONY: alpha-config-% 14 | alpha-config-%: 15 | docker-compose -f docker-compose.yaml -f docker-compose-alpha-config.yaml $* 16 | 17 | .PHONY: nginx-up 18 | nginx-up: 19 | docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml up -d 20 | 21 | .PHONY: nginx-% 22 | nginx-%: 23 | docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $* 24 | 25 | .PHONY: keycloak-up 26 | keycloak-up: 27 | docker-compose -f docker-compose-keycloak.yaml up -d 28 | 29 | .PHONY: keycloak-% 30 | keycloak-%: 31 | docker-compose -f docker-compose-keycloak.yaml $* 32 | 33 | .PHONY: kubernetes-up 34 | kubernetes-up: 35 | make -C kubernetes create-cluster 36 | make -C kubernetes deploy 37 | 38 | .PHONY: kubernetes-down 39 | kubernetes-down: 40 | make -C kubernetes delete-cluster 41 | 42 | .PHONY: traefik-up 43 | traefik-up: 44 | docker-compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d 45 | 46 | .PHONY: traefik-% 47 | traefik-%: 48 | docker-compose -f docker-compose.yaml -f docker-compose-traefik.yaml $* 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Expected Behavior 4 | 5 | 6 | 7 | 8 | ## Current Behavior 9 | 10 | 11 | 12 | 13 | ## Possible Solution 14 | 15 | 16 | 17 | 18 | ## Steps to Reproduce (for bugs) 19 | 20 | 21 | 22 | 23 | 1. 24 | 2. 25 | 3. 26 | 4. 27 | 28 | ## Context 29 | 30 | 31 | 32 | 33 | ## Your Environment 34 | 35 | 36 | 37 | - Version used: 38 | -------------------------------------------------------------------------------- /pkg/apis/sessions/interfaces.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // SessionStore is an interface to storing user sessions in the proxy 11 | type SessionStore interface { 12 | Save(rw http.ResponseWriter, req *http.Request, s *SessionState) error 13 | Load(req *http.Request) (*SessionState, error) 14 | Clear(rw http.ResponseWriter, req *http.Request) error 15 | } 16 | 17 | var ErrLockNotObtained = errors.New("lock: not obtained") 18 | var ErrNotLocked = errors.New("tried to release not existing lock") 19 | 20 | // Lock is an interface for controlling session locks 21 | type Lock interface { 22 | // Obtain obtains the lock on the distributed 23 | // lock resource if no lock exists yet. 24 | // Otherwise it will return ErrLockNotObtained 25 | Obtain(ctx context.Context, expiration time.Duration) error 26 | // Peek returns true if the lock currently exists 27 | // Otherwise it returns false. 28 | Peek(ctx context.Context) (bool, error) 29 | // Refresh refreshes the expiration time of the lock, 30 | // if is still applied. 31 | // Otherwise it will return ErrNotLocked 32 | Refresh(ctx context.Context, expiration time.Duration) error 33 | // Release removes the existing lock, 34 | // Otherwise it will return ErrNotLocked 35 | Release(ctx context.Context) error 36 | } 37 | -------------------------------------------------------------------------------- /dist.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | 5 | if [[ -z ${BINARY} ]] || [[ -z ${VERSION} ]]; then 6 | echo "Missing required env var: BINARY=X VERSION=X $(basename $0)" 7 | exit 1 8 | fi 9 | 10 | ARCHS=(darwin-amd64 linux-amd64 linux-arm64 linux-ppc64le linux-armv6 freebsd-amd64 windows-amd64) 11 | 12 | mkdir -p release 13 | 14 | # Create architecture specific release dirs 15 | for ARCH in "${ARCHS[@]}"; do 16 | mkdir -p release/${BINARY}-${VERSION}.${ARCH} 17 | 18 | GO_OS=$(echo $ARCH | awk -F- '{print $1}') 19 | GO_ARCH=$(echo $ARCH | awk -F- '{print $2}') 20 | 21 | # Create architecture specific binaries 22 | if [[ ${GO_ARCH} == "armv6" ]]; then 23 | GO111MODULE=on GOOS=${GO_OS} GOARCH=arm GOARM=6 CGO_ENABLED=0 go build -ldflags="-X main.VERSION=${VERSION}" \ 24 | -o release/${BINARY}-${VERSION}.${ARCH}/${BINARY} . 25 | else 26 | GO111MODULE=on GOOS=${GO_OS} GOARCH=${GO_ARCH} CGO_ENABLED=0 go build -ldflags="-X main.VERSION=${VERSION}" \ 27 | -o release/${BINARY}-${VERSION}.${ARCH}/${BINARY} . 28 | fi 29 | 30 | cd release 31 | 32 | # Create sha256sum for architecture specific binary 33 | sha256sum ${BINARY}-${VERSION}.${ARCH}/${BINARY} > ${BINARY}-${VERSION}.${ARCH}-sha256sum.txt 34 | 35 | # Create tar file for architecture specific binary 36 | tar -czvf ${BINARY}-${VERSION}.${ARCH}.tar.gz ${BINARY}-${VERSION}.${ARCH} 37 | 38 | cd .. 39 | done 40 | -------------------------------------------------------------------------------- /pkg/middleware/healthcheck.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/justinas/alice" 8 | ) 9 | 10 | func NewHealthCheck(paths, userAgents []string) alice.Constructor { 11 | return func(next http.Handler) http.Handler { 12 | return healthCheck(paths, userAgents, next) 13 | } 14 | } 15 | 16 | func healthCheck(paths, userAgents []string, next http.Handler) http.Handler { 17 | // Use a map as a set to check health check paths 18 | pathSet := make(map[string]struct{}) 19 | for _, path := range paths { 20 | if len(path) > 0 { 21 | pathSet[path] = struct{}{} 22 | } 23 | } 24 | 25 | // Use a map as a set to check health check paths 26 | userAgentSet := make(map[string]struct{}) 27 | for _, userAgent := range userAgents { 28 | if len(userAgent) > 0 { 29 | userAgentSet[userAgent] = struct{}{} 30 | } 31 | } 32 | 33 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 34 | if isHealthCheckRequest(pathSet, userAgentSet, req) { 35 | rw.WriteHeader(http.StatusOK) 36 | fmt.Fprintf(rw, "OK") 37 | return 38 | } 39 | 40 | next.ServeHTTP(rw, req) 41 | }) 42 | } 43 | 44 | func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool { 45 | if _, ok := paths[req.URL.EscapedPath()]; ok { 46 | return true 47 | } 48 | if _, ok := userAgents[req.Header.Get("User-Agent")]; ok { 49 | return true 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /pkg/upstream/static.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 9 | ) 10 | 11 | const defaultStaticResponseCode = 200 12 | 13 | // newStaticResponseHandler creates a new staticResponseHandler that serves a 14 | // a static response code. 15 | func newStaticResponseHandler(upstream string, code *int) http.Handler { 16 | return &staticResponseHandler{ 17 | code: derefStaticCode(code), 18 | upstream: upstream, 19 | } 20 | } 21 | 22 | // staticResponseHandler responds with a static response with the given response code. 23 | type staticResponseHandler struct { 24 | code int 25 | upstream string 26 | } 27 | 28 | // ServeHTTP serves a static response. 29 | func (s *staticResponseHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 30 | scope := middleware.GetRequestScope(req) 31 | // If scope is nil, this will panic. 32 | // A scope should always be injected before this handler is called. 33 | scope.Upstream = s.upstream 34 | 35 | rw.WriteHeader(s.code) 36 | _, err := fmt.Fprintf(rw, "Authenticated") 37 | if err != nil { 38 | logger.Errorf("Error writing static response: %v", err) 39 | } 40 | } 41 | 42 | // derefStaticCode returns the derefenced value, or the default if the value is nil 43 | func derefStaticCode(code *int) int { 44 | if code != nil { 45 | return *code 46 | } 47 | return defaultStaticResponseCode 48 | } 49 | -------------------------------------------------------------------------------- /pkg/apis/options/server.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | // Server represents the configuration for an HTTP(S) server 4 | type Server struct { 5 | // BindAddress is the address on which to serve traffic. 6 | // Leave blank or set to "-" to disable. 7 | BindAddress string 8 | 9 | // SecureBindAddress is the address on which to serve secure traffic. 10 | // Leave blank or set to "-" to disable. 11 | SecureBindAddress string 12 | 13 | // TLS contains the information for loading the certificate and key for the 14 | // secure traffic and further configuration for the TLS server. 15 | TLS *TLS 16 | } 17 | 18 | // TLS contains the information for loading a TLS certificate and key 19 | // as well as an optional minimal TLS version that is acceptable. 20 | type TLS struct { 21 | // Key is the TLS key data to use. 22 | // Typically this will come from a file. 23 | Key *SecretSource 24 | 25 | // Cert is the TLS certificate data to use. 26 | // Typically this will come from a file. 27 | Cert *SecretSource 28 | 29 | // MinVersion is the minimal TLS version that is acceptable. 30 | // E.g. Set to "TLS1.3" to select TLS version 1.3 31 | MinVersion string 32 | 33 | // CipherSuites is a list of TLS cipher suites that are allowed. 34 | // E.g.: 35 | // - TLS_RSA_WITH_RC4_128_SHA 36 | // - TLS_RSA_WITH_AES_256_GCM_SHA384 37 | // If not specified, the default Go safe cipher list is used. 38 | // List of valid cipher suites can be found in the [crypto/tls documentation](https://pkg.go.dev/crypto/tls#pkg-constants). 39 | CipherSuites []string 40 | } 41 | -------------------------------------------------------------------------------- /docs/versioned_sidebars/version-6.1.x-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-6.1.x/docs": [ 3 | { 4 | "type": "doc", 5 | "id": "version-6.1.x/installation" 6 | }, 7 | { 8 | "type": "doc", 9 | "id": "version-6.1.x/behaviour" 10 | }, 11 | { 12 | "collapsed": false, 13 | "type": "category", 14 | "label": "Configuration", 15 | "items": [ 16 | { 17 | "type": "doc", 18 | "id": "version-6.1.x/configuration/overview" 19 | }, 20 | { 21 | "type": "doc", 22 | "id": "version-6.1.x/configuration/oauth_provider" 23 | }, 24 | { 25 | "type": "doc", 26 | "id": "version-6.1.x/configuration/session_storage" 27 | }, 28 | { 29 | "type": "doc", 30 | "id": "version-6.1.x/configuration/tls" 31 | } 32 | ] 33 | }, 34 | { 35 | "collapsed": false, 36 | "type": "category", 37 | "label": "Features", 38 | "items": [ 39 | { 40 | "type": "doc", 41 | "id": "version-6.1.x/features/endpoints" 42 | }, 43 | { 44 | "type": "doc", 45 | "id": "version-6.1.x/features/request_signatures" 46 | } 47 | ] 48 | }, 49 | { 50 | "collapsed": false, 51 | "type": "category", 52 | "label": "Community", 53 | "items": [ 54 | { 55 | "type": "doc", 56 | "id": "version-6.1.x/community/security" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /docs/versioned_sidebars/version-7.1.x-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-7.1.x/docs": [ 3 | { 4 | "type": "doc", 5 | "id": "version-7.1.x/installation" 6 | }, 7 | { 8 | "type": "doc", 9 | "id": "version-7.1.x/behaviour" 10 | }, 11 | { 12 | "collapsed": false, 13 | "type": "category", 14 | "label": "Configuration", 15 | "items": [ 16 | { 17 | "type": "doc", 18 | "id": "version-7.1.x/configuration/overview" 19 | }, 20 | { 21 | "type": "doc", 22 | "id": "version-7.1.x/configuration/oauth_provider" 23 | }, 24 | { 25 | "type": "doc", 26 | "id": "version-7.1.x/configuration/session_storage" 27 | }, 28 | { 29 | "type": "doc", 30 | "id": "version-7.1.x/configuration/tls" 31 | }, 32 | { 33 | "type": "doc", 34 | "id": "version-7.1.x/configuration/alpha-config" 35 | } 36 | ] 37 | }, 38 | { 39 | "collapsed": false, 40 | "type": "category", 41 | "label": "Features", 42 | "items": [ 43 | { 44 | "type": "doc", 45 | "id": "version-7.1.x/features/endpoints" 46 | } 47 | ] 48 | }, 49 | { 50 | "collapsed": false, 51 | "type": "category", 52 | "label": "Community", 53 | "items": [ 54 | { 55 | "type": "doc", 56 | "id": "version-7.1.x/community/security" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /docs/versioned_sidebars/version-7.2.x-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-7.2.x/docs": [ 3 | { 4 | "type": "doc", 5 | "id": "version-7.2.x/installation" 6 | }, 7 | { 8 | "type": "doc", 9 | "id": "version-7.2.x/behaviour" 10 | }, 11 | { 12 | "collapsed": false, 13 | "type": "category", 14 | "label": "Configuration", 15 | "items": [ 16 | { 17 | "type": "doc", 18 | "id": "version-7.2.x/configuration/overview" 19 | }, 20 | { 21 | "type": "doc", 22 | "id": "version-7.2.x/configuration/oauth_provider" 23 | }, 24 | { 25 | "type": "doc", 26 | "id": "version-7.2.x/configuration/session_storage" 27 | }, 28 | { 29 | "type": "doc", 30 | "id": "version-7.2.x/configuration/tls" 31 | }, 32 | { 33 | "type": "doc", 34 | "id": "version-7.2.x/configuration/alpha-config" 35 | } 36 | ] 37 | }, 38 | { 39 | "collapsed": false, 40 | "type": "category", 41 | "label": "Features", 42 | "items": [ 43 | { 44 | "type": "doc", 45 | "id": "version-7.2.x/features/endpoints" 46 | } 47 | ] 48 | }, 49 | { 50 | "collapsed": false, 51 | "type": "category", 52 | "label": "Community", 53 | "items": [ 54 | { 55 | "type": "doc", 56 | "id": "version-7.2.x/community/security" 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /pkg/apis/middleware/scope_test.go: -------------------------------------------------------------------------------- 1 | package middleware_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Scope Suite", func() { 12 | Context("GetRequestScope", func() { 13 | var request *http.Request 14 | 15 | BeforeEach(func() { 16 | var err error 17 | request, err = http.NewRequest("", "http://127.0.0.1/", nil) 18 | Expect(err).ToNot(HaveOccurred()) 19 | }) 20 | 21 | Context("with a scope", func() { 22 | var scope *middleware.RequestScope 23 | 24 | BeforeEach(func() { 25 | scope = &middleware.RequestScope{} 26 | request = middleware.AddRequestScope(request, scope) 27 | }) 28 | 29 | It("returns the scope", func() { 30 | s := middleware.GetRequestScope(request) 31 | Expect(s).ToNot(BeNil()) 32 | Expect(s).To(Equal(scope)) 33 | }) 34 | 35 | Context("if the scope is then modified", func() { 36 | BeforeEach(func() { 37 | Expect(scope.SaveSession).To(BeFalse()) 38 | scope.SaveSession = true 39 | }) 40 | 41 | It("returns the updated session", func() { 42 | s := middleware.GetRequestScope(request) 43 | Expect(s).ToNot(BeNil()) 44 | Expect(s).To(Equal(scope)) 45 | Expect(s.SaveSession).To(BeTrue()) 46 | }) 47 | }) 48 | }) 49 | 50 | Context("without a scope", func() { 51 | It("returns nil", func() { 52 | Expect(middleware.GetRequestScope(request)).To(BeNil()) 53 | }) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /pkg/upstream/file.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "net/http" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 9 | ) 10 | 11 | const fileScheme = "file" 12 | 13 | // newFileServer creates a new fileServer that can serve requests 14 | // to a file system location. 15 | func newFileServer(id, path, fileSystemPath string) http.Handler { 16 | return &fileServer{ 17 | upstream: id, 18 | handler: newFileServerForPath(path, fileSystemPath), 19 | } 20 | } 21 | 22 | // newFileServerForPath creates a http.Handler to serve files from the filesystem 23 | func newFileServerForPath(path string, filesystemPath string) http.Handler { 24 | // Windows fileSSystemPath will be be prefixed with `/`, eg`/C:/..., 25 | // if they were parsed by url.Parse` 26 | if runtime.GOOS == "windows" { 27 | filesystemPath = strings.TrimPrefix(filesystemPath, "/") 28 | } 29 | 30 | return http.StripPrefix(path, http.FileServer(http.Dir(filesystemPath))) 31 | } 32 | 33 | // fileServer represents a single filesystem upstream proxy 34 | type fileServer struct { 35 | upstream string 36 | handler http.Handler 37 | } 38 | 39 | // ServeHTTP proxies requests to the upstream provider while signing the 40 | // request headers 41 | func (u *fileServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 42 | scope := middleware.GetRequestScope(req) 43 | // If scope is nil, this will panic. 44 | // A scope should always be injected before this handler is called. 45 | scope.Upstream = u.upstream 46 | 47 | u.handler.ServeHTTP(rw, req) 48 | } 49 | -------------------------------------------------------------------------------- /contrib/local-environment/docker-compose-traefik.yaml: -------------------------------------------------------------------------------- 1 | # This docker-compose file can be used to bring up an example instance of oauth2-proxy 2 | # for manual testing and exploration of features. 3 | # Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider, 4 | # HTTPBin as an example upstream. 5 | # 6 | # This can either be created using docker-compose 7 | # docker-compose -f docker-compose-traefik.yaml 8 | # Or: 9 | # make traefik- (eg. make traefik-up, make traefik-down) 10 | # 11 | # Access one of the following URLs to initiate a login flow: 12 | # - http://oauth2-proxy.localhost 13 | # - http://httpbin.oauth2-proxy.localhost 14 | # 15 | # The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost 16 | # 17 | # Note, the above URLs should work with Chrome, but you may need to add hosts 18 | # entries for other browsers 19 | # 127.0.0.1 oauth2-proxy.localhost 20 | # 127.0.0.1 httpbin.oauth2-proxy.localhost 21 | # 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost 22 | version: '3.0' 23 | services: 24 | 25 | oauth2-proxy: 26 | ports: [] 27 | hostname: oauth2-proxy 28 | volumes: 29 | - "./oauth2-proxy-traefik.cfg:/oauth2-proxy.cfg" 30 | networks: 31 | oauth2-proxy: 32 | 33 | # Reverse proxy 34 | gateway: 35 | container_name: traefik 36 | image: traefik:2.4.2 37 | volumes: 38 | - "./traefik:/etc/traefik" 39 | ports: 40 | - "80:80" 41 | - "9090:8080" 42 | depends_on: 43 | - oauth2-proxy 44 | networks: 45 | oauth2-proxy: 46 | httpbin: 47 | 48 | networks: 49 | oauth2-proxy: 50 | -------------------------------------------------------------------------------- /docs/versioned_sidebars/version-7.0.x-sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "version-7.0.x/docs": [ 3 | { 4 | "type": "doc", 5 | "id": "version-7.0.x/installation" 6 | }, 7 | { 8 | "type": "doc", 9 | "id": "version-7.0.x/behaviour" 10 | }, 11 | { 12 | "collapsed": false, 13 | "type": "category", 14 | "label": "Configuration", 15 | "items": [ 16 | { 17 | "type": "doc", 18 | "id": "version-7.0.x/configuration/overview" 19 | }, 20 | { 21 | "type": "doc", 22 | "id": "version-7.0.x/configuration/oauth_provider" 23 | }, 24 | { 25 | "type": "doc", 26 | "id": "version-7.0.x/configuration/session_storage" 27 | }, 28 | { 29 | "type": "doc", 30 | "id": "version-7.0.x/configuration/tls" 31 | }, 32 | { 33 | "type": "doc", 34 | "id": "version-7.0.x/configuration/alpha-config" 35 | } 36 | ] 37 | }, 38 | { 39 | "collapsed": false, 40 | "type": "category", 41 | "label": "Features", 42 | "items": [ 43 | { 44 | "type": "doc", 45 | "id": "version-7.0.x/features/endpoints" 46 | }, 47 | { 48 | "type": "doc", 49 | "id": "version-7.0.x/features/request_signatures" 50 | } 51 | ] 52 | }, 53 | { 54 | "collapsed": false, 55 | "type": "category", 56 | "label": "Community", 57 | "items": [ 58 | { 59 | "type": "doc", 60 | "id": "version-7.0.x/community/security" 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Here's how OAuth2 Proxy releases are created. 4 | 5 | ## Schedule 6 | 7 | Our aim is to release once a quarter, but bug fixes will be prioritised and might be released earlier. 8 | 9 | ## The Process 10 | 11 | Note this uses `v4.1.0` as an example release number. 12 | 13 | 1. Create a draft Github release 14 | * Use format `v4.1.0` for both the tag and title 15 | 2. Update [CHANGELOG.md](CHANGELOG.md) 16 | * Write the release highlights 17 | * Copy in headings ready for the next release 18 | 3. Create release commit 19 | ``` 20 | git checkout -b release-v4.1.0 21 | ``` 22 | 4. Create pull request getting other maintainers to review 23 | 5. Copy the release notes in to the draft Github release, adding a link to [CHANGELOG.md](CHANGELOG.md) 24 | 6. Update you local master branch 25 | ``` 26 | git checkout master 27 | git pull 28 | ``` 29 | 7. Create & push the tag 30 | ``` 31 | git tag v4.1.0 32 | git push --tags 33 | ``` 34 | 8. Make the release artefacts 35 | ``` 36 | make release 37 | ``` 38 | 9. Upload all the files (not the folders) from the `/release` folder to Github release as binary artefacts. There should be both the tarballs (`tar.gz`) and the checksum files (`sha256sum.txt`). 39 | 10. Publish release in Github 40 | 11. Make and push docker images to Quay 41 | ``` 42 | make docker-all 43 | make docker-push-all 44 | ``` 45 | Note: Ensure the docker tags don't include `-dirty`. This means you have uncommitted changes. 46 | 47 | 12. Verify everything looks good at [quay](https://quay.io/repository/oauth2-proxy/oauth2-proxy?tag=latest&tab=tags) and [github](https://github.com/oauth2-proxy/oauth2-proxy/releases) 48 | -------------------------------------------------------------------------------- /contrib/local-environment/docker-compose-nginx.yaml: -------------------------------------------------------------------------------- 1 | # This docker-compose file can be used to bring up an example instance of oauth2-proxy 2 | # for manual testing and exploration of features. 3 | # Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider, 4 | # etcd for storage for Dex, nginx as a reverse proxy and other http services for upstreams 5 | # 6 | # This file is an extension of the main compose file and must be used with it 7 | # docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml 8 | # Alternatively: 9 | # make nginx- (eg make nginx-up, make nginx-down) 10 | # 11 | # Access one of the following URLs to initiate a login flow: 12 | # - http://oauth2-proxy.localhost 13 | # - http://httpbin.oauth2-proxy.localhost 14 | # 15 | # The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost 16 | # 17 | # Note, the above URLs should work with Chrome, but you may need to add hosts 18 | # entries for other browsers 19 | # 127.0.0.1 oauth2-proxy.localhost 20 | # 127.0.0.1 httpbin.oauth2-proxy.localhost 21 | # 127.0.0.1 oauth2-proxy.oauth2-proxy.localhost 22 | version: '3.0' 23 | services: 24 | oauth2-proxy: 25 | ports: [] 26 | hostname: oauth2-proxy 27 | volumes: 28 | - "./oauth2-proxy-nginx.cfg:/oauth2-proxy.cfg" 29 | networks: 30 | oauth2-proxy: {} 31 | nginx: 32 | container_name: nginx 33 | image: nginx:1.18 34 | ports: 35 | - 80:80/tcp 36 | hostname: nginx 37 | volumes: 38 | - "./nginx.conf:/etc/nginx/conf.d/default.conf" 39 | networks: 40 | oauth2-proxy: {} 41 | httpbin: {} 42 | networks: 43 | oauth2-proxy: {} 44 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | # - $default-branch 8 | pull_request: 9 | branches: 10 | - '**' 11 | # - $default-branch 12 | 13 | jobs: 14 | build: 15 | env: 16 | COVER: true 17 | runs-on: ubuntu-20.04 18 | steps: 19 | 20 | - name: Check out code 21 | uses: actions/checkout@v2 22 | 23 | - name: Set up Go 1.17 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: 1.17.x 27 | id: go 28 | 29 | - name: Get dependencies 30 | run: | 31 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.36.0 32 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 33 | chmod +x ./cc-test-reporter 34 | 35 | - name: Verify Code Generation 36 | run: | 37 | make verify-generate 38 | 39 | - name: Lint 40 | run: | 41 | make lint 42 | 43 | - name: Build 44 | run: | 45 | make build 46 | 47 | - name: Test 48 | env: 49 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 50 | run: | 51 | ./.github/workflows/test.sh 52 | 53 | docker: 54 | runs-on: ubuntu-20.04 55 | steps: 56 | 57 | - name: Check out code 58 | uses: actions/checkout@v2 59 | 60 | - name: Set up Docker Buildx 61 | id: buildx 62 | uses: crazy-max/ghaction-docker-buildx@v3 63 | with: 64 | buildx-version: latest 65 | qemu-version: latest 66 | 67 | - name: Docker Build 68 | run: | 69 | make docker 70 | -------------------------------------------------------------------------------- /pkg/requests/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | 6 | middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 7 | ) 8 | 9 | const ( 10 | XForwardedProto = "X-Forwarded-Proto" 11 | XForwardedHost = "X-Forwarded-Host" 12 | XForwardedURI = "X-Forwarded-Uri" 13 | ) 14 | 15 | // GetRequestProto returns the request scheme or X-Forwarded-Proto if present 16 | // and the request is proxied. 17 | func GetRequestProto(req *http.Request) string { 18 | proto := req.Header.Get(XForwardedProto) 19 | if !IsProxied(req) || proto == "" { 20 | proto = req.URL.Scheme 21 | } 22 | return proto 23 | } 24 | 25 | // GetRequestHost returns the request host header or X-Forwarded-Host if 26 | // present and the request is proxied. 27 | func GetRequestHost(req *http.Request) string { 28 | host := req.Header.Get(XForwardedHost) 29 | if !IsProxied(req) || host == "" { 30 | host = req.Host 31 | } 32 | return host 33 | } 34 | 35 | // GetRequestURI return the request URI or X-Forwarded-Uri if present and the 36 | // request is proxied. 37 | func GetRequestURI(req *http.Request) string { 38 | uri := req.Header.Get(XForwardedURI) 39 | if !IsProxied(req) || uri == "" { 40 | // Use RequestURI to preserve ?query 41 | uri = req.URL.RequestURI() 42 | } 43 | return uri 44 | } 45 | 46 | // IsProxied determines if a request was from a proxy based on the RequestScope 47 | // ReverseProxy tracker. 48 | func IsProxied(req *http.Request) bool { 49 | scope := middlewareapi.GetRequestScope(req) 50 | if scope == nil { 51 | return false 52 | } 53 | return scope.ReverseProxy 54 | } 55 | 56 | func IsForwardedRequest(req *http.Request) bool { 57 | return IsProxied(req) && 58 | req.Host != GetRequestHost(req) 59 | } 60 | -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'OAuth2 Proxy', 3 | tagline: 'A lightweight authentication proxy written in Go', 4 | url: 'https://oauth2-proxy.github.io', 5 | baseUrl: '/oauth2-proxy/', 6 | onBrokenLinks: 'throw', 7 | favicon: 'img/logos/OAuth2_Proxy_icon.svg', 8 | organizationName: 'oauth2-proxy', // Usually your GitHub org/user name. 9 | projectName: 'oauth2-proxy', // Usually your repo name. 10 | themeConfig: { 11 | navbar: { 12 | title: 'OAuth2 Proxy', 13 | logo: { 14 | alt: 'OAuth2 Proxy', 15 | src: 'img/logos/OAuth2_Proxy_icon.svg', 16 | }, 17 | items: [ 18 | { 19 | to: 'docs/', 20 | activeBasePath: 'docs', 21 | label: 'Docs', 22 | position: 'left', 23 | }, 24 | { 25 | type: 'docsVersionDropdown', 26 | position: 'right', 27 | dropdownActiveClassDisabled: true, 28 | }, 29 | { 30 | href: 'https://github.com/oauth2-proxy/oauth2-proxy', 31 | label: 'GitHub', 32 | position: 'right', 33 | }, 34 | ], 35 | }, 36 | footer: { 37 | style: 'dark', 38 | copyright: `Copyright © ${new Date().getFullYear()} OAuth2 Proxy.`, 39 | }, 40 | }, 41 | presets: [ 42 | [ 43 | '@docusaurus/preset-classic', 44 | { 45 | docs: { 46 | sidebarPath: require.resolve('./sidebars.js'), 47 | // Please change this to your repo. 48 | editUrl: 49 | 'https://github.com/oauth2-proxy/oauth2-proxy/edit/master/docs/', 50 | }, 51 | theme: { 52 | customCss: require.resolve('./src/css/custom.css'), 53 | }, 54 | }, 55 | ], 56 | ], 57 | }; 58 | -------------------------------------------------------------------------------- /pkg/apis/options/header.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | // Header represents an individual header that will be added to a request or 4 | // response header. 5 | type Header struct { 6 | // Name is the header name to be used for this set of values. 7 | // Names should be unique within a list of Headers. 8 | Name string `json:"name,omitempty"` 9 | 10 | // PreserveRequestValue determines whether any values for this header 11 | // should be preserved for the request to the upstream server. 12 | // This option only applies to injected request headers. 13 | // Defaults to false (headers that match this header will be stripped). 14 | PreserveRequestValue bool `json:"preserveRequestValue,omitempty"` 15 | 16 | // Values contains the desired values for this header 17 | Values []HeaderValue `json:"values,omitempty"` 18 | } 19 | 20 | // HeaderValue represents a single header value and the sources that can 21 | // make up the header value 22 | type HeaderValue struct { 23 | // Allow users to load the value from a secret source 24 | *SecretSource `json:",omitempty"` 25 | 26 | // Allow users to load the value from a session claim 27 | *ClaimSource `json:",omitempty"` 28 | } 29 | 30 | // ClaimSource allows loading a header value from a claim within the session 31 | type ClaimSource struct { 32 | // Claim is the name of the claim in the session that the value should be 33 | // loaded from. 34 | Claim string `json:"claim,omitempty"` 35 | 36 | // Prefix is an optional prefix that will be prepended to the value of the 37 | // claim if it is non-empty. 38 | Prefix string `json:"prefix,omitempty"` 39 | 40 | // BasicAuthPassword converts this claim into a basic auth header. 41 | // Note the value of claim will become the basic auth username and the 42 | // basicAuthPassword will be used as the password value. 43 | BasicAuthPassword *SecretSource `json:"basicAuthPassword,omitempty"` 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 15 * * 2' 11 | 12 | jobs: 13 | CodeQL-Build: 14 | 15 | strategy: 16 | fail-fast: false 17 | 18 | # CodeQL runs on ubuntu-latest and windows-latest 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v2 24 | with: 25 | # We must fetch at least the immediate parents so that if this is 26 | # a pull request then we can checkout the head. 27 | fetch-depth: 2 28 | 29 | # If this run was triggered by a pull request event, then checkout 30 | # the head of the pull request instead of the merge commit. 31 | - run: git checkout HEAD^2 32 | if: ${{ github.event_name == 'pull_request' }} 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v1 37 | with: 38 | languages: go 39 | 40 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 41 | # If this step fails, then you should remove it and run the build manually (see below) 42 | - name: Autobuild 43 | uses: github/codeql-action/autobuild@v1 44 | 45 | # ℹ️ Command-line programs to run using the OS shell. 46 | # 📚 https://git.io/JvXDl 47 | 48 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 49 | # and modify them (or add more) to build your code if your project 50 | # uses a compiled language 51 | 52 | #- run: | 53 | # make bootstrap 54 | # make release 55 | 56 | - name: Perform CodeQL Analysis 57 | uses: github/codeql-action/analyze@v1 58 | -------------------------------------------------------------------------------- /contrib/local-environment/traefik/dynamic.yaml: -------------------------------------------------------------------------------- 1 | http: 2 | routers: 3 | oauth2-proxy-route: 4 | rule: "Host(`oauth2-proxy.oauth2-proxy.localhost`)" 5 | middlewares: 6 | - auth-headers 7 | service: oauth-backend 8 | httpbin-route: 9 | rule: "Host(`httpbin.oauth2-proxy.localhost`)" 10 | service: httpbin-service 11 | middlewares: 12 | - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin 13 | httpbin-route-2: 14 | rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/no-auto-redirect`)" 15 | service: httpbin-service 16 | middlewares: 17 | - oauth-auth-wo-redirect # unauthenticated session will return a 401 18 | services-oauth2-route: 19 | rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/oauth2/`)" 20 | middlewares: 21 | - auth-headers 22 | service: oauth-backend 23 | 24 | services: 25 | httpbin-service: 26 | loadBalancer: 27 | servers: 28 | - url: http://httpbin 29 | oauth-backend: 30 | loadBalancer: 31 | servers: 32 | - url: http://oauth2-proxy:4180 33 | 34 | middlewares: 35 | auth-headers: 36 | headers: 37 | stsSeconds: 315360000 38 | browserXssFilter: true 39 | contentTypeNosniff: true 40 | forceSTSHeader: true 41 | stsIncludeSubdomains: true 42 | stsPreload: true 43 | frameDeny: true 44 | oauth-auth-redirect: 45 | forwardAuth: 46 | address: http://oauth2-proxy:4180 47 | trustForwardHeader: true 48 | authResponseHeaders: 49 | - X-Auth-Request-Access-Token 50 | - Authorization 51 | oauth-auth-wo-redirect: 52 | forwardAuth: 53 | address: http://oauth2-proxy:4180/oauth2/auth 54 | trustForwardHeader: true 55 | authResponseHeaders: 56 | - X-Auth-Request-Access-Token 57 | - Authorization 58 | -------------------------------------------------------------------------------- /contrib/local-environment/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # This docker-compose file can be used to bring up an example instance of oauth2-proxy 2 | # for manual testing and exploration of features. 3 | # Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider, 4 | # etcd for storage for Dex and HTTPBin as an example upstream. 5 | # 6 | # This can either be created using docker-compose 7 | # docker-compose -f docker-compose.yaml 8 | # Or: 9 | # make (eg. make up, make down) 10 | # 11 | # Access http://localhost:4180 to initiate a login cycle 12 | version: '3.0' 13 | services: 14 | oauth2-proxy: 15 | container_name: oauth2-proxy 16 | image: quay.io/oauth2-proxy/oauth2-proxy:v7.3.0 17 | command: --config /oauth2-proxy.cfg 18 | ports: 19 | - 4180:4180/tcp 20 | hostname: oauth2-proxy 21 | volumes: 22 | - "./oauth2-proxy.cfg:/oauth2-proxy.cfg" 23 | restart: unless-stopped 24 | networks: 25 | dex: {} 26 | httpbin: {} 27 | depends_on: 28 | - dex 29 | - httpbin 30 | dex: 31 | container_name: dex 32 | image: ghcr.io/dexidp/dex:v2.30.3 33 | command: dex serve /dex.yaml 34 | ports: 35 | - 4190:4190/tcp 36 | hostname: dex 37 | volumes: 38 | - "./dex.yaml:/dex.yaml" 39 | restart: unless-stopped 40 | networks: 41 | dex: 42 | aliases: 43 | - dex.localhost 44 | etcd: {} 45 | depends_on: 46 | - etcd 47 | httpbin: 48 | container_name: httpbin 49 | image: kennethreitz/httpbin 50 | ports: 51 | - 8080:80/tcp 52 | networks: 53 | httpbin: {} 54 | etcd: 55 | container_name: etcd 56 | image: gcr.io/etcd-development/etcd:v3.4.7 57 | entrypoint: /usr/local/bin/etcd 58 | command: 59 | - --listen-client-urls=http://0.0.0.0:2379 60 | - --advertise-client-urls=http://etcd:2379 61 | networks: 62 | etcd: {} 63 | networks: 64 | dex: {} 65 | etcd: {} 66 | httpbin: {} 67 | -------------------------------------------------------------------------------- /pkg/upstream/file_test.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | 10 | middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/ginkgo/extensions/table" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("File Server Suite", func() { 17 | var dir string 18 | var handler http.Handler 19 | var id string 20 | 21 | const ( 22 | foo = "foo" 23 | bar = "bar" 24 | baz = "baz" 25 | pageNotFound = "404 page not found\n" 26 | ) 27 | 28 | BeforeEach(func() { 29 | // Generate a random id before each test to check the GAP-Upstream-Address 30 | // is being set correctly 31 | idBytes := make([]byte, 16) 32 | _, err := io.ReadFull(rand.Reader, idBytes) 33 | Expect(err).ToNot(HaveOccurred()) 34 | id = string(idBytes) 35 | 36 | handler = newFileServer(id, "/files", filesDir) 37 | }) 38 | 39 | AfterEach(func() { 40 | Expect(os.RemoveAll(dir)).To(Succeed()) 41 | }) 42 | 43 | DescribeTable("fileServer ServeHTTP", 44 | func(requestPath string, expectedResponseCode int, expectedBody string) { 45 | req := httptest.NewRequest("", requestPath, nil) 46 | req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{}) 47 | 48 | rw := httptest.NewRecorder() 49 | handler.ServeHTTP(rw, req) 50 | 51 | scope := middlewareapi.GetRequestScope(req) 52 | Expect(scope.Upstream).To(Equal(id)) 53 | 54 | Expect(rw.Code).To(Equal(expectedResponseCode)) 55 | Expect(rw.Body.String()).To(Equal(expectedBody)) 56 | }, 57 | Entry("for file foo", "/files/foo", 200, foo), 58 | Entry("for file bar", "/files/bar", 200, bar), 59 | Entry("for file foo/baz", "/files/subdir/baz", 200, baz), 60 | Entry("for a non-existent file inside the path", "/files/baz", 404, pageNotFound), 61 | Entry("for a non-existent file oustide the path", "/baz", 404, pageNotFound), 62 | ) 63 | }) 64 | -------------------------------------------------------------------------------- /pkg/validation/header.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 7 | ) 8 | 9 | func validateHeaders(headers []options.Header) []string { 10 | msgs := []string{} 11 | names := make(map[string]struct{}) 12 | 13 | for _, header := range headers { 14 | msgs = append(msgs, validateHeader(header, names)...) 15 | } 16 | return msgs 17 | } 18 | 19 | func validateHeader(header options.Header, names map[string]struct{}) []string { 20 | msgs := []string{} 21 | 22 | if header.Name == "" { 23 | msgs = append(msgs, "header has empty name: names are required for all headers") 24 | } 25 | 26 | if _, ok := names[header.Name]; ok { 27 | msgs = append(msgs, fmt.Sprintf("multiple headers found with name %q: header names must be unique", header.Name)) 28 | } 29 | names[header.Name] = struct{}{} 30 | 31 | for _, value := range header.Values { 32 | msgs = append(msgs, 33 | prefixValues(fmt.Sprintf("invalid header %q: invalid values: ", header.Name), 34 | validateHeaderValue(header.Name, value)..., 35 | )..., 36 | ) 37 | } 38 | return msgs 39 | } 40 | 41 | func validateHeaderValue(name string, value options.HeaderValue) []string { 42 | switch { 43 | case value.SecretSource != nil && value.ClaimSource == nil: 44 | return []string{validateSecretSource(*value.SecretSource)} 45 | case value.SecretSource == nil && value.ClaimSource != nil: 46 | return validateHeaderValueClaimSource(*value.ClaimSource) 47 | default: 48 | return []string{"header value has multiple entries: only one entry per value is allowed"} 49 | } 50 | } 51 | 52 | func validateHeaderValueClaimSource(claim options.ClaimSource) []string { 53 | msgs := []string{} 54 | 55 | if claim.Claim == "" { 56 | msgs = append(msgs, "claim should not be empty") 57 | } 58 | 59 | if claim.BasicAuthPassword != nil { 60 | msgs = append(msgs, prefixValues("invalid basicAuthPassword: ", validateSecretSource(*claim.BasicAuthPassword))...) 61 | } 62 | return msgs 63 | } 64 | -------------------------------------------------------------------------------- /pkg/apis/middleware/scope.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 8 | ) 9 | 10 | type scopeKey string 11 | 12 | // RequestScopeKey uses a typed string to reduce likelihood of clashing 13 | // with other context keys 14 | const RequestScopeKey scopeKey = "request-scope" 15 | 16 | // RequestScope contains information regarding the request that is being made. 17 | // The RequestScope is used to pass information between different middlewares 18 | // within the chain. 19 | type RequestScope struct { 20 | // ReverseProxy tracks whether OAuth2-Proxy is operating in reverse proxy 21 | // mode and if request `X-Forwarded-*` headers should be trusted 22 | ReverseProxy bool 23 | 24 | // RequestID is set to the request's `X-Request-Id` header if set. 25 | // Otherwise a random UUID is set. 26 | RequestID string 27 | 28 | // Session details the authenticated users information (if it exists). 29 | Session *sessions.SessionState 30 | 31 | // SaveSession indicates whether the session storage should attempt to save 32 | // the session or not. 33 | SaveSession bool 34 | 35 | // ClearSession indicates whether the user should be logged out or not. 36 | ClearSession bool 37 | 38 | // SessionRevalidated indicates whether the session has been revalidated since 39 | // it was loaded or not. 40 | SessionRevalidated bool 41 | 42 | // Upstream tracks which upstream was used for this request 43 | Upstream string 44 | } 45 | 46 | // GetRequestScope returns the current request scope from the given request 47 | func GetRequestScope(req *http.Request) *RequestScope { 48 | scope := req.Context().Value(RequestScopeKey) 49 | if scope == nil { 50 | return nil 51 | } 52 | 53 | return scope.(*RequestScope) 54 | } 55 | 56 | // AddRequestScope adds a RequestScope to a request 57 | func AddRequestScope(req *http.Request, scope *RequestScope) *http.Request { 58 | ctx := context.WithValue(req.Context(), RequestScopeKey, scope) 59 | return req.WithContext(ctx) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/sessions/tests/mock_store.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 9 | ) 10 | 11 | // entry is a MockStore cache entry with an expiration 12 | type entry struct { 13 | data []byte 14 | expiration time.Duration 15 | } 16 | 17 | // MockStore is a generic in-memory implementation of persistence.Store 18 | // for mocking in tests 19 | type MockStore struct { 20 | cache map[string]entry 21 | lockCache map[string]*MockLock 22 | elapsed time.Duration 23 | } 24 | 25 | // NewMockStore creates a MockStore 26 | func NewMockStore() *MockStore { 27 | return &MockStore{ 28 | cache: map[string]entry{}, 29 | lockCache: map[string]*MockLock{}, 30 | elapsed: 0 * time.Second, 31 | } 32 | } 33 | 34 | // Save sets a key to the data to the memory cache 35 | func (s *MockStore) Save(_ context.Context, key string, value []byte, exp time.Duration) error { 36 | s.cache[key] = entry{ 37 | data: value, 38 | expiration: exp, 39 | } 40 | return nil 41 | } 42 | 43 | // Load gets data from the memory cache via a key 44 | func (s *MockStore) Load(_ context.Context, key string) ([]byte, error) { 45 | entry, ok := s.cache[key] 46 | if !ok || entry.expiration <= s.elapsed { 47 | delete(s.cache, key) 48 | return nil, fmt.Errorf("key not found: %s", key) 49 | } 50 | return entry.data, nil 51 | } 52 | 53 | // Clear deletes an entry from the memory cache 54 | func (s *MockStore) Clear(_ context.Context, key string) error { 55 | delete(s.cache, key) 56 | return nil 57 | } 58 | 59 | func (s *MockStore) Lock(key string) sessions.Lock { 60 | if s.lockCache[key] != nil { 61 | return s.lockCache[key] 62 | } 63 | lock := &MockLock{} 64 | s.lockCache[key] = lock 65 | return lock 66 | } 67 | 68 | // FastForward simulates the flow of time to test expirations 69 | func (s *MockStore) FastForward(duration time.Duration) { 70 | for _, mockLock := range s.lockCache { 71 | mockLock.FastForward(duration) 72 | } 73 | s.elapsed += duration 74 | } 75 | -------------------------------------------------------------------------------- /pkg/validation/cookie.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "sort" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 9 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption" 10 | ) 11 | 12 | func validateCookie(o options.Cookie) []string { 13 | msgs := validateCookieSecret(o.Secret) 14 | 15 | if o.Refresh >= o.Expire { 16 | msgs = append(msgs, fmt.Sprintf( 17 | "cookie_refresh (%q) must be less than cookie_expire (%q)", 18 | o.Refresh.String(), 19 | o.Expire.String())) 20 | } 21 | 22 | switch o.SameSite { 23 | case "", "none", "lax", "strict": 24 | default: 25 | msgs = append(msgs, fmt.Sprintf("cookie_samesite (%q) must be one of ['', 'lax', 'strict', 'none']", o.SameSite)) 26 | } 27 | 28 | // Sort cookie domains by length, so that we try longer (and more specific) domains first 29 | sort.Slice(o.Domains, func(i, j int) bool { 30 | return len(o.Domains[i]) > len(o.Domains[j]) 31 | }) 32 | 33 | msgs = append(msgs, validateCookieName(o.Name)...) 34 | return msgs 35 | } 36 | 37 | func validateCookieName(name string) []string { 38 | msgs := []string{} 39 | 40 | cookie := &http.Cookie{Name: name} 41 | if cookie.String() == "" { 42 | msgs = append(msgs, fmt.Sprintf("invalid cookie name: %q", name)) 43 | } 44 | 45 | if len(name) > 256 { 46 | msgs = append(msgs, fmt.Sprintf("cookie name should be under 256 characters: cookie name is %d characters", len(name))) 47 | } 48 | return msgs 49 | } 50 | 51 | func validateCookieSecret(secret string) []string { 52 | if secret == "" { 53 | return []string{"missing setting: cookie-secret"} 54 | } 55 | 56 | secretBytes := encryption.SecretBytes(secret) 57 | // Check if the secret is a valid length 58 | switch len(secretBytes) { 59 | case 16, 24, 32: 60 | // Valid secret size found 61 | return []string{} 62 | } 63 | // Invalid secret size found, return a message 64 | return []string{fmt.Sprintf( 65 | "cookie_secret must be 16, 24, or 32 bytes to create an AES cipher, but is %d bytes", 66 | len(secretBytes)), 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/validation/logging.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | ) 10 | 11 | // configureLogger is responsible for configuring the logger based on the options given 12 | func configureLogger(o options.Logging, msgs []string) []string { 13 | // Setup the log file 14 | if len(o.File.Filename) > 0 { 15 | // Validate that the file/dir can be written 16 | file, err := os.OpenFile(o.File.Filename, os.O_WRONLY|os.O_CREATE, 0600) 17 | if err != nil { 18 | if os.IsPermission(err) { 19 | return append(msgs, "unable to write to log file: "+o.File.Filename) 20 | } 21 | } 22 | err = file.Close() 23 | if err != nil { 24 | return append(msgs, "error closing the log file: "+o.File.Filename) 25 | } 26 | 27 | logger.Printf("Redirecting logging to file: %s", o.File.Filename) 28 | 29 | logWriter := &lumberjack.Logger{ 30 | Filename: o.File.Filename, 31 | MaxSize: o.File.MaxSize, // megabytes 32 | MaxAge: o.File.MaxAge, // days 33 | MaxBackups: o.File.MaxBackups, 34 | LocalTime: o.LocalTime, 35 | Compress: o.File.Compress, 36 | } 37 | 38 | logger.SetOutput(logWriter) 39 | } 40 | 41 | // Supply a sanity warning to the logger if all logging is disabled 42 | if !o.StandardEnabled && !o.AuthEnabled && !o.RequestEnabled { 43 | logger.Error("Warning: Logging disabled. No further logs will be shown.") 44 | } 45 | 46 | // Pass configuration values to the standard logger 47 | logger.SetStandardEnabled(o.StandardEnabled) 48 | logger.SetErrToInfo(o.ErrToInfo) 49 | logger.SetAuthEnabled(o.AuthEnabled) 50 | logger.SetReqEnabled(o.RequestEnabled) 51 | logger.SetStandardTemplate(o.StandardFormat) 52 | logger.SetAuthTemplate(o.AuthFormat) 53 | logger.SetReqTemplate(o.RequestFormat) 54 | 55 | logger.SetExcludePaths(o.ExcludePaths) 56 | 57 | if !o.LocalTime { 58 | logger.SetFlags(logger.Flags() | logger.LUTC) 59 | } 60 | 61 | return msgs 62 | } 63 | -------------------------------------------------------------------------------- /pkg/sessions/redis/client.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-redis/redis/v8" 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 9 | ) 10 | 11 | // Client is wrapper interface for redis.Client and redis.ClusterClient. 12 | type Client interface { 13 | Get(ctx context.Context, key string) ([]byte, error) 14 | Lock(key string) sessions.Lock 15 | Set(ctx context.Context, key string, value []byte, expiration time.Duration) error 16 | Del(ctx context.Context, key string) error 17 | } 18 | 19 | var _ Client = (*client)(nil) 20 | 21 | type client struct { 22 | *redis.Client 23 | } 24 | 25 | func newClient(c *redis.Client) Client { 26 | return &client{ 27 | Client: c, 28 | } 29 | } 30 | 31 | func (c *client) Get(ctx context.Context, key string) ([]byte, error) { 32 | return c.Client.Get(ctx, key).Bytes() 33 | } 34 | 35 | func (c *client) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error { 36 | return c.Client.Set(ctx, key, value, expiration).Err() 37 | } 38 | 39 | func (c *client) Del(ctx context.Context, key string) error { 40 | return c.Client.Del(ctx, key).Err() 41 | } 42 | 43 | func (c *client) Lock(key string) sessions.Lock { 44 | return NewLock(c.Client, key) 45 | } 46 | 47 | var _ Client = (*clusterClient)(nil) 48 | 49 | type clusterClient struct { 50 | *redis.ClusterClient 51 | } 52 | 53 | func newClusterClient(c *redis.ClusterClient) Client { 54 | return &clusterClient{ 55 | ClusterClient: c, 56 | } 57 | } 58 | 59 | func (c *clusterClient) Get(ctx context.Context, key string) ([]byte, error) { 60 | return c.ClusterClient.Get(ctx, key).Bytes() 61 | } 62 | 63 | func (c *clusterClient) Set(ctx context.Context, key string, value []byte, expiration time.Duration) error { 64 | return c.ClusterClient.Set(ctx, key, value, expiration).Err() 65 | } 66 | 67 | func (c *clusterClient) Del(ctx context.Context, key string) error { 68 | return c.ClusterClient.Del(ctx, key).Err() 69 | } 70 | 71 | func (c *clusterClient) Lock(key string) sessions.Lock { 72 | return NewLock(c.ClusterClient, key) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/middleware/redirect_to_https.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/justinas/alice" 10 | requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util" 11 | ) 12 | 13 | const httpsScheme = "https" 14 | 15 | // NewRedirectToHTTPS creates a new redirectToHTTPS middleware that will redirect 16 | // HTTP requests to HTTPS 17 | func NewRedirectToHTTPS(httpsPort string) alice.Constructor { 18 | return func(next http.Handler) http.Handler { 19 | return redirectToHTTPS(httpsPort, next) 20 | } 21 | } 22 | 23 | // redirectToHTTPS is an HTTP middleware the will redirect a request to HTTPS 24 | // if it is not already HTTPS. 25 | // If the request is to a non standard port, the redirection request will be 26 | // to the port from the httpsAddress given. 27 | func redirectToHTTPS(httpsPort string, next http.Handler) http.Handler { 28 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 29 | proto := requestutil.GetRequestProto(req) 30 | if strings.EqualFold(proto, httpsScheme) || (req.TLS != nil && proto == req.URL.Scheme) { 31 | // Only care about the connection to us being HTTPS if the proto wasn't 32 | // from a trusted `X-Forwarded-Proto` (proto == req.URL.Scheme). 33 | // Otherwise the proto is source of truth 34 | next.ServeHTTP(rw, req) 35 | return 36 | } 37 | 38 | // Copy the request URL 39 | targetURL, _ := url.Parse(req.URL.String()) 40 | // Set the scheme to HTTPS 41 | targetURL.Scheme = httpsScheme 42 | 43 | // Set the Host in case the targetURL still does not have one 44 | // or it isn't X-Forwarded-Host aware 45 | targetURL.Host = requestutil.GetRequestHost(req) 46 | 47 | // Overwrite the port if the original request was to a non-standard port 48 | if targetURL.Port() != "" { 49 | // If Port was not empty, this should be fine to ignore the error 50 | host, _, _ := net.SplitHostPort(targetURL.Host) 51 | targetURL.Host = net.JoinHostPort(host, httpsPort) 52 | } 53 | 54 | http.Redirect(rw, req, targetURL.String(), http.StatusPermanentRedirect) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/apis/middleware/session.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/coreos/go-oidc/v3/oidc" 8 | sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 9 | ) 10 | 11 | // TokenToSessionFunc takes a raw ID Token and converts it into a SessionState. 12 | type TokenToSessionFunc func(ctx context.Context, token string) (*sessionsapi.SessionState, error) 13 | 14 | // VerifyFunc takes a raw bearer token and verifies it returning the converted 15 | // oidc.IDToken representation of the token. 16 | type VerifyFunc func(ctx context.Context, token string) (*oidc.IDToken, error) 17 | 18 | // CreateTokenToSessionFunc provides a handler that is a default implementation 19 | // for converting a JWT into a session. 20 | func CreateTokenToSessionFunc(verify VerifyFunc) TokenToSessionFunc { 21 | return func(ctx context.Context, token string) (*sessionsapi.SessionState, error) { 22 | var claims struct { 23 | Subject string `json:"sub"` 24 | Email string `json:"email"` 25 | Verified *bool `json:"email_verified"` 26 | PreferredUsername string `json:"preferred_username"` 27 | Groups []string `json:"groups"` 28 | } 29 | 30 | idToken, err := verify(ctx, token) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if err := idToken.Claims(&claims); err != nil { 36 | return nil, fmt.Errorf("failed to parse bearer token claims: %v", err) 37 | } 38 | 39 | if claims.Email == "" { 40 | claims.Email = claims.Subject 41 | } 42 | 43 | if claims.Verified != nil && !*claims.Verified { 44 | return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email) 45 | } 46 | 47 | newSession := &sessionsapi.SessionState{ 48 | Email: claims.Email, 49 | User: claims.Subject, 50 | Groups: claims.Groups, 51 | PreferredUsername: claims.PreferredUsername, 52 | AccessToken: token, 53 | IDToken: token, 54 | RefreshToken: "", 55 | ExpiresOn: &idToken.Expiry, 56 | } 57 | 58 | return newSession, nil 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /contrib/oauth2-proxy_autocomplete.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Autocompletion for oauth2-proxy 3 | # 4 | # To install this, copy/move this file to /etc/bash.completion.d/ 5 | # or add a line to your ~/.bashrc | ~/.bash_profile that says ". /path/to/oauth2-proxy/contrib/oauth2-proxy_autocomplete.sh" 6 | # 7 | 8 | _oauth2_proxy() { 9 | _oauth2_proxy_commands=$(oauth2-proxy -h 2>&1 | sed -n '/^\s*--/s/ \+/ /gp' | awk '{print $1}' | tr '\n' ' ') 10 | local cur prev 11 | COMPREPLY=() 12 | cur="${COMP_WORDS[COMP_CWORD]}" 13 | prev="${COMP_WORDS[COMP_CWORD-1]}" 14 | case "$prev" in 15 | --@(config|tls-cert-file|tls-key-file|authenticated-emails-file|htpasswd-file|custom-templates-dir|logging-filename|jwt-key-file)) 16 | _filedir 17 | return 0 18 | ;; 19 | --provider) 20 | COMPREPLY=( $(compgen -W "google azure facebook github keycloak gitlab linkedin login.gov digitalocean" -- ${cur}) ) 21 | return 0 22 | ;; 23 | --real-client-ip-header) 24 | COMPREPLY=( $(compgen -W 'X-Real-IP X-Forwarded-For X-ProxyUser-IP' -- ${cur}) ) 25 | return 0 26 | ;; 27 | --@(http-address|https-address|redirect-url|upstream|basic-auth-password|skip-auth-regex|flush-interval|extra-jwt-issuers|email-domain|whitelist-domain|trusted-ip|keycloak-group|azure-tenant|bitbucket-team|bitbucket-repository|github-org|github-team|github-repo|github-token|gitlab-group|github-user|google-group|google-admin-email|google-service-account-json|client-id|client_secret|banner|footer|proxy-prefix|ping-path|cookie-name|cookie-secret|cookie-domain|cookie-path|cookie-expire|cookie-refresh|cookie-samesite|redist-sentinel-master-name|redist-sentinel-connection-urls|redist-cluster-connection-urls|logging-max-size|logging-max-age|logging-max-backups|standard-logging-format|request-logging-format|exclude-logging-paths|auth-logging-format|oidc-issuer-url|oidc-jwks-url|login-url|redeem-url|profile-url|resource|validate-url|scope|approval-prompt|signature-key|acr-values|jwt-key|pubjwk-url|force-json-errors)) 28 | return 0 29 | ;; 30 | esac 31 | COMPREPLY=( $(compgen -W "${_oauth2_proxy_commands}" -- ${cur}) ) 32 | return 0; 33 | } 34 | complete -F _oauth2_proxy oauth2-proxy 35 | -------------------------------------------------------------------------------- /providers/nextcloud_test.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const formatJSON = "format=json" 11 | 12 | func testNextcloudProvider(hostname string) *NextcloudProvider { 13 | p := NewNextcloudProvider( 14 | &ProviderData{ 15 | ProviderName: "", 16 | LoginURL: &url.URL{}, 17 | RedeemURL: &url.URL{}, 18 | ProfileURL: &url.URL{}, 19 | ValidateURL: &url.URL{}, 20 | Scope: ""}) 21 | if hostname != "" { 22 | updateURL(p.Data().LoginURL, hostname) 23 | updateURL(p.Data().RedeemURL, hostname) 24 | updateURL(p.Data().ProfileURL, hostname) 25 | updateURL(p.Data().ValidateURL, hostname) 26 | } 27 | return p 28 | } 29 | 30 | func TestNextcloudProviderDefaults(t *testing.T) { 31 | p := testNextcloudProvider("") 32 | assert.NotEqual(t, nil, p) 33 | assert.Equal(t, "Nextcloud", p.Data().ProviderName) 34 | assert.Equal(t, "", 35 | p.Data().LoginURL.String()) 36 | assert.Equal(t, "", 37 | p.Data().RedeemURL.String()) 38 | assert.Equal(t, "", 39 | p.Data().ValidateURL.String()) 40 | } 41 | 42 | func TestNextcloudProviderOverrides(t *testing.T) { 43 | p := NewNextcloudProvider( 44 | &ProviderData{ 45 | LoginURL: &url.URL{ 46 | Scheme: "https", 47 | Host: "example.com", 48 | Path: "/index.php/apps/oauth2/authorize"}, 49 | RedeemURL: &url.URL{ 50 | Scheme: "https", 51 | Host: "example.com", 52 | Path: "/index.php/apps/oauth2/api/v1/token"}, 53 | ValidateURL: &url.URL{ 54 | Scheme: "https", 55 | Host: "example.com", 56 | Path: "/test/ocs/v2.php/cloud/user", 57 | RawQuery: formatJSON}, 58 | Scope: "profile"}) 59 | assert.NotEqual(t, nil, p) 60 | assert.Equal(t, "Nextcloud", p.Data().ProviderName) 61 | assert.Equal(t, "https://example.com/index.php/apps/oauth2/authorize", 62 | p.Data().LoginURL.String()) 63 | assert.Equal(t, "https://example.com/index.php/apps/oauth2/api/v1/token", 64 | p.Data().RedeemURL.String()) 65 | assert.Equal(t, "https://example.com/test/ocs/v2.php/cloud/user?"+formatJSON, 66 | p.Data().ValidateURL.String()) 67 | } 68 | -------------------------------------------------------------------------------- /providers/util.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | 9 | "golang.org/x/oauth2" 10 | ) 11 | 12 | const ( 13 | tokenTypeBearer = "Bearer" 14 | tokenTypeToken = "token" 15 | 16 | acceptHeader = "Accept" 17 | acceptApplicationJSON = "application/json" 18 | ) 19 | 20 | func makeAuthorizationHeader(prefix, token string, extraHeaders map[string]string) http.Header { 21 | header := make(http.Header) 22 | for key, value := range extraHeaders { 23 | header.Add(key, value) 24 | } 25 | header.Set("Authorization", fmt.Sprintf("%s %s", prefix, token)) 26 | return header 27 | } 28 | 29 | func makeOIDCHeader(accessToken string) http.Header { 30 | // extra headers required by the IDP when making authenticated requests 31 | extraHeaders := map[string]string{ 32 | acceptHeader: acceptApplicationJSON, 33 | } 34 | return makeAuthorizationHeader(tokenTypeBearer, accessToken, extraHeaders) 35 | } 36 | 37 | func makeLoginURL(p *ProviderData, redirectURI, state string, extraParams url.Values) url.URL { 38 | a := *p.LoginURL 39 | params, _ := url.ParseQuery(a.RawQuery) 40 | params.Set("redirect_uri", redirectURI) 41 | params.Add("scope", p.Scope) 42 | params.Set("client_id", p.ClientID) 43 | params.Set("response_type", "code") 44 | params.Add("state", state) 45 | for n, p := range extraParams { 46 | for _, v := range p { 47 | params.Add(n, v) 48 | } 49 | } 50 | a.RawQuery = params.Encode() 51 | return a 52 | } 53 | 54 | // getIDToken extracts an IDToken stored in the `Extra` fields of an 55 | // oauth2.Token 56 | func getIDToken(token *oauth2.Token) string { 57 | idToken, ok := token.Extra("id_token").(string) 58 | if !ok { 59 | return "" 60 | } 61 | return idToken 62 | } 63 | 64 | // formatGroup coerces an OIDC groups claim into a string 65 | // If it is non-string, marshal it into JSON. 66 | func formatGroup(rawGroup interface{}) (string, error) { 67 | if group, ok := rawGroup.(string); ok { 68 | return group, nil 69 | } 70 | 71 | jsonGroup, err := json.Marshal(rawGroup) 72 | if err != nil { 73 | return "", err 74 | } 75 | return string(jsonGroup), nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/apis/options/common.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // SecretSource references an individual secret value. 10 | // Only one source within the struct should be defined at any time. 11 | type SecretSource struct { 12 | // Value expects a base64 encoded string value. 13 | Value []byte `json:"value,omitempty"` 14 | 15 | // FromEnv expects the name of an environment variable. 16 | FromEnv string `json:"fromEnv,omitempty"` 17 | 18 | // FromFile expects a path to a file containing the secret value. 19 | FromFile string `json:"fromFile,omitempty"` 20 | } 21 | 22 | // Duration is an alias for time.Duration so that we can ensure the marshalling 23 | // and unmarshalling of string durations is done as users expect. 24 | // Intentional blank line below to keep this first part of the comment out of 25 | // any generated references. 26 | 27 | // Duration is as string representation of a period of time. 28 | // A duration string is a is a possibly signed sequence of decimal numbers, 29 | // each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". 30 | // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". 31 | // +reference-gen:alias-name=string 32 | type Duration time.Duration 33 | 34 | // UnmarshalJSON parses the duration string and sets the value of duration 35 | // to the value of the duration string. 36 | func (d *Duration) UnmarshalJSON(data []byte) error { 37 | input := string(data) 38 | if unquoted, err := strconv.Unquote(input); err == nil { 39 | input = unquoted 40 | } 41 | 42 | du, err := time.ParseDuration(input) 43 | if err != nil { 44 | return err 45 | } 46 | *d = Duration(du) 47 | return nil 48 | } 49 | 50 | // MarshalJSON ensures that when the string is marshalled to JSON as a human 51 | // readable string. 52 | func (d *Duration) MarshalJSON() ([]byte, error) { 53 | dStr := fmt.Sprintf("%q", d.Duration().String()) 54 | return []byte(dStr), nil 55 | } 56 | 57 | // Duration returns the time.Duration version of this Duration 58 | func (d *Duration) Duration() time.Duration { 59 | if d == nil { 60 | return time.Duration(0) 61 | } 62 | return time.Duration(*d) 63 | } 64 | -------------------------------------------------------------------------------- /docs/docs/community/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | :::note 7 | OAuth2 Proxy is a community project. 8 | Maintainers do not work on this project full time, and as such, 9 | while we endeavour to respond to disclosures as quickly as possible, 10 | this may take longer than in projects with corporate sponsorship. 11 | ::: 12 | 13 | ## Security Disclosures 14 | 15 | :::important 16 | If you believe you have found a vulnerability within OAuth2 Proxy or any of its 17 | dependencies, please do NOT open an issue or PR on GitHub, please do NOT post 18 | any details publicly. 19 | ::: 20 | 21 | Security disclosures MUST be done in private. 22 | If you have found an issue that you would like to bring to the attention of the 23 | maintenance team for OAuth2 Proxy, please compose an email and send it to the 24 | list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file. 25 | 26 | Please include as much detail as possible. 27 | Ideally, your disclosure should include: 28 | - A reproducible case that can be used to demonstrate the exploit 29 | - How you discovered this vulnerability 30 | - A potential fix for the issue (if you have thought of one) 31 | - Versions affected (if not present in master) 32 | - Your GitHub ID 33 | 34 | ### How will we respond to disclosures? 35 | 36 | We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories) 37 | to privately discuss fixes for disclosed vulnerabilities. 38 | If you include a GitHub ID with your disclosure we will add you as a collaborator 39 | for the advisory so that you can join the discussion and validate any fixes 40 | we may propose. 41 | 42 | For minor issues and previously disclosed vulnerabilities (typically for 43 | dependencies), we may use regular PRs for fixes and forego the security advisory. 44 | 45 | Once a fix has been agreed upon, we will merge the fix and create a new release. 46 | If we have multiple security issues in flight simultaneously, we may delay 47 | merging fixes until all patches are ready. 48 | We may also backport the fix to previous releases, 49 | but this will be at the discretion of the maintainers. 50 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | paths: ['docs/**'] 7 | push: 8 | branches: [master] 9 | paths: ['docs/**'] 10 | 11 | jobs: 12 | checks: 13 | if: github.event_name != 'push' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: '17.x' 20 | - name: Test Build 21 | working-directory: ./docs 22 | env: 23 | NODE_OPTIONS: --openssl-legacy-provider 24 | run: | 25 | if [ -e yarn.lock ]; then 26 | yarn install --frozen-lockfile 27 | elif [ -e package-lock.json ]; then 28 | npm ci 29 | else 30 | npm i 31 | fi 32 | npm run build 33 | gh-release: 34 | if: github.event_name != 'pull_request' 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v1 38 | - uses: actions/setup-node@v1 39 | with: 40 | node-version: '17.x' 41 | - name: Add key to allow access to repository 42 | env: 43 | SSH_AUTH_SOCK: /tmp/ssh_agent.sock 44 | NODE_OPTIONS: --openssl-legacy-provider 45 | run: | 46 | mkdir -p ~/.ssh 47 | ssh-keyscan github.com >> ~/.ssh/known_hosts 48 | echo "${{ secrets.GH_PAGES_DEPLOY }}" > ~/.ssh/id_rsa 49 | chmod 600 ~/.ssh/id_rsa 50 | cat <> ~/.ssh/config 51 | Host github.com 52 | HostName github.com 53 | IdentityFile ~/.ssh/id_rsa 54 | EOT 55 | - name: Release to GitHub Pages 56 | working-directory: ./docs 57 | env: 58 | USE_SSH: true 59 | GIT_USER: git 60 | NODE_OPTIONS: --openssl-legacy-provider 61 | run: | 62 | git config --global user.email "actions@gihub.com" 63 | git config --global user.name "gh-actions" 64 | if [ -e yarn.lock ]; then 65 | yarn install --frozen-lockfile 66 | elif [ -e package-lock.json ]; then 67 | npm ci 68 | else 69 | npm i 70 | fi 71 | npx docusaurus deploy 72 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-6.1.x/community/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | :::note 7 | OAuth2 Proxy is a community project. 8 | Maintainers do not work on this project full time, and as such, 9 | while we endeavour to respond to disclosures as quickly as possible, 10 | this may take longer than in projects with corporate sponsorship. 11 | ::: 12 | 13 | ## Security Disclosures 14 | 15 | :::important 16 | If you believe you have found a vulnerability within OAuth2 Proxy or any of its 17 | dependencies, please do NOT open an issue or PR on GitHub, please do NOT post any 18 | details publicly. 19 | ::: 20 | 21 | Security disclosures MUST be done in private. 22 | If you have found an issue that you would like to bring to the attention of the 23 | maintenance team for OAuth2 Proxy, please compose an email and send it to the 24 | list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file. 25 | 26 | Please include as much detail as possible. 27 | Ideally, your disclosure should include: 28 | - A reproducible case that can be used to demonstrate the exploit 29 | - How you discovered this vulnerability 30 | - A potential fix for the issue (if you have thought of one) 31 | - Versions affected (if not present in master) 32 | - Your GitHub ID 33 | 34 | ### How will we respond to disclosures? 35 | 36 | We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories) 37 | to privately discuss fixes for disclosed vulnerabilities. 38 | If you include a GitHub ID with your disclosure we will add you as a collaborator 39 | for the advisory so that you can join the discussion and validate any fixes 40 | we may propose. 41 | 42 | For minor issues and previously disclosed vulnerabilities (typically for 43 | dependencies), we may use regular PRs for fixes and forego the security advisory. 44 | 45 | Once a fix has been agreed upon, we will merge the fix and create a new release. 46 | If we have multiple security issues in flight simultaneously, we may delay 47 | merging fixes until all patches are ready. 48 | We may also backport the fix to previous releases, 49 | but this will be at the discretion of the maintainers. 50 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.0.x/community/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | :::note 7 | OAuth2 Proxy is a community project. 8 | Maintainers do not work on this project full time, and as such, 9 | while we endeavour to respond to disclosures as quickly as possible, 10 | this may take longer than in projects with corporate sponsorship. 11 | ::: 12 | 13 | ## Security Disclosures 14 | 15 | :::important 16 | If you believe you have found a vulnerability within OAuth2 Proxy or any of its 17 | dependencies, please do NOT open an issue or PR on GitHub, please do NOT post 18 | any details publicly. 19 | ::: 20 | 21 | Security disclosures MUST be done in private. 22 | If you have found an issue that you would like to bring to the attention of the 23 | maintenance team for OAuth2 Proxy, please compose an email and send it to the 24 | list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file. 25 | 26 | Please include as much detail as possible. 27 | Ideally, your disclosure should include: 28 | - A reproducible case that can be used to demonstrate the exploit 29 | - How you discovered this vulnerability 30 | - A potential fix for the issue (if you have thought of one) 31 | - Versions affected (if not present in master) 32 | - Your GitHub ID 33 | 34 | ### How will we respond to disclosures? 35 | 36 | We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories) 37 | to privately discuss fixes for disclosed vulnerabilities. 38 | If you include a GitHub ID with your disclosure we will add you as a collaborator 39 | for the advisory so that you can join the discussion and validate any fixes 40 | we may propose. 41 | 42 | For minor issues and previously disclosed vulnerabilities (typically for 43 | dependencies), we may use regular PRs for fixes and forego the security advisory. 44 | 45 | Once a fix has been agreed upon, we will merge the fix and create a new release. 46 | If we have multiple security issues in flight simultaneously, we may delay 47 | merging fixes until all patches are ready. 48 | We may also backport the fix to previous releases, 49 | but this will be at the discretion of the maintainers. 50 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.1.x/community/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | :::note 7 | OAuth2 Proxy is a community project. 8 | Maintainers do not work on this project full time, and as such, 9 | while we endeavour to respond to disclosures as quickly as possible, 10 | this may take longer than in projects with corporate sponsorship. 11 | ::: 12 | 13 | ## Security Disclosures 14 | 15 | :::important 16 | If you believe you have found a vulnerability within OAuth2 Proxy or any of its 17 | dependencies, please do NOT open an issue or PR on GitHub, please do NOT post 18 | any details publicly. 19 | ::: 20 | 21 | Security disclosures MUST be done in private. 22 | If you have found an issue that you would like to bring to the attention of the 23 | maintenance team for OAuth2 Proxy, please compose an email and send it to the 24 | list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file. 25 | 26 | Please include as much detail as possible. 27 | Ideally, your disclosure should include: 28 | - A reproducible case that can be used to demonstrate the exploit 29 | - How you discovered this vulnerability 30 | - A potential fix for the issue (if you have thought of one) 31 | - Versions affected (if not present in master) 32 | - Your GitHub ID 33 | 34 | ### How will we respond to disclosures? 35 | 36 | We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories) 37 | to privately discuss fixes for disclosed vulnerabilities. 38 | If you include a GitHub ID with your disclosure we will add you as a collaborator 39 | for the advisory so that you can join the discussion and validate any fixes 40 | we may propose. 41 | 42 | For minor issues and previously disclosed vulnerabilities (typically for 43 | dependencies), we may use regular PRs for fixes and forego the security advisory. 44 | 45 | Once a fix has been agreed upon, we will merge the fix and create a new release. 46 | If we have multiple security issues in flight simultaneously, we may delay 47 | merging fixes until all patches are ready. 48 | We may also backport the fix to previous releases, 49 | but this will be at the discretion of the maintainers. 50 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.2.x/community/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | :::note 7 | OAuth2 Proxy is a community project. 8 | Maintainers do not work on this project full time, and as such, 9 | while we endeavour to respond to disclosures as quickly as possible, 10 | this may take longer than in projects with corporate sponsorship. 11 | ::: 12 | 13 | ## Security Disclosures 14 | 15 | :::important 16 | If you believe you have found a vulnerability within OAuth2 Proxy or any of its 17 | dependencies, please do NOT open an issue or PR on GitHub, please do NOT post 18 | any details publicly. 19 | ::: 20 | 21 | Security disclosures MUST be done in private. 22 | If you have found an issue that you would like to bring to the attention of the 23 | maintenance team for OAuth2 Proxy, please compose an email and send it to the 24 | list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file. 25 | 26 | Please include as much detail as possible. 27 | Ideally, your disclosure should include: 28 | - A reproducible case that can be used to demonstrate the exploit 29 | - How you discovered this vulnerability 30 | - A potential fix for the issue (if you have thought of one) 31 | - Versions affected (if not present in master) 32 | - Your GitHub ID 33 | 34 | ### How will we respond to disclosures? 35 | 36 | We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories) 37 | to privately discuss fixes for disclosed vulnerabilities. 38 | If you include a GitHub ID with your disclosure we will add you as a collaborator 39 | for the advisory so that you can join the discussion and validate any fixes 40 | we may propose. 41 | 42 | For minor issues and previously disclosed vulnerabilities (typically for 43 | dependencies), we may use regular PRs for fixes and forego the security advisory. 44 | 45 | Once a fix has been agreed upon, we will merge the fix and create a new release. 46 | If we have multiple security issues in flight simultaneously, we may delay 47 | merging fixes until all patches are ready. 48 | We may also backport the fix to previous releases, 49 | but this will be at the discretion of the maintainers. 50 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.3.x/community/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: security 3 | title: Security 4 | --- 5 | 6 | :::note 7 | OAuth2 Proxy is a community project. 8 | Maintainers do not work on this project full time, and as such, 9 | while we endeavour to respond to disclosures as quickly as possible, 10 | this may take longer than in projects with corporate sponsorship. 11 | ::: 12 | 13 | ## Security Disclosures 14 | 15 | :::important 16 | If you believe you have found a vulnerability within OAuth2 Proxy or any of its 17 | dependencies, please do NOT open an issue or PR on GitHub, please do NOT post 18 | any details publicly. 19 | ::: 20 | 21 | Security disclosures MUST be done in private. 22 | If you have found an issue that you would like to bring to the attention of the 23 | maintenance team for OAuth2 Proxy, please compose an email and send it to the 24 | list of maintainers in our [MAINTAINERS](https://github.com/oauth2-proxy/oauth2-proxy/blob/master/MAINTAINERS) file. 25 | 26 | Please include as much detail as possible. 27 | Ideally, your disclosure should include: 28 | - A reproducible case that can be used to demonstrate the exploit 29 | - How you discovered this vulnerability 30 | - A potential fix for the issue (if you have thought of one) 31 | - Versions affected (if not present in master) 32 | - Your GitHub ID 33 | 34 | ### How will we respond to disclosures? 35 | 36 | We use [GitHub Security Advisories](https://docs.github.com/en/github/managing-security-vulnerabilities/about-github-security-advisories) 37 | to privately discuss fixes for disclosed vulnerabilities. 38 | If you include a GitHub ID with your disclosure we will add you as a collaborator 39 | for the advisory so that you can join the discussion and validate any fixes 40 | we may propose. 41 | 42 | For minor issues and previously disclosed vulnerabilities (typically for 43 | dependencies), we may use regular PRs for fixes and forego the security advisory. 44 | 45 | Once a fix has been agreed upon, we will merge the fix and create a new release. 46 | If we have multiple security issues in flight simultaneously, we may delay 47 | merging fixes until all patches are ready. 48 | We may also backport the fix to previous releases, 49 | but this will be at the discretion of the maintainers. 50 | -------------------------------------------------------------------------------- /pkg/middleware/metrics_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/ginkgo/extensions/table" 10 | . "github.com/onsi/gomega" 11 | 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/prometheus/client_golang/prometheus/testutil" 14 | ) 15 | 16 | var _ = Describe("Instrumentation suite", func() { 17 | type requestTableInput struct { 18 | registry *prometheus.Registry 19 | requestString string 20 | expectedHandler http.Handler 21 | expectedMetrics []string 22 | expectedStatus int 23 | // Prometheus output is large so is stored in testdata 24 | expectedResultsFile string 25 | } 26 | 27 | DescribeTable("when serving a request", 28 | func(in *requestTableInput) { 29 | req := httptest.NewRequest("", in.requestString, nil) 30 | 31 | rw := httptest.NewRecorder() 32 | 33 | handler := NewRequestMetrics(in.registry)(in.expectedHandler) 34 | handler.ServeHTTP(rw, req) 35 | 36 | Expect(rw.Code).To(Equal(in.expectedStatus)) 37 | 38 | expectedPrometheusText, err := os.Open(in.expectedResultsFile) 39 | Expect(err).NotTo(HaveOccurred()) 40 | 41 | err = testutil.GatherAndCompare(in.registry, expectedPrometheusText, in.expectedMetrics...) 42 | Expect(err).NotTo(HaveOccurred()) 43 | }, 44 | Entry("successfully", func() *requestTableInput { 45 | in := &requestTableInput{ 46 | registry: prometheus.NewRegistry(), 47 | requestString: "http://example.com/metrics", 48 | expectedMetrics: []string{ 49 | "oauth2_proxy_requests_total", 50 | }, 51 | expectedStatus: 200, 52 | expectedResultsFile: "testdata/metrics/successfulrequest.txt", 53 | } 54 | in.expectedHandler = NewMetricsHandler(in.registry, in.registry) 55 | 56 | return in 57 | }()), 58 | Entry("with not found", &requestTableInput{ 59 | registry: prometheus.NewRegistry(), 60 | requestString: "http://example.com/", 61 | expectedHandler: http.NotFoundHandler(), 62 | expectedMetrics: []string{"oauth2_proxy_requests_total"}, 63 | expectedStatus: 404, 64 | expectedResultsFile: "testdata/metrics/notfoundrequest.txt", 65 | }), 66 | ) 67 | }) 68 | -------------------------------------------------------------------------------- /pkg/app/redirect/validator.go: -------------------------------------------------------------------------------- 1 | package redirect 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 9 | 10 | util "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util" 11 | ) 12 | 13 | var ( 14 | // Used to check final redirects are not susceptible to open redirects. 15 | // Matches //, /\ and both of these with whitespace in between (eg / / or / \). 16 | invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`) 17 | ) 18 | 19 | // Validator is an interface to allow validation of application 20 | // redirect URLs. 21 | // As these values are determined from the request, they must go 22 | // through thorough checks to ensure the safety of the end user. 23 | type Validator interface { 24 | IsValidRedirect(redirect string) bool 25 | } 26 | 27 | // NewValidator constructs a new redirect validator. 28 | func NewValidator(allowedDomains []string) Validator { 29 | return &validator{ 30 | allowedDomains: allowedDomains, 31 | } 32 | } 33 | 34 | // validator implements the Validator interface to allow validation 35 | // of redirect URLs. 36 | type validator struct { 37 | allowedDomains []string 38 | } 39 | 40 | // IsValidRedirect checks whether the redirect URL is safe and allowed. 41 | func (v *validator) IsValidRedirect(redirect string) bool { 42 | switch { 43 | case redirect == "": 44 | // The user didn't specify a redirect. 45 | // In this case, we expect the proxt to fallback to `/` 46 | return false 47 | case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect): 48 | return true 49 | case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"): 50 | redirectURL, err := url.Parse(redirect) 51 | if err != nil { 52 | logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect) 53 | return false 54 | } 55 | 56 | if util.IsEndpointAllowed(redirectURL, v.allowedDomains) { 57 | return true 58 | } 59 | 60 | logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect) 61 | return false 62 | default: 63 | logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect) 64 | return false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/apis/options/sessions.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | // SessionOptions contains configuration options for the SessionStore providers. 4 | type SessionOptions struct { 5 | Type string `flag:"session-store-type" cfg:"session_store_type"` 6 | Cookie CookieStoreOptions `cfg:",squash"` 7 | Redis RedisStoreOptions `cfg:",squash"` 8 | } 9 | 10 | // CookieSessionStoreType is used to indicate the CookieSessionStore should be 11 | // used for storing sessions. 12 | var CookieSessionStoreType = "cookie" 13 | 14 | // RedisSessionStoreType is used to indicate the RedisSessionStore should be 15 | // used for storing sessions. 16 | var RedisSessionStoreType = "redis" 17 | 18 | // CookieStoreOptions contains configuration options for the CookieSessionStore. 19 | type CookieStoreOptions struct { 20 | Minimal bool `flag:"session-cookie-minimal" cfg:"session_cookie_minimal"` 21 | } 22 | 23 | // RedisStoreOptions contains configuration options for the RedisSessionStore. 24 | type RedisStoreOptions struct { 25 | ConnectionURL string `flag:"redis-connection-url" cfg:"redis_connection_url"` 26 | Password string `flag:"redis-password" cfg:"redis_password"` 27 | UseSentinel bool `flag:"redis-use-sentinel" cfg:"redis_use_sentinel"` 28 | SentinelPassword string `flag:"redis-sentinel-password" cfg:"redis_sentinel_password"` 29 | SentinelMasterName string `flag:"redis-sentinel-master-name" cfg:"redis_sentinel_master_name"` 30 | SentinelConnectionURLs []string `flag:"redis-sentinel-connection-urls" cfg:"redis_sentinel_connection_urls"` 31 | UseCluster bool `flag:"redis-use-cluster" cfg:"redis_use_cluster"` 32 | ClusterConnectionURLs []string `flag:"redis-cluster-connection-urls" cfg:"redis_cluster_connection_urls"` 33 | CAPath string `flag:"redis-ca-path" cfg:"redis_ca_path"` 34 | InsecureSkipTLSVerify bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify"` 35 | IdleTimeout int `flag:"redis-connection-idle-timeout" cfg:"redis_connection_idle_timeout"` 36 | } 37 | 38 | func sessionOptionsDefaults() SessionOptions { 39 | return SessionOptions{ 40 | Type: CookieSessionStoreType, 41 | Cookie: CookieStoreOptions{ 42 | Minimal: false, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contrib/local-environment/docker-compose-keycloak.yaml: -------------------------------------------------------------------------------- 1 | # This docker-compose file can be used to bring up an example instance of oauth2-proxy 2 | # for manual testing and exploration of features. 3 | # Alongside OAuth2-Proxy, this file also starts Keycloak to act as the identity provider, 4 | # HTTPBin as an example upstream. 5 | # 6 | # This can either be created using docker-compose 7 | # docker-compose -f docker-compose-keycloak.yaml 8 | # Or: 9 | # make keycloak- (eg. make keycloak-up, make keycloak-down) 10 | # 11 | # Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password 12 | # Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings 13 | version: '3.0' 14 | services: 15 | 16 | oauth2-proxy: 17 | container_name: oauth2-proxy 18 | image: quay.io/oauth2-proxy/oauth2-proxy:v7.3.0 19 | command: --config /oauth2-proxy.cfg 20 | hostname: oauth2-proxy 21 | volumes: 22 | - "./oauth2-proxy-keycloak.cfg:/oauth2-proxy.cfg" 23 | restart: unless-stopped 24 | networks: 25 | keycloak: {} 26 | httpbin: {} 27 | oauth2-proxy: {} 28 | depends_on: 29 | - httpbin 30 | - keycloak 31 | ports: 32 | - 4180:4180/tcp 33 | 34 | httpbin: 35 | container_name: httpbin 36 | image: kennethreitz/httpbin:latest 37 | hostname: httpbin 38 | networks: 39 | httpbin: {} 40 | 41 | keycloak: 42 | container_name: keycloak 43 | image: jboss/keycloak:10.0.0 44 | hostname: keycloak 45 | command: 46 | [ 47 | '-b', 48 | '0.0.0.0', 49 | '-Djboss.socket.binding.port-offset=1000', 50 | '-Dkeycloak.migration.action=import', 51 | '-Dkeycloak.migration.provider=dir', 52 | '-Dkeycloak.migration.dir=/realm-config', 53 | '-Dkeycloak.migration.strategy=IGNORE_EXISTING', 54 | ] 55 | volumes: 56 | - ./keycloak:/realm-config 57 | environment: 58 | KEYCLOAK_USER: admin@example.com 59 | KEYCLOAK_PASSWORD: password 60 | networks: 61 | keycloak: 62 | aliases: 63 | - keycloak.localtest.me 64 | ports: 65 | - 9080:9080/tcp 66 | 67 | networks: 68 | httpbin: {} 69 | keycloak: {} 70 | oauth2-proxy: {} 71 | -------------------------------------------------------------------------------- /pkg/sessions/redis/lock.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/bsm/redislock" 10 | "github.com/go-redis/redis/v8" 11 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 12 | ) 13 | 14 | const LockSuffix = "lock" 15 | 16 | type Lock struct { 17 | client redis.Cmdable 18 | locker *redislock.Client 19 | lock *redislock.Lock 20 | key string 21 | } 22 | 23 | // NewLock instantiate a new lock instance. This will not yet apply a lock on Redis side. 24 | // For that you have to call Obtain(ctx context.Context, expiration time.Duration) 25 | func NewLock(client redis.Cmdable, key string) sessions.Lock { 26 | return &Lock{ 27 | client: client, 28 | locker: redislock.New(client), 29 | key: key, 30 | } 31 | } 32 | 33 | // Obtain obtains a distributed lock on Redis for the configured key. 34 | func (l *Lock) Obtain(ctx context.Context, expiration time.Duration) error { 35 | lock, err := l.locker.Obtain(ctx, l.lockKey(), expiration, nil) 36 | if errors.Is(err, redislock.ErrNotObtained) { 37 | return sessions.ErrLockNotObtained 38 | } 39 | if err != nil { 40 | return err 41 | } 42 | l.lock = lock 43 | return nil 44 | } 45 | 46 | // Refresh refreshes an already existing lock. 47 | func (l *Lock) Refresh(ctx context.Context, expiration time.Duration) error { 48 | if l.lock == nil { 49 | return sessions.ErrNotLocked 50 | } 51 | err := l.lock.Refresh(ctx, expiration, nil) 52 | if errors.Is(err, redislock.ErrNotObtained) { 53 | return sessions.ErrNotLocked 54 | } 55 | return err 56 | } 57 | 58 | // Peek returns true, if the lock is still applied. 59 | func (l *Lock) Peek(ctx context.Context) (bool, error) { 60 | v, err := l.client.Exists(ctx, l.lockKey()).Result() 61 | if err != nil { 62 | return false, err 63 | } 64 | if v == 0 { 65 | return false, nil 66 | } 67 | return true, nil 68 | } 69 | 70 | // Release releases the lock on Redis side. 71 | func (l *Lock) Release(ctx context.Context) error { 72 | if l.lock == nil { 73 | return sessions.ErrNotLocked 74 | } 75 | err := l.lock.Release(ctx) 76 | if errors.Is(err, redislock.ErrLockNotHeld) { 77 | return sessions.ErrNotLocked 78 | } 79 | return err 80 | } 81 | 82 | func (l *Lock) lockKey() string { 83 | return fmt.Sprintf("%s.%s", l.key, LockSuffix) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/upstream/rewrite_test.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "regexp" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app/pagewriter" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/ginkgo/extensions/table" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("Rewrite", func() { 15 | type rewritePathTableInput struct { 16 | rewriteRegex *regexp.Regexp 17 | rewriteTarget string 18 | requestTarget string 19 | expectedRequestURI string 20 | } 21 | 22 | DescribeTable("should rewrite the request path", 23 | func(in rewritePathTableInput) { 24 | req := httptest.NewRequest("", in.requestTarget, nil) 25 | rw := httptest.NewRecorder() 26 | 27 | var gotRequestURI string 28 | handler := newRewritePath(in.rewriteRegex, in.rewriteTarget, &pagewriter.WriterFuncs{})(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 | gotRequestURI = r.RequestURI 30 | })) 31 | handler.ServeHTTP(rw, req) 32 | 33 | Expect(gotRequestURI).To(Equal(in.expectedRequestURI)) 34 | }, 35 | Entry("when the path matches the regexp", rewritePathTableInput{ 36 | rewriteRegex: regexp.MustCompile("^/http/(.*)"), 37 | rewriteTarget: "/$1", 38 | requestTarget: "http://example.com/http/foo/bar", 39 | expectedRequestURI: "http://example.com/foo/bar", 40 | }), 41 | Entry("when the path does not match the regexp", rewritePathTableInput{ 42 | rewriteRegex: regexp.MustCompile("^/http/(.*)"), 43 | rewriteTarget: "/$1", 44 | requestTarget: "https://example.com/https/foo/bar", 45 | expectedRequestURI: "https://example.com/https/foo/bar", 46 | }), 47 | Entry("when the regexp is not anchored", rewritePathTableInput{ 48 | rewriteRegex: regexp.MustCompile("/http/(.*)"), 49 | rewriteTarget: "/$1", 50 | requestTarget: "http://example.com/bar/http/foo/bar", 51 | expectedRequestURI: "http://example.com/bar/foo/bar", 52 | }), 53 | Entry("when the regexp is rewriting to a query", rewritePathTableInput{ 54 | rewriteRegex: regexp.MustCompile(`/articles/([a-z0-9\-]*)`), 55 | rewriteTarget: "/article?id=$1", 56 | requestTarget: "http://example.com/articles/blog-2021-01-01", 57 | expectedRequestURI: "http://example.com/article?id=blog-2021-01-01", 58 | }), 59 | ) 60 | }) 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # This ARG has to be at the top, otherwise the docker daemon does not known what to do with FROM ${RUNTIME_IMAGE} 2 | ARG RUNTIME_IMAGE=alpine:3.16 3 | 4 | # All builds should be done using the platform native to the build node to allow 5 | # cache sharing of the go mod download step. 6 | # Go cross compilation is also faster than emulation the go compilation across 7 | # multiple platforms. 8 | FROM --platform=${BUILDPLATFORM} golang:1.18-buster AS builder 9 | 10 | # Copy sources 11 | WORKDIR $GOPATH/src/github.com/oauth2-proxy/oauth2-proxy 12 | 13 | # Fetch dependencies 14 | COPY go.mod go.sum ./ 15 | RUN go mod download 16 | 17 | # Now pull in our code 18 | COPY . . 19 | 20 | # Arguments go here so that the previous steps can be cached if no external 21 | # sources have changed. 22 | ARG VERSION 23 | ARG TARGETPLATFORM 24 | ARG BUILDPLATFORM 25 | 26 | # Build binary and make sure there is at least an empty key file. 27 | # This is useful for GCP App Engine custom runtime builds, because 28 | # you cannot use multiline variables in their app.yaml, so you have to 29 | # build the key into the container and then tell it where it is 30 | # by setting OAUTH2_PROXY_JWT_KEY_FILE=/etc/ssl/private/jwt_signing_key.pem 31 | # in app.yaml instead. 32 | # Set the cross compilation arguments based on the TARGETPLATFORM which is 33 | # automatically set by the docker engine. 34 | RUN case ${TARGETPLATFORM} in \ 35 | "linux/amd64") GOARCH=amd64 ;; \ 36 | # arm64 and arm64v8 are equivilant in go and do not require a goarm 37 | # https://github.com/golang/go/wiki/GoArm 38 | "linux/arm64" | "linux/arm64/v8") GOARCH=arm64 ;; \ 39 | "linux/ppc64le") GOARCH=ppc64le ;; \ 40 | "linux/arm/v6") GOARCH=arm GOARM=6 ;; \ 41 | esac && \ 42 | printf "Building OAuth2 Proxy for arch ${GOARCH}\n" && \ 43 | GOARCH=${GOARCH} VERSION=${VERSION} make build && touch jwt_signing_key.pem 44 | 45 | # Copy binary to alpine 46 | FROM ${RUNTIME_IMAGE} 47 | COPY nsswitch.conf /etc/nsswitch.conf 48 | COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/oauth2-proxy /bin/oauth2-proxy 49 | COPY --from=builder /go/src/github.com/oauth2-proxy/oauth2-proxy/jwt_signing_key.pem /etc/ssl/private/jwt_signing_key.pem 50 | 51 | # UID/GID 65532 is also known as nonroot user in distroless image 52 | USER 65532:65532 53 | 54 | ENTRYPOINT ["/bin/oauth2-proxy"] 55 | -------------------------------------------------------------------------------- /pkg/app/redirect/getters.go: -------------------------------------------------------------------------------- 1 | package redirect 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util" 8 | ) 9 | 10 | // redirectGetter represents a method to allow the proxy to determine a redirect 11 | // based on the original request. 12 | type redirectGetter func(req *http.Request) string 13 | 14 | // getRdQuerystringRedirect handles this getAppRedirect strategy: 15 | // - `rd` querysting parameter 16 | func (a *appDirector) getRdQuerystringRedirect(req *http.Request) string { 17 | return a.validateRedirect( 18 | req.Form.Get("rd"), 19 | "Invalid redirect provided in rd querystring parameter: %s", 20 | ) 21 | } 22 | 23 | // getXAuthRequestRedirect handles this getAppRedirect strategy: 24 | // - `X-Auth-Request-Redirect` Header 25 | func (a *appDirector) getXAuthRequestRedirect(req *http.Request) string { 26 | return a.validateRedirect( 27 | req.Header.Get("X-Auth-Request-Redirect"), 28 | "Invalid redirect provided in X-Auth-Request-Redirect header: %s", 29 | ) 30 | } 31 | 32 | // getXForwardedHeadersRedirect handles these getAppRedirect strategies: 33 | // - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled) 34 | // - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*) 35 | func (a *appDirector) getXForwardedHeadersRedirect(req *http.Request) string { 36 | if !requestutil.IsForwardedRequest(req) { 37 | return "" 38 | } 39 | 40 | uri := requestutil.GetRequestURI(req) 41 | if a.hasProxyPrefix(uri) { 42 | uri = "/" 43 | } 44 | 45 | redirect := fmt.Sprintf( 46 | "%s://%s%s", 47 | requestutil.GetRequestProto(req), 48 | requestutil.GetRequestHost(req), 49 | uri, 50 | ) 51 | 52 | return a.validateRedirect(redirect, 53 | "Invalid redirect generated from X-Forwarded-* headers: %s") 54 | } 55 | 56 | // getURIRedirect handles these getAppRedirect strategies: 57 | // - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled) 58 | // - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*) 59 | // - `/` 60 | func (a *appDirector) getURIRedirect(req *http.Request) string { 61 | redirect := a.validateRedirect( 62 | requestutil.GetRequestURI(req), 63 | "Invalid redirect generated from X-Forwarded-Uri header: %s", 64 | ) 65 | if redirect == "" { 66 | redirect = req.URL.RequestURI() 67 | } 68 | 69 | if a.hasProxyPrefix(redirect) { 70 | return "/" 71 | } 72 | return redirect 73 | } 74 | -------------------------------------------------------------------------------- /pkg/requests/requests_suite_test.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 13 | . "github.com/onsi/ginkgo" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var ( 18 | server *httptest.Server 19 | serverAddr string 20 | ) 21 | 22 | func TestRequetsSuite(t *testing.T) { 23 | logger.SetOutput(GinkgoWriter) 24 | logger.SetErrOutput(GinkgoWriter) 25 | log.SetOutput(GinkgoWriter) 26 | 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Requests Suite") 29 | } 30 | 31 | var _ = BeforeSuite(func() { 32 | // Set up a webserver that reflects requests 33 | mux := http.NewServeMux() 34 | mux.Handle("/json/", &testHTTPUpstream{}) 35 | mux.HandleFunc("/string/", func(rw http.ResponseWriter, _ *http.Request) { 36 | rw.Write([]byte("OK")) 37 | }) 38 | server = httptest.NewServer(mux) 39 | serverAddr = fmt.Sprintf("http://%s", server.Listener.Addr().String()) 40 | }) 41 | 42 | var _ = AfterSuite(func() { 43 | server.Close() 44 | }) 45 | 46 | // testHTTPRequest is a struct used to capture the state of a request made to 47 | // the test server 48 | type testHTTPRequest struct { 49 | Method string 50 | Header http.Header 51 | Body []byte 52 | RequestURI string 53 | } 54 | 55 | type testHTTPUpstream struct{} 56 | 57 | func (t *testHTTPUpstream) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 58 | request, err := toTestHTTPRequest(req) 59 | if err != nil { 60 | t.writeError(rw, err) 61 | return 62 | } 63 | 64 | data, err := json.Marshal(request) 65 | if err != nil { 66 | t.writeError(rw, err) 67 | return 68 | } 69 | 70 | rw.Header().Set("Content-Type", "application/json") 71 | rw.Write(data) 72 | } 73 | 74 | func (t *testHTTPUpstream) writeError(rw http.ResponseWriter, err error) { 75 | rw.WriteHeader(500) 76 | if err != nil { 77 | rw.Write([]byte(err.Error())) 78 | } 79 | } 80 | 81 | func toTestHTTPRequest(req *http.Request) (testHTTPRequest, error) { 82 | requestBody := []byte{} 83 | if req.Body != http.NoBody { 84 | var err error 85 | requestBody, err = ioutil.ReadAll(req.Body) 86 | if err != nil { 87 | return testHTTPRequest{}, err 88 | } 89 | } 90 | 91 | return testHTTPRequest{ 92 | Method: req.Method, 93 | Header: req.Header, 94 | Body: requestBody, 95 | RequestURI: req.RequestURI, 96 | }, nil 97 | } 98 | -------------------------------------------------------------------------------- /providers/internal_util.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/url" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 9 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" 10 | ) 11 | 12 | // stripToken is a helper function to obfuscate "access_token" 13 | // query parameters 14 | func stripToken(endpoint string) string { 15 | return stripParam("access_token", endpoint) 16 | } 17 | 18 | // stripParam generalizes the obfuscation of a particular 19 | // query parameter - typically 'access_token' or 'client_secret' 20 | // The parameter's second half is replaced by '...' and returned 21 | // as part of the encoded query parameters. 22 | // If the target parameter isn't found, the endpoint is returned 23 | // unmodified. 24 | func stripParam(param, endpoint string) string { 25 | u, err := url.Parse(endpoint) 26 | if err != nil { 27 | logger.Errorf("error attempting to strip %s: %s", param, err) 28 | return endpoint 29 | } 30 | 31 | if u.RawQuery != "" { 32 | values, err := url.ParseQuery(u.RawQuery) 33 | if err != nil { 34 | logger.Errorf("error attempting to strip %s: %s", param, err) 35 | return u.String() 36 | } 37 | 38 | if val := values.Get(param); val != "" { 39 | values.Set(param, val[:(len(val)/2)]+"...") 40 | u.RawQuery = values.Encode() 41 | return u.String() 42 | } 43 | } 44 | 45 | return endpoint 46 | } 47 | 48 | // validateToken returns true if token is valid 49 | func validateToken(ctx context.Context, p Provider, accessToken string, header http.Header) bool { 50 | if accessToken == "" || p.Data().ValidateURL == nil || p.Data().ValidateURL.String() == "" { 51 | return false 52 | } 53 | endpoint := p.Data().ValidateURL.String() 54 | if len(header) == 0 { 55 | params := url.Values{"access_token": {accessToken}} 56 | endpoint = endpoint + "?" + params.Encode() 57 | } 58 | 59 | result := requests.New(endpoint). 60 | WithContext(ctx). 61 | WithHeaders(header). 62 | Do() 63 | if result.Error() != nil { 64 | logger.Errorf("GET %s", stripToken(endpoint)) 65 | logger.Errorf("token validation request failed: %s", result.Error()) 66 | return false 67 | } 68 | 69 | logger.Printf("%d GET %s %s", result.StatusCode(), stripToken(endpoint), result.Body()) 70 | 71 | if result.StatusCode() == 200 { 72 | return true 73 | } 74 | logger.Errorf("token validation request failed: status %d - %s", result.StatusCode(), result.Body()) 75 | return false 76 | } 77 | -------------------------------------------------------------------------------- /pkg/app/pagewriter/templates.go: -------------------------------------------------------------------------------- 1 | package pagewriter 2 | 3 | import ( 4 | // Import embed to allow importing default page templates 5 | _ "embed" 6 | 7 | "fmt" 8 | "html/template" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 14 | ) 15 | 16 | const ( 17 | errorTemplateName = "error.html" 18 | signInTemplateName = "sign_in.html" 19 | ) 20 | 21 | //go:embed error.html 22 | var defaultErrorTemplate string 23 | 24 | //go:embed sign_in.html 25 | var defaultSignInTemplate string 26 | 27 | // loadTemplates adds the Sign In and Error templates from the custom template 28 | // directory, or uses the defaults if they do not exist or the custom directory 29 | // is not provided. 30 | func loadTemplates(customDir string) (*template.Template, error) { 31 | t := template.New("").Funcs(template.FuncMap{ 32 | "ToUpper": strings.ToUpper, 33 | "ToLower": strings.ToLower, 34 | }) 35 | var err error 36 | t, err = addTemplate(t, customDir, signInTemplateName, defaultSignInTemplate) 37 | if err != nil { 38 | return nil, fmt.Errorf("could not add Sign In template: %v", err) 39 | } 40 | t, err = addTemplate(t, customDir, errorTemplateName, defaultErrorTemplate) 41 | if err != nil { 42 | return nil, fmt.Errorf("could not add Error template: %v", err) 43 | } 44 | 45 | return t, nil 46 | } 47 | 48 | // addTemplate will add the template from the custom directory if provided, 49 | // else it will add the default template. 50 | func addTemplate(t *template.Template, customDir, fileName, defaultTemplate string) (*template.Template, error) { 51 | filePath := filepath.Join(customDir, fileName) 52 | if customDir != "" && isFile(filePath) { 53 | t, err := t.ParseFiles(filePath) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to parse template %s: %v", filePath, err) 56 | } 57 | return t, nil 58 | } 59 | t, err := t.Parse(defaultTemplate) 60 | if err != nil { 61 | // This should not happen. 62 | // Default templates should be tested and so should never fail to parse. 63 | logger.Panic("Could not parse defaultTemplate: ", err) 64 | } 65 | return t, nil 66 | } 67 | 68 | // isFile checks if the file exists and checks whether it is a regular file. 69 | // If either of these fail then it cannot be used as a template file. 70 | func isFile(fileName string) bool { 71 | info, err := os.Stat(fileName) 72 | if err != nil { 73 | logger.Errorf("Could not load file %s: %v, will use default template", fileName, err) 74 | return false 75 | } 76 | return info.Mode().IsRegular() 77 | } 78 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-6.1.x/features/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: endpoints 3 | title: Endpoints 4 | --- 5 | 6 | OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable. 7 | 8 | - /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info 9 | - /ping - returns a 200 OK response, which is intended for use with health checks 10 | - /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) 11 | - /oauth2/sign_out - this URL is used to clear the session cookie 12 | - /oauth2/start - a URL that will redirect to start the OAuth cycle 13 | - /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. 14 | - /oauth2/userinfo - the URL is used to return user's email from the session in JSON format. 15 | - /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](../configuration/overview.md#configuring-for-use-with-the-nginx-auth_request-directive) 16 | 17 | ### Sign out 18 | 19 | To sign the user out, redirect them to `/oauth2/sign_out`. This endpoint only removes oauth2-proxy's own cookies, i.e. the user is still logged in with the authentication provider and may automatically re-login when accessing the application again. You will also need to redirect the user to the authentication provider's sign out page afterwards using the `rd` query parameter, i.e. redirect the user to something like (notice the url-encoding!): 20 | 21 | ``` 22 | /oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page 23 | ``` 24 | 25 | Alternatively, include the redirect URL in the `X-Auth-Request-Redirect` header: 26 | 27 | ``` 28 | GET /oauth2/sign_out HTTP/1.1 29 | X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page 30 | ... 31 | ``` 32 | 33 | (The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.) 34 | 35 | BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored. 36 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.0.x/features/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: endpoints 3 | title: Endpoints 4 | --- 5 | 6 | OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable. 7 | 8 | - /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info 9 | - /ping - returns a 200 OK response, which is intended for use with health checks 10 | - /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) 11 | - /oauth2/sign_out - this URL is used to clear the session cookie 12 | - /oauth2/start - a URL that will redirect to start the OAuth cycle 13 | - /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. 14 | - /oauth2/userinfo - the URL is used to return user's email from the session in JSON format. 15 | - /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](../configuration/overview.md#configuring-for-use-with-the-nginx-auth_request-directive) 16 | 17 | ### Sign out 18 | 19 | To sign the user out, redirect them to `/oauth2/sign_out`. This endpoint only removes oauth2-proxy's own cookies, i.e. the user is still logged in with the authentication provider and may automatically re-login when accessing the application again. You will also need to redirect the user to the authentication provider's sign out page afterwards using the `rd` query parameter, i.e. redirect the user to something like (notice the url-encoding!): 20 | 21 | ``` 22 | /oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page 23 | ``` 24 | 25 | Alternatively, include the redirect URL in the `X-Auth-Request-Redirect` header: 26 | 27 | ``` 28 | GET /oauth2/sign_out HTTP/1.1 29 | X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page 30 | ... 31 | ``` 32 | 33 | (The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.) 34 | 35 | BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored. 36 | -------------------------------------------------------------------------------- /pkg/validation/allowlist.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 10 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip" 11 | ) 12 | 13 | func validateAllowlists(o *options.Options) []string { 14 | msgs := []string{} 15 | 16 | msgs = append(msgs, validateAuthRoutes(o)...) 17 | msgs = append(msgs, validateAuthRegexes(o)...) 18 | msgs = append(msgs, validateTrustedIPs(o)...) 19 | 20 | if len(o.TrustedIPs) > 0 && o.ReverseProxy { 21 | _, err := fmt.Fprintln(os.Stderr, "WARNING: mixing --trusted-ip with --reverse-proxy is a potential security vulnerability. An attacker can inject a trusted IP into an X-Real-IP or X-Forwarded-For header if they aren't properly protected outside of oauth2-proxy") 22 | if err != nil { 23 | panic(err) 24 | } 25 | } 26 | 27 | return msgs 28 | } 29 | 30 | // validateAuthRoutes validates method=path routes passed with options.SkipAuthRoutes 31 | func validateAuthRoutes(o *options.Options) []string { 32 | msgs := []string{} 33 | for _, route := range o.SkipAuthRoutes { 34 | var regex string 35 | parts := strings.SplitN(route, "=", 2) 36 | if len(parts) == 1 { 37 | regex = parts[0] 38 | } else { 39 | regex = parts[1] 40 | } 41 | _, err := regexp.Compile(regex) 42 | if err != nil { 43 | msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err)) 44 | } 45 | } 46 | return msgs 47 | } 48 | 49 | // validateRegex validates regex paths passed with options.SkipAuthRegex 50 | func validateAuthRegexes(o *options.Options) []string { 51 | return validateRegexes(o.SkipAuthRegex) 52 | } 53 | 54 | // validateTrustedIPs validates IP/CIDRs for IP based allowlists 55 | func validateTrustedIPs(o *options.Options) []string { 56 | msgs := []string{} 57 | for i, ipStr := range o.TrustedIPs { 58 | if nil == ip.ParseIPNet(ipStr) { 59 | msgs = append(msgs, fmt.Sprintf("trusted_ips[%d] (%s) could not be recognized", i, ipStr)) 60 | } 61 | } 62 | return msgs 63 | } 64 | 65 | // validateAPIRoutes validates regex paths passed with options.ApiRoutes 66 | func validateAPIRoutes(o *options.Options) []string { 67 | return validateRegexes(o.APIRoutes) 68 | } 69 | 70 | // validateRegexes validates all regexes and returns a list of messages in case of error 71 | func validateRegexes(regexes []string) []string { 72 | msgs := []string{} 73 | for _, regex := range regexes { 74 | _, err := regexp.Compile(regex) 75 | if err != nil { 76 | msgs = append(msgs, fmt.Sprintf("error compiling regex /%s/: %v", regex, err)) 77 | } 78 | } 79 | return msgs 80 | } 81 | -------------------------------------------------------------------------------- /pkg/apis/options/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path" 7 | 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("GetSecretValue", func() { 14 | var fileDir string 15 | const secretEnvKey = "SECRET_ENV_KEY" 16 | const secretEnvValue = "secret-env-value" 17 | var secretFileValue = []byte("secret-file-value") 18 | 19 | BeforeEach(func() { 20 | os.Setenv(secretEnvKey, secretEnvValue) 21 | 22 | var err error 23 | fileDir, err = ioutil.TempDir("", "oauth2-proxy-util-get-secret-value") 24 | Expect(err).ToNot(HaveOccurred()) 25 | Expect(ioutil.WriteFile(path.Join(fileDir, "secret-file"), secretFileValue, 0600)).To(Succeed()) 26 | }) 27 | 28 | AfterEach(func() { 29 | os.Unsetenv(secretEnvKey) 30 | os.RemoveAll(fileDir) 31 | }) 32 | 33 | It("returns the correct value from the string value", func() { 34 | value, err := GetSecretValue(&options.SecretSource{ 35 | Value: []byte("secret-value-1"), 36 | }) 37 | Expect(err).ToNot(HaveOccurred()) 38 | Expect(string(value)).To(Equal("secret-value-1")) 39 | }) 40 | 41 | It("returns the correct value from the environment", func() { 42 | value, err := GetSecretValue(&options.SecretSource{ 43 | FromEnv: secretEnvKey, 44 | }) 45 | Expect(err).ToNot(HaveOccurred()) 46 | Expect(value).To(BeEquivalentTo(secretEnvValue)) 47 | }) 48 | 49 | It("returns the correct value from a file", func() { 50 | value, err := GetSecretValue(&options.SecretSource{ 51 | FromFile: path.Join(fileDir, "secret-file"), 52 | }) 53 | Expect(err).ToNot(HaveOccurred()) 54 | Expect(value).To(Equal(secretFileValue)) 55 | }) 56 | 57 | It("when the file does not exist", func() { 58 | value, err := GetSecretValue(&options.SecretSource{ 59 | FromFile: path.Join(fileDir, "not-exist"), 60 | }) 61 | Expect(err).To(HaveOccurred()) 62 | Expect(value).To(BeEmpty()) 63 | }) 64 | 65 | It("with no source set", func() { 66 | value, err := GetSecretValue(&options.SecretSource{}) 67 | Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")) 68 | Expect(value).To(BeEmpty()) 69 | }) 70 | 71 | It("with multiple sources set", func() { 72 | value, err := GetSecretValue(&options.SecretSource{ 73 | FromEnv: secretEnvKey, 74 | FromFile: path.Join(fileDir, "secret-file"), 75 | }) 76 | Expect(err).To(MatchError("secret source is invalid: exactly one entry required, specify either value, fromEnv or fromFile")) 77 | Expect(value).To(BeEmpty()) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /contrib/local-environment/kubernetes/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | @echo "Usage:" 3 | @echo " make create-cluster" 4 | @echo " make deploy" 5 | 6 | # create kind cluster with nginx-ingress as the most popular ingress controller for K8S 7 | .PHONY: deploy 8 | create-cluster: 9 | kind create cluster --name oauth2-proxy --config kind-cluster.yaml 10 | make setup-dns 11 | make setup-ingress 12 | 13 | .PHONY: setup-ingress 14 | setup-ingress: 15 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/kind/deploy.yaml 16 | kubectl --namespace ingress-nginx rollout status --timeout 5m deployment/ingress-nginx-controller 17 | 18 | # default Pod CIDR is 10.244.0.0/16 https://github.com/kubernetes-sigs/kind/blob/a6e8108025bc7a9440beedb8ef7714aec84fe87e/pkg/apis/config/v1alpha4/default.go#L52 19 | # what makes cluster host IP equal to 10.244.0.1 20 | # thus we add dex.localtest.me and oauth2-proxy.localtest.me stub hosts pointing to this IP 21 | # NOT NEEDED IN REAL LIFE! 22 | .PHONY: setup-dns 23 | setup-dns: 24 | kubectl apply -f custom-dns.yaml 25 | kubectl -n kube-system rollout restart deployment/coredns 26 | kubectl -n kube-system rollout status --timeout 5m deployment/coredns 27 | 28 | .PHONY: delete-cluster 29 | delete-cluster: 30 | kind delete cluster --name oauth2-proxy 31 | 32 | .PHONY: deploy 33 | deploy: 34 | kubectl apply -f oauth2-proxy-example-full.yaml 35 | kubectl rollout status --timeout 5m deployment/oauth2-proxy-example-oauth2-proxy-sample 36 | kubectl rollout status --timeout 1m deployment/oauth2-proxy-example-httpbin 37 | kubectl rollout status --timeout 1m deployment/oauth2-proxy-example-hello-world 38 | 39 | .PHONY: undeploy 40 | undeploy: 41 | kubectl delete -f oauth2-proxy-example-full.yaml 42 | 43 | ###################### 44 | ###### HELM CMDs ##### 45 | ###################### 46 | .PHONY: helm-init 47 | helm-init: 48 | helm dep update 49 | 50 | # unpacking is useful to be able to explore underlying helm charts 51 | .PHONY: helm-unpack 52 | helm-unpack: 53 | cd charts; for f in *.tgz; do tar -zxf "$$f"; done 54 | 55 | .PHONY: helm-deploy 56 | helm-deploy: helm-init 57 | helm upgrade --wait --debug --install --render-subchart-notes oauth2-proxy-example . 58 | 59 | .PHONY: helm-undeploy 60 | helm-undeploy: 61 | helm del oauth2-proxy-example 62 | 63 | # creates K8S manifest from helm chart 64 | .PHONY: helm-create-manifest 65 | helm-create-manifest: helm-init 66 | echo "# WARNING: This file is auto-generated by 'make helm-create-manifest'! DO NOT EDIT MANUALLY!" > oauth2-proxy-example-full.yaml 67 | helm template --namespace default oauth2-proxy-example . >> oauth2-proxy-example-full.yaml 68 | -------------------------------------------------------------------------------- /pkg/upstream/static_test.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/ginkgo/extensions/table" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("Static Response Suite", func() { 16 | const authenticated = "Authenticated" 17 | var id string 18 | 19 | BeforeEach(func() { 20 | // Generate a random id before each test to check the GAP-Upstream-Address 21 | // is being set correctly 22 | idBytes := make([]byte, 16) 23 | _, err := io.ReadFull(rand.Reader, idBytes) 24 | Expect(err).ToNot(HaveOccurred()) 25 | id = string(idBytes) 26 | }) 27 | 28 | type serveHTTPTableInput struct { 29 | requestPath string 30 | staticCode int 31 | expectedBody string 32 | expectedCode int 33 | } 34 | 35 | DescribeTable("staticResponse ServeHTTP", 36 | func(in *serveHTTPTableInput) { 37 | var code *int 38 | if in.staticCode != 0 { 39 | code = &in.staticCode 40 | } 41 | handler := newStaticResponseHandler(id, code) 42 | 43 | req := httptest.NewRequest("", in.requestPath, nil) 44 | req = middlewareapi.AddRequestScope(req, &middlewareapi.RequestScope{}) 45 | 46 | rw := httptest.NewRecorder() 47 | handler.ServeHTTP(rw, req) 48 | 49 | scope := middlewareapi.GetRequestScope(req) 50 | Expect(scope.Upstream).To(Equal(id)) 51 | 52 | Expect(rw.Code).To(Equal(in.expectedCode)) 53 | Expect(rw.Body.String()).To(Equal(in.expectedBody)) 54 | }, 55 | Entry("with no given code", &serveHTTPTableInput{ 56 | requestPath: "/", 57 | staticCode: 0, // Placeholder for nil 58 | expectedBody: authenticated, 59 | expectedCode: http.StatusOK, 60 | }), 61 | Entry("with status OK", &serveHTTPTableInput{ 62 | requestPath: "/abc", 63 | staticCode: http.StatusOK, 64 | expectedBody: authenticated, 65 | expectedCode: http.StatusOK, 66 | }), 67 | Entry("with status NoContent", &serveHTTPTableInput{ 68 | requestPath: "/def", 69 | staticCode: http.StatusNoContent, 70 | expectedBody: authenticated, 71 | expectedCode: http.StatusNoContent, 72 | }), 73 | Entry("with status NotFound", &serveHTTPTableInput{ 74 | requestPath: "/ghi", 75 | staticCode: http.StatusNotFound, 76 | expectedBody: authenticated, 77 | expectedCode: http.StatusNotFound, 78 | }), 79 | Entry("with status Teapot", &serveHTTPTableInput{ 80 | requestPath: "/jkl", 81 | staticCode: http.StatusTeapot, 82 | expectedBody: authenticated, 83 | expectedCode: http.StatusTeapot, 84 | }), 85 | ) 86 | }) 87 | -------------------------------------------------------------------------------- /providers/nextcloud.go: -------------------------------------------------------------------------------- 1 | package providers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 8 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" 9 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 10 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests" 11 | ) 12 | 13 | // NextcloudProvider represents an Nextcloud based Identity Provider 14 | type NextcloudProvider struct { 15 | *ProviderData 16 | } 17 | 18 | var _ Provider = (*NextcloudProvider)(nil) 19 | 20 | const nextCloudProviderName = "Nextcloud" 21 | 22 | // NewNextcloudProvider initiates a new NextcloudProvider 23 | func NewNextcloudProvider(p *ProviderData) *NextcloudProvider { 24 | p.ProviderName = nextCloudProviderName 25 | p.getAuthorizationHeaderFunc = makeOIDCHeader 26 | if p.EmailClaim == options.OIDCEmailClaim { 27 | // This implies the email claim has not been overridden, we should set a default 28 | // for this provider 29 | p.EmailClaim = "ocs.data.email" 30 | } 31 | return &NextcloudProvider{ProviderData: p} 32 | } 33 | 34 | // EnrichSession uses the Nextcloud userinfo endpoint to populate 35 | // the session's email, user, and groups. 36 | func (p *NextcloudProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { 37 | // Fallback to ValidateURL if ProfileURL not set for legacy compatibility 38 | profileURL := p.ValidateURL.String() 39 | if p.ProfileURL.String() != "" { 40 | profileURL = p.ProfileURL.String() 41 | } 42 | 43 | json, err := requests.New(profileURL). 44 | WithContext(ctx). 45 | SetHeader("Authorization", "Bearer "+s.AccessToken). 46 | Do(). 47 | UnmarshalJSON() 48 | if err != nil { 49 | logger.Errorf("failed making request %v", err) 50 | return err 51 | } 52 | 53 | groups, err := json.GetPath("ocs", "data", "groups").StringArray() 54 | if err == nil { 55 | for _, group := range groups { 56 | if group != "" { 57 | s.Groups = append(s.Groups, group) 58 | } 59 | } 60 | } 61 | 62 | user, err := json.GetPath("ocs", "data", "id").String() 63 | if err != nil { 64 | return fmt.Errorf("unable to extract id from userinfo endpoint: %v", err) 65 | } 66 | s.User = user 67 | 68 | email, err := json.GetPath("ocs", "data", "email").String() 69 | if err != nil { 70 | return fmt.Errorf("unable to extract email from userinfo endpoint: %v", err) 71 | } 72 | s.Email = email 73 | 74 | return nil 75 | } 76 | 77 | // ValidateSession validates the AccessToken 78 | func (p *NextcloudProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool { 79 | return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken)) 80 | } 81 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.1.x/features/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: endpoints 3 | title: Endpoints 4 | --- 5 | 6 | OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable. 7 | 8 | - /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info 9 | - /ping - returns a 200 OK response, which is intended for use with health checks 10 | - /metrics - Metrics endpoint for Prometheus to scrape, serve on the address specified by `--metrics-address`, disabled by default 11 | - /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) 12 | - /oauth2/sign_out - this URL is used to clear the session cookie 13 | - /oauth2/start - a URL that will redirect to start the OAuth cycle 14 | - /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. 15 | - /oauth2/userinfo - the URL is used to return user's email from the session in JSON format. 16 | - /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](../configuration/overview.md#configuring-for-use-with-the-nginx-auth_request-directive) 17 | 18 | ### Sign out 19 | 20 | To sign the user out, redirect them to `/oauth2/sign_out`. This endpoint only removes oauth2-proxy's own cookies, i.e. the user is still logged in with the authentication provider and may automatically re-login when accessing the application again. You will also need to redirect the user to the authentication provider's sign out page afterwards using the `rd` query parameter, i.e. redirect the user to something like (notice the url-encoding!): 21 | 22 | ``` 23 | /oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page 24 | ``` 25 | 26 | Alternatively, include the redirect URL in the `X-Auth-Request-Redirect` header: 27 | 28 | ``` 29 | GET /oauth2/sign_out HTTP/1.1 30 | X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page 31 | ... 32 | ``` 33 | 34 | (The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.) 35 | 36 | BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored. 37 | -------------------------------------------------------------------------------- /docs/versioned_docs/version-7.2.x/features/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: endpoints 3 | title: Endpoints 4 | --- 5 | 6 | OAuth2 Proxy responds directly to the following endpoints. All other endpoints will be proxied upstream when authenticated. The `/oauth2` prefix can be changed with the `--proxy-prefix` config variable. 7 | 8 | - /robots.txt - returns a 200 OK response that disallows all User-agents from all paths; see [robotstxt.org](http://www.robotstxt.org/) for more info 9 | - /ping - returns a 200 OK response, which is intended for use with health checks 10 | - /metrics - Metrics endpoint for Prometheus to scrape, serve on the address specified by `--metrics-address`, disabled by default 11 | - /oauth2/sign_in - the login page, which also doubles as a sign out page (it clears cookies) 12 | - /oauth2/sign_out - this URL is used to clear the session cookie 13 | - /oauth2/start - a URL that will redirect to start the OAuth cycle 14 | - /oauth2/callback - the URL used at the end of the OAuth cycle. The oauth app will be configured with this as the callback url. 15 | - /oauth2/userinfo - the URL is used to return user's email from the session in JSON format. 16 | - /oauth2/auth - only returns a 202 Accepted response or a 401 Unauthorized response; for use with the [Nginx `auth_request` directive](../configuration/overview.md#configuring-for-use-with-the-nginx-auth_request-directive) 17 | 18 | ### Sign out 19 | 20 | To sign the user out, redirect them to `/oauth2/sign_out`. This endpoint only removes oauth2-proxy's own cookies, i.e. the user is still logged in with the authentication provider and may automatically re-login when accessing the application again. You will also need to redirect the user to the authentication provider's sign out page afterwards using the `rd` query parameter, i.e. redirect the user to something like (notice the url-encoding!): 21 | 22 | ``` 23 | /oauth2/sign_out?rd=https%3A%2F%2Fmy-oidc-provider.example.com%2Fsign_out_page 24 | ``` 25 | 26 | Alternatively, include the redirect URL in the `X-Auth-Request-Redirect` header: 27 | 28 | ``` 29 | GET /oauth2/sign_out HTTP/1.1 30 | X-Auth-Request-Redirect: https://my-oidc-provider/sign_out_page 31 | ... 32 | ``` 33 | 34 | (The "sign_out_page" should be the [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0.html#rfc.section.2.1) from [the metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) if your OIDC provider supports Session Management and Discovery.) 35 | 36 | BEWARE that the domain you want to redirect to (`my-oidc-provider.example.com` in the example) must be added to the [`--whitelist-domain`](../configuration/overview) configuration option otherwise the redirect will be ignored. 37 | -------------------------------------------------------------------------------- /pkg/requests/result.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/bitly/go-simplejson" 9 | ) 10 | 11 | // Result is the result of a request created by a Builder 12 | type Result interface { 13 | Error() error 14 | StatusCode() int 15 | Headers() http.Header 16 | Body() []byte 17 | UnmarshalInto(interface{}) error 18 | UnmarshalJSON() (*simplejson.Json, error) 19 | } 20 | 21 | type result struct { 22 | err error 23 | response *http.Response 24 | body []byte 25 | } 26 | 27 | // Error returns an error from the result if present 28 | func (r *result) Error() error { 29 | return r.err 30 | } 31 | 32 | // StatusCode returns the response's status code 33 | func (r *result) StatusCode() int { 34 | if r.response != nil { 35 | return r.response.StatusCode 36 | } 37 | return 0 38 | } 39 | 40 | // Headers returns the response's headers 41 | func (r *result) Headers() http.Header { 42 | if r.response != nil { 43 | return r.response.Header 44 | } 45 | return nil 46 | } 47 | 48 | // Body returns the response's body 49 | func (r *result) Body() []byte { 50 | return r.body 51 | } 52 | 53 | // UnmarshalInto attempts to unmarshal the response into the given interface. 54 | // The response body is assumed to be JSON. 55 | // The response must have a 200 status otherwise an error will be returned. 56 | func (r *result) UnmarshalInto(into interface{}) error { 57 | body, err := r.getBodyForUnmarshal() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if err := json.Unmarshal(body, into); err != nil { 63 | return fmt.Errorf("error unmarshalling body: %v", err) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // UnmarshalJSON performs the request and attempts to unmarshal the response into a 70 | // simplejson.Json. The response body is assume to be JSON. 71 | // The response must have a 200 status otherwise an error will be returned. 72 | func (r *result) UnmarshalJSON() (*simplejson.Json, error) { 73 | body, err := r.getBodyForUnmarshal() 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | data, err := simplejson.NewJson(body) 79 | if err != nil { 80 | return nil, fmt.Errorf("error reading json: %v", err) 81 | } 82 | return data, nil 83 | } 84 | 85 | // getBodyForUnmarshal returns the body if there wasn't an error and the status 86 | // code was 200. 87 | func (r *result) getBodyForUnmarshal() ([]byte, error) { 88 | if r.Error() != nil { 89 | return nil, r.Error() 90 | } 91 | 92 | // Only unmarshal body if the response was successful 93 | if r.StatusCode() != http.StatusOK { 94 | return nil, fmt.Errorf("unexpected status \"%d\": %s", r.StatusCode(), r.Body()) 95 | } 96 | 97 | return r.Body(), nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/apis/options/common_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "time" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/ginkgo/extensions/table" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Common", func() { 14 | Context("Duration", func() { 15 | type marshalJSONTableInput struct { 16 | duration Duration 17 | expectedJSON string 18 | } 19 | 20 | DescribeTable("MarshalJSON", 21 | func(in marshalJSONTableInput) { 22 | data, err := in.duration.MarshalJSON() 23 | Expect(err).ToNot(HaveOccurred()) 24 | Expect(string(data)).To(Equal(in.expectedJSON)) 25 | 26 | var d Duration 27 | Expect(json.Unmarshal(data, &d)).To(Succeed()) 28 | Expect(d).To(Equal(in.duration)) 29 | }, 30 | Entry("30 seconds", marshalJSONTableInput{ 31 | duration: Duration(30 * time.Second), 32 | expectedJSON: "\"30s\"", 33 | }), 34 | Entry("1 minute", marshalJSONTableInput{ 35 | duration: Duration(1 * time.Minute), 36 | expectedJSON: "\"1m0s\"", 37 | }), 38 | Entry("1 hour 15 minutes", marshalJSONTableInput{ 39 | duration: Duration(75 * time.Minute), 40 | expectedJSON: "\"1h15m0s\"", 41 | }), 42 | Entry("A zero Duration", marshalJSONTableInput{ 43 | duration: Duration(0), 44 | expectedJSON: "\"0s\"", 45 | }), 46 | ) 47 | 48 | type unmarshalJSONTableInput struct { 49 | json string 50 | expectedErr error 51 | expectedDuration Duration 52 | } 53 | 54 | DescribeTable("UnmarshalJSON", 55 | func(in unmarshalJSONTableInput) { 56 | // A duration must be initialised pointer before UnmarshalJSON will work. 57 | zero := Duration(0) 58 | d := &zero 59 | 60 | err := d.UnmarshalJSON([]byte(in.json)) 61 | if in.expectedErr != nil { 62 | Expect(err).To(MatchError(in.expectedErr.Error())) 63 | } else { 64 | Expect(err).ToNot(HaveOccurred()) 65 | } 66 | Expect(d).ToNot(BeNil()) 67 | Expect(*d).To(Equal(in.expectedDuration)) 68 | }, 69 | Entry("1m", unmarshalJSONTableInput{ 70 | json: "\"1m\"", 71 | expectedDuration: Duration(1 * time.Minute), 72 | }), 73 | Entry("30s", unmarshalJSONTableInput{ 74 | json: "\"30s\"", 75 | expectedDuration: Duration(30 * time.Second), 76 | }), 77 | Entry("1h15m", unmarshalJSONTableInput{ 78 | json: "\"1h15m\"", 79 | expectedDuration: Duration(75 * time.Minute), 80 | }), 81 | Entry("am", unmarshalJSONTableInput{ 82 | json: "\"am\"", 83 | expectedErr: errors.New("time: invalid duration \"am\""), 84 | expectedDuration: Duration(0), 85 | }), 86 | ) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /pkg/validation/providers_test.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 5 | . "github.com/onsi/ginkgo" 6 | . "github.com/onsi/ginkgo/extensions/table" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("Providers", func() { 11 | type validateProvidersTableInput struct { 12 | options *options.Options 13 | errStrings []string 14 | } 15 | 16 | validProvider := options.Provider{ 17 | ID: "ProviderID", 18 | ClientID: "ClientID", 19 | ClientSecret: "ClientSecret", 20 | } 21 | 22 | validLoginGovProvider := options.Provider{ 23 | Type: "login.gov", 24 | ID: "ProviderIDLoginGov", 25 | ClientID: "ClientID", 26 | ClientSecret: "ClientSecret", 27 | } 28 | 29 | missingIDProvider := options.Provider{ 30 | ClientID: "ClientID", 31 | ClientSecret: "ClientSecret", 32 | } 33 | 34 | missingProvider := "at least one provider has to be defined" 35 | emptyIDMsg := "provider has empty id: ids are required for all providers" 36 | duplicateProviderIDMsg := "multiple providers found with id ProviderID: provider ids must be unique" 37 | skipButtonAndMultipleProvidersMsg := "SkipProviderButton and multiple providers are mutually exclusive" 38 | 39 | DescribeTable("validateProviders", 40 | func(o *validateProvidersTableInput) { 41 | Expect(validateProviders(o.options)).To(ConsistOf(o.errStrings)) 42 | }, 43 | Entry("with no providers", &validateProvidersTableInput{ 44 | options: &options.Options{}, 45 | errStrings: []string{missingProvider}, 46 | }), 47 | Entry("with valid providers", &validateProvidersTableInput{ 48 | options: &options.Options{ 49 | Providers: options.Providers{ 50 | validProvider, 51 | validLoginGovProvider, 52 | }, 53 | }, 54 | errStrings: []string{}, 55 | }), 56 | Entry("with an empty providerID", &validateProvidersTableInput{ 57 | options: &options.Options{ 58 | Providers: options.Providers{ 59 | missingIDProvider, 60 | }, 61 | }, 62 | errStrings: []string{emptyIDMsg}, 63 | }), 64 | Entry("with same providerID", &validateProvidersTableInput{ 65 | options: &options.Options{ 66 | Providers: options.Providers{ 67 | validProvider, 68 | validProvider, 69 | }, 70 | }, 71 | errStrings: []string{duplicateProviderIDMsg}, 72 | }), 73 | Entry("with multiple providers and skip provider button", &validateProvidersTableInput{ 74 | options: &options.Options{ 75 | SkipProviderButton: true, 76 | Providers: options.Providers{ 77 | validProvider, 78 | validLoginGovProvider, 79 | }, 80 | }, 81 | errStrings: []string{skipButtonAndMultipleProvidersMsg}, 82 | }), 83 | ) 84 | }) 85 | -------------------------------------------------------------------------------- /pkg/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/fsnotify/fsnotify" 10 | 11 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 12 | ) 13 | 14 | // WatchFileForUpdates performs an action every time a file on disk is updated 15 | func WatchFileForUpdates(filename string, done <-chan bool, action func()) error { 16 | filename = filepath.Clean(filename) 17 | watcher, err := fsnotify.NewWatcher() 18 | if err != nil { 19 | return fmt.Errorf("failed to create watcher for '%s': %s", filename, err) 20 | } 21 | 22 | go func() { 23 | defer watcher.Close() 24 | 25 | for { 26 | select { 27 | case <-done: 28 | logger.Printf("shutting down watcher for: %s", filename) 29 | return 30 | case event := <-watcher.Events: 31 | filterEvent(watcher, event, filename, action) 32 | case err = <-watcher.Errors: 33 | logger.Errorf("error watching '%s': %s", filename, err) 34 | } 35 | } 36 | }() 37 | if err := watcher.Add(filename); err != nil { 38 | return fmt.Errorf("failed to add '%s' to watcher: %v", filename, err) 39 | } 40 | logger.Printf("watching '%s' for updates", filename) 41 | 42 | return nil 43 | } 44 | 45 | // Filter file operations based on the events sent by the watcher. 46 | // Execute the action() function when the following conditions are met: 47 | // - the real path of the file was changed (Kubernetes ConfigMap/Secret) 48 | // - the file is modified or created 49 | func filterEvent(watcher *fsnotify.Watcher, event fsnotify.Event, filename string, action func()) { 50 | switch filepath.Clean(event.Name) == filename { 51 | // In Kubernetes the file path is a symlink, so we should take action 52 | // when the ConfigMap/Secret is replaced. 53 | case event.Op&fsnotify.Remove != 0: 54 | logger.Printf("watching interrupted on event: %s", event) 55 | WaitForReplacement(filename, event.Op, watcher) 56 | action() 57 | case event.Op&(fsnotify.Create|fsnotify.Write) != 0: 58 | logger.Printf("reloading after event: %s", event) 59 | action() 60 | } 61 | } 62 | 63 | // WaitForReplacement waits for a file to exist on disk and then starts a watch 64 | // for the file 65 | func WaitForReplacement(filename string, op fsnotify.Op, watcher *fsnotify.Watcher) { 66 | const sleepInterval = 50 * time.Millisecond 67 | 68 | // Avoid a race when fsnofity.Remove is preceded by fsnotify.Chmod. 69 | if op&fsnotify.Chmod != 0 { 70 | time.Sleep(sleepInterval) 71 | } 72 | for { 73 | if _, err := os.Stat(filename); err == nil { 74 | if err := watcher.Add(filename); err == nil { 75 | logger.Printf("watching resumed for '%s'", filename) 76 | return 77 | } 78 | } 79 | time.Sleep(sleepInterval) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/cookies/cookies.go: -------------------------------------------------------------------------------- 1 | package cookies 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "strings" 8 | "time" 9 | 10 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" 11 | "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" 12 | requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util" 13 | ) 14 | 15 | // MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions, 16 | // value and creation time 17 | func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration, now time.Time) *http.Cookie { 18 | domain := GetCookieDomain(req, opts.Domains) 19 | // If nothing matches, create the cookie with the shortest domain 20 | if domain == "" && len(opts.Domains) > 0 { 21 | logger.Errorf("Warning: request host %q did not match any of the specific cookie domains of %q", 22 | requestutil.GetRequestHost(req), 23 | strings.Join(opts.Domains, ","), 24 | ) 25 | domain = opts.Domains[len(opts.Domains)-1] 26 | } 27 | 28 | c := &http.Cookie{ 29 | Name: name, 30 | Value: value, 31 | Path: opts.Path, 32 | Domain: domain, 33 | Expires: now.Add(expiration), 34 | HttpOnly: opts.HTTPOnly, 35 | Secure: opts.Secure, 36 | SameSite: ParseSameSite(opts.SameSite), 37 | } 38 | 39 | warnInvalidDomain(c, req) 40 | 41 | return c 42 | } 43 | 44 | // GetCookieDomain returns the correct cookie domain given a list of domains 45 | // by checking the X-Fowarded-Host and host header of an an http request 46 | func GetCookieDomain(req *http.Request, cookieDomains []string) string { 47 | host := requestutil.GetRequestHost(req) 48 | for _, domain := range cookieDomains { 49 | if strings.HasSuffix(host, domain) { 50 | return domain 51 | } 52 | } 53 | return "" 54 | } 55 | 56 | // Parse a valid http.SameSite value from a user supplied string for use of making cookies. 57 | func ParseSameSite(v string) http.SameSite { 58 | switch v { 59 | case "lax": 60 | return http.SameSiteLaxMode 61 | case "strict": 62 | return http.SameSiteStrictMode 63 | case "none": 64 | return http.SameSiteNoneMode 65 | case "": 66 | return 0 67 | default: 68 | panic(fmt.Sprintf("Invalid value for SameSite: %s", v)) 69 | } 70 | } 71 | 72 | // warnInvalidDomain logs a warning if the request host and cookie domain are 73 | // mismatched. 74 | func warnInvalidDomain(c *http.Cookie, req *http.Request) { 75 | if c.Domain == "" { 76 | return 77 | } 78 | 79 | host := requestutil.GetRequestHost(req) 80 | if h, _, err := net.SplitHostPort(host); err == nil { 81 | host = h 82 | } 83 | if !strings.HasSuffix(host, c.Domain) { 84 | logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", host, c.Domain) 85 | } 86 | } 87 | --------------------------------------------------------------------------------