git.iko.soy/typeable/oauth2-proxy
on master help sign in
CHANGELOG.md+27
since splitsnapshots
unifiedsplit
@@ -6,8 +6,35 @@
6
6
7## Breaking Changes
7## Breaking Changes
8
8
9+## Changes since v7.15.4
10+
11+# V7.15.4
12+
13+## Release Highlights
14+
15+- ๐Ÿ”ต Golang version upgrade to v1.26.7
16+ - Upgrade of all dependencies to their latest versions
17+- ๐Ÿ•ต๏ธโ€โ™€๏ธ Vulnerabilities have been addressed
18+ - [CVE-2026-33818](https://nvd.nist.gov/vuln/detail/CVE-2026-33818)
19+ - [CVE-2026-39821](https://nvd.nist.gov/vuln/detail/CVE-2026-39821)
20+ - [CVE-2026-39822](https://nvd.nist.gov/vuln/detail/CVE-2026-39822)
21+ - [CVE-2026-42505](https://nvd.nist.gov/vuln/detail/CVE-2026-42505)
22+ - [CVE-2026-46600](https://nvd.nist.gov/vuln/detail/CVE-2026-46600)
23+ - [CVE-2026-56852](https://nvd.nist.gov/vuln/detail/CVE-2026-56852)
24+ - [CVE-2026-56853](https://nvd.nist.gov/vuln/detail/CVE-2026-56853)
25+ - [CVE-2026-56858](https://nvd.nist.gov/vuln/detail/CVE-2026-56858)
26+ - [CVE-2026-56859](https://nvd.nist.gov/vuln/detail/CVE-2026-56859)
27+ - [CVE-2026-56860](https://nvd.nist.gov/vuln/detail/CVE-2026-56860)
28+ - [CVE-2026-56862](https://nvd.nist.gov/vuln/detail/CVE-2026-56862)
29+
30+## Important Notes
31+
32+## Breaking Changes
33+
9## Changes since v7.15.3
34## Changes since v7.15.3
10
35
36+- [#3493](https://github.com/oauth2-proxy/oauth2-proxy/pull/3493) chore(deps): update gomod @tuunit
37+
11# V7.15.3
38# V7.15.3
12
39
13## Release Highlights
40## Release Highlights
contrib/local-environment/Makefile+2โˆ’2
@@ -49,8 +49,8 @@ kubernetes-down:
49
49
50.PHONY: traefik-up
50.PHONY: traefik-up
51traefik-up:
51traefik-up:
52โˆ’ docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml up -d
52+ docker compose -f docker-compose-traefik.yaml up -d
53
53
54.PHONY: traefik-%
54.PHONY: traefik-%
55traefik-%:
55traefik-%:
56โˆ’ docker compose -f docker-compose.yaml -f docker-compose-traefik.yaml $*
56+ docker compose -f docker-compose-traefik.yaml $*
contrib/local-environment/dex-traefik.yamlnew file+25
@@ -0,0 +1,25 @@
1+# This configuration is intended to be used with the Traefik local environment.
2+# Dex uses in-memory storage because this environment is disposable.
3+issuer: http://dex.localtest.me/dex
4+storage:
5+ type: memory
6+web:
7+ http: 0.0.0.0:5556
8+oauth2:
9+ skipApprovalScreen: true
10+expiry:
11+ signingKeys: "4h"
12+ idTokens: "1h"
13+staticClients:
14+ - id: oauth2-proxy
15+ redirectURIs:
16+ - "http://oauth2-proxy.localtest.me/oauth2/callback"
17+ name: "OAuth2 Proxy"
18+ secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
19+enablePasswordDB: true
20+staticPasswords:
21+ - email: "admin@example.com"
22+ # bcrypt hash of the string "password"
23+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
24+ username: "admin"
25+ userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"
contrib/local-environment/dex.yaml+1โˆ’6
@@ -3,11 +3,7 @@
3# This should configure Dex to run on port 5556 and provides a static login
3# This should configure Dex to run on port 5556 and provides a static login
4issuer: http://dex.localtest.me:5556/dex
4issuer: http://dex.localtest.me:5556/dex
5storage:
5storage:
6โˆ’ type: etcd
6+ type: memory
7โˆ’ config:
8โˆ’ endpoints:
9โˆ’ - http://etcd:2379
10โˆ’ namespace: dex/
11web:
7web:
12 http: 0.0.0.0:5556
8 http: 0.0.0.0:5556
13oauth2:
9oauth2:
@@ -21,7 +17,6 @@ staticClients:
21 # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
17 # These redirect URIs point to the `--redirect-url` for OAuth2 proxy.
22 - "http://oauth2-proxy.localtest.me:4180/oauth2/callback" # For basic proxy example.
18 - "http://oauth2-proxy.localtest.me:4180/oauth2/callback" # For basic proxy example.
23 - "http://oauth2-proxy.localtest.me:8080/oauth2/callback" # For nginx example.
19 - "http://oauth2-proxy.localtest.me:8080/oauth2/callback" # For nginx example.
24โˆ’ - "http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback" # For traefik example.
25 name: "OAuth2 Proxy"
20 name: "OAuth2 Proxy"
26 secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
21 secret: b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK
27enablePasswordDB: true
22enablePasswordDB: true
contrib/local-environment/docker-compose-alpha-config.yaml+2โˆ’15
@@ -1,7 +1,7 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
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.
2# for manual testing and exploration of features.
3# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
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.
4+# HTTPBin as an example upstream.
5# This file also uses alpha configuration when configuring OAuth2 Proxy.
5# This file also uses alpha configuration when configuring OAuth2 Proxy.
6#
6#
7# This file is an extension of the main compose file and must be used with it
7# This file is an extension of the main compose file and must be used with it
@@ -14,7 +14,7 @@ version: "3.0"
14services:
14services:
15 oauth2-proxy:
15 oauth2-proxy:
16 container_name: oauth2-proxy
16 container_name: oauth2-proxy
17โˆ’ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
18 command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
18 command: --config /oauth2-proxy.cfg --alpha-config /oauth2-proxy-alpha-config.yaml
19 hostname: oauth2-proxy
19 hostname: oauth2-proxy
20 volumes:
20 volumes:
@@ -43,25 +43,12 @@ services:
43 dex:
43 dex:
44 aliases:
44 aliases:
45 - dex.localtest.me
45 - dex.localtest.me
46โˆ’ etcd: {}
47โˆ’ depends_on:
48โˆ’ - etcd
49 httpbin:
46 httpbin:
50 container_name: httpbin
47 container_name: httpbin
51 image: kennethreitz/httpbin
48 image: kennethreitz/httpbin
52 ports: []
49 ports: []
53 networks:
50 networks:
54 httpbin: {}
51 httpbin: {}
55โˆ’ etcd:
56โˆ’ container_name: etcd
57โˆ’ image: gcr.io/etcd-development/etcd:v3.7.1
58โˆ’ entrypoint: /usr/local/bin/etcd
59โˆ’ command:
60โˆ’ - --listen-client-urls=http://0.0.0.0:2379
61โˆ’ - --advertise-client-urls=http://etcd:2379
62โˆ’ networks:
63โˆ’ etcd: {}
64networks:
52networks:
65 dex: {}
53 dex: {}
66โˆ’ etcd: {}
67 httpbin: {}
54 httpbin: {}
contrib/local-environment/docker-compose-gitea.yaml+1โˆ’1
@@ -14,7 +14,7 @@ version: '3.0'
14services:
14services:
15 oauth2-proxy:
15 oauth2-proxy:
16 container_name: oauth2-proxy
16 container_name: oauth2-proxy
17โˆ’ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
18 command: --config /oauth2-proxy.cfg
18 command: --config /oauth2-proxy.cfg
19 hostname: oauth2-proxy
19 hostname: oauth2-proxy
20 volumes:
20 volumes:
contrib/local-environment/docker-compose-keycloak.yaml+2โˆ’2
@@ -14,7 +14,7 @@ version: "3.0"
14services:
14services:
15 oauth2-proxy:
15 oauth2-proxy:
16 container_name: oauth2-proxy
16 container_name: oauth2-proxy
17โˆ’ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
17+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
18 command: --config /oauth2-proxy.cfg
18 command: --config /oauth2-proxy.cfg
19 hostname: oauth2-proxy
19 hostname: oauth2-proxy
20 volumes:
20 volumes:
@@ -40,7 +40,7 @@ services:
40
40
41 keycloak:
41 keycloak:
42 container_name: keycloak
42 container_name: keycloak
43โˆ’ image: keycloak/keycloak:25.0
43+ image: keycloak/keycloak:26.7
44 hostname: keycloak
44 hostname: keycloak
45 command:
45 command:
46 - "start-dev"
46 - "start-dev"
contrib/local-environment/docker-compose-nginx.yaml+3โˆ’28
@@ -1,28 +1,18 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
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.
2# for manual testing and exploration of features.
3# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
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
4+# nginx as a reverse proxy and other HTTP services for upstreams.
5#
5#
6# This file is an extension of the main compose file and must be used with it
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 <command>
7# docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml <command>
8# Alternatively:
8# Alternatively:
9# make nginx-<command> (eg make nginx-up, make nginx-down)
9# make nginx-<command> (eg make nginx-up, make nginx-down)
10#
10#
11โˆ’# Access one of the following URLs to initiate a login flow:
11+# Access http://oauth2-proxy.localtest.me:8080 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
22version: "3.0"
12version: "3.0"
23services:
13services:
24 oauth2-proxy:
14 oauth2-proxy:
25โˆ’ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
15+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
26 ports: []
16 ports: []
27 hostname: oauth2-proxy
17 hostname: oauth2-proxy
28 container_name: oauth2-proxy
18 container_name: oauth2-proxy
@@ -33,7 +23,6 @@ services:
33 networks:
23 networks:
34 oauth2-proxy: {}
24 oauth2-proxy: {}
35 dex: {}
25 dex: {}
36โˆ’ etcd: {}
37 httpbin: {}
26 httpbin: {}
38 depends_on:
27 depends_on:
39 - dex
28 - dex
@@ -52,7 +41,6 @@ services:
52 networks:
41 networks:
53 oauth2-proxy: {}
42 oauth2-proxy: {}
54 dex: {}
43 dex: {}
55โˆ’ etcd: {}
56 httpbin: {}
44 httpbin: {}
57 dex:
45 dex:
58 container_name: dex
46 container_name: dex
@@ -68,26 +56,13 @@ services:
68 dex:
56 dex:
69 aliases:
57 aliases:
70 - dex.localtest.me
58 - dex.localtest.me
71โˆ’ etcd: {}
72โˆ’ depends_on:
73โˆ’ - etcd
74 httpbin:
59 httpbin:
75 container_name: httpbin
60 container_name: httpbin
76 image: kennethreitz/httpbin
61 image: kennethreitz/httpbin
77 ports: []
62 ports: []
78 networks:
63 networks:
79 httpbin: {}
64 httpbin: {}
80โˆ’ etcd:
81โˆ’ container_name: etcd
82โˆ’ image: gcr.io/etcd-development/etcd:v3.7.1
83โˆ’ entrypoint: /usr/local/bin/etcd
84โˆ’ command:
85โˆ’ - --listen-client-urls=http://0.0.0.0:2379
86โˆ’ - --advertise-client-urls=http://etcd:2379
87โˆ’ networks:
88โˆ’ etcd: {}
89networks:
65networks:
90 dex: {}
66 dex: {}
91โˆ’ etcd: {}
92 httpbin: {}
67 httpbin: {}
93 oauth2-proxy: {}
68 oauth2-proxy: {}
contrib/local-environment/docker-compose-traefik.yaml+42โˆ’18
@@ -1,7 +1,7 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
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.
2# for manual testing and exploration of features.
3โˆ’# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
3+# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider
4โˆ’# HTTPBin as an example upstream.
4+# and HTTPBin as an example upstream.
5#
5#
6# This can either be created using docker-compose
6# This can either be created using docker-compose
7# docker-compose -f docker-compose-traefik.yaml <command>
7# docker-compose -f docker-compose-traefik.yaml <command>
@@ -9,32 +9,51 @@
9# make traefik-<command> (eg. make traefik-up, make traefik-down)
9# make traefik-<command> (eg. make traefik-up, make traefik-down)
10#
10#
11# Access one of the following URLs to initiate a login flow:
11# Access one of the following URLs to initiate a login flow:
12โˆ’# - http://oauth2-proxy.localhost
12+# - http://oauth2-proxy.localtest.me
13โˆ’# - http://httpbin.oauth2-proxy.localhost
13+# - http://httpbin.localtest.me
14+# - http://dex.localtest.me/dex
14#
15#
15โˆ’# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.oauth2-proxy.localhost
16+# The OAuth2 Proxy itself is hosted at http://oauth2-proxy.localtest.me
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'
23services:
17services:
24
18
25 oauth2-proxy:
19 oauth2-proxy:
20+ container_name: oauth2-proxy
26โˆ’ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
21+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
22+ command: --config /oauth2-proxy.cfg
27 ports: []
23 ports: []
28 hostname: oauth2-proxy
24 hostname: oauth2-proxy
29 volumes:
25 volumes:
30 - "./oauth2-proxy-traefik.cfg:/oauth2-proxy.cfg"
26 - "./oauth2-proxy-traefik.cfg:/oauth2-proxy.cfg"
27+ restart: unless-stopped
28+ networks:
29+ oauth2-proxy: {}
30+ dex: {}
31+ depends_on:
32+ - dex
33+ - httpbin
34+
35+ dex:
36+ container_name: dex
37+ image: ghcr.io/dexidp/dex:v2.45.1
38+ command: dex serve /dex.yaml
39+ hostname: dex
40+ volumes:
41+ - "./dex-traefik.yaml:/dex.yaml"
42+ restart: unless-stopped
31 networks:
43 networks:
32โˆ’ oauth2-proxy:
44+ dex: {}
45+
46+ httpbin:
47+ container_name: httpbin
48+ image: kennethreitz/httpbin
49+ ports: []
50+ networks:
51+ httpbin: {}
33
52
34 # Reverse proxy
53 # Reverse proxy
35 gateway:
54 gateway:
36 container_name: traefik
55 container_name: traefik
37โˆ’ image: traefik:v2.11.54
56+ image: traefik:v3.7.10
38 volumes:
57 volumes:
39 - "./traefik:/etc/traefik"
58 - "./traefik:/etc/traefik"
40 ports:
59 ports:
@@ -43,8 +62,13 @@ services:
43 depends_on:
62 depends_on:
44 - oauth2-proxy
63 - oauth2-proxy
45 networks:
64 networks:
46โˆ’ oauth2-proxy:
65+ oauth2-proxy: {}
47โˆ’ httpbin:
66+ httpbin: {}
67+ dex:
68+ aliases:
69+ - dex.localtest.me
48
70
49networks:
71networks:
72+ dex: {}
73+ httpbin: {}
50โˆ’ oauth2-proxy:
74+ oauth2-proxy: {}
contrib/local-environment/docker-compose.yaml+2โˆ’15
@@ -1,7 +1,7 @@
1# This docker-compose file can be used to bring up an example instance of oauth2-proxy
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.
2# for manual testing and exploration of features.
3# Alongside OAuth2-Proxy, this file also starts Dex to act as the identity provider,
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.
4+# HTTPBin as an example upstream.
5#
5#
6# This can either be created using docker-compose
6# This can either be created using docker-compose
7# docker-compose -f docker-compose.yaml <command>
7# docker-compose -f docker-compose.yaml <command>
@@ -13,7 +13,7 @@ version: "3.0"
13services:
13services:
14 oauth2-proxy:
14 oauth2-proxy:
15 container_name: oauth2-proxy
15 container_name: oauth2-proxy
16โˆ’ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.3
16+ image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.4
17 command: --config /oauth2-proxy.cfg
17 command: --config /oauth2-proxy.cfg
18 hostname: oauth2-proxy
18 hostname: oauth2-proxy
19 volumes:
19 volumes:
@@ -41,25 +41,12 @@ services:
41 dex:
41 dex:
42 aliases:
42 aliases:
43 - dex.localtest.me
43 - dex.localtest.me
44โˆ’ etcd: {}
45โˆ’ depends_on:
46โˆ’ - etcd
47 httpbin:
44 httpbin:
48 container_name: httpbin
45 container_name: httpbin
49 image: kennethreitz/httpbin
46 image: kennethreitz/httpbin
50 ports: []
47 ports: []
51 networks:
48 networks:
52 httpbin: {}
49 httpbin: {}
53โˆ’ etcd:
54โˆ’ container_name: etcd
55โˆ’ image: gcr.io/etcd-development/etcd:v3.7.1
56โˆ’ entrypoint: /usr/local/bin/etcd
57โˆ’ command:
58โˆ’ - --listen-client-urls=http://0.0.0.0:2379
59โˆ’ - --advertise-client-urls=http://etcd:2379
60โˆ’ networks:
61โˆ’ etcd: {}
62networks:
50networks:
63 dex: {}
51 dex: {}
64โˆ’ etcd: {}
65 httpbin: {}
52 httpbin: {}
contrib/local-environment/oauth2-proxy-traefik.cfg+4โˆ’4
@@ -2,14 +2,14 @@ http_address="0.0.0.0:4180"
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
2cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w="
3provider="oidc"
3provider="oidc"
4email_domains=["example.com"]
4email_domains=["example.com"]
5โˆ’oidc_issuer_url="http://dex.localhost:5556/dex"
5+oidc_issuer_url="http://dex.localtest.me/dex"
6client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
6client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK"
7client_id="oauth2-proxy"
7client_id="oauth2-proxy"
8cookie_secure="false"
8cookie_secure="false"
9
9
10โˆ’redirect_url="http://oauth2-proxy.oauth2-proxy.localhost/oauth2/callback"
10+redirect_url="http://oauth2-proxy.localtest.me/oauth2/callback"
11โˆ’cookie_domains=".oauth2-proxy.localhost" # Required so cookie can be read on all subdomains.
11+cookie_domains=".localtest.me" # Required so cookie can be read on all subdomains.
12โˆ’whitelist_domains=".oauth2-proxy.localhost" # Required to allow redirection back to original requested target.
12+whitelist_domains=".localtest.me" # Required to allow redirection back to original requested target.
13
13
14# Mandatory option when using oauth2-proxy with traefik
14# Mandatory option when using oauth2-proxy with traefik
15reverse_proxy="true"
15reverse_proxy="true"
contrib/local-environment/traefik/dynamic.yaml+11โˆ’4
@@ -1,22 +1,25 @@
1http:
1http:
2 routers:
2 routers:
3 oauth2-proxy-route:
3 oauth2-proxy-route:
4โˆ’ rule: "Host(`oauth2-proxy.oauth2-proxy.localhost`)"
4+ rule: "Host(`oauth2-proxy.localtest.me`)"
5 middlewares:
5 middlewares:
6 - auth-headers
6 - auth-headers
7 service: oauth-backend
7 service: oauth-backend
8+ dex-route:
9+ rule: "Host(`dex.localtest.me`)"
10+ service: dex-service
8 httpbin-route:
11 httpbin-route:
9โˆ’ rule: "Host(`httpbin.oauth2-proxy.localhost`)"
12+ rule: "Host(`httpbin.localtest.me`)"
10 service: httpbin-service
13 service: httpbin-service
11 middlewares:
14 middlewares:
12 - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
15 - oauth-auth-redirect # redirects all unauthenticated to oauth2 signin
13 httpbin-route-2:
16 httpbin-route-2:
14โˆ’ rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/no-auto-redirect`)"
17+ rule: "Host(`httpbin.localtest.me`) && PathPrefix(`/no-auto-redirect`)"
15 service: httpbin-service
18 service: httpbin-service
16 middlewares:
19 middlewares:
17 - oauth-auth-wo-redirect # unauthenticated session will return a 401
20 - oauth-auth-wo-redirect # unauthenticated session will return a 401
18 services-oauth2-route:
21 services-oauth2-route:
19โˆ’ rule: "Host(`httpbin.oauth2-proxy.localhost`) && PathPrefix(`/oauth2/`)"
22+ rule: "Host(`httpbin.localtest.me`) && PathPrefix(`/oauth2/`)"
20 middlewares:
23 middlewares:
21 - auth-headers
24 - auth-headers
22 service: oauth-backend
25 service: oauth-backend
@@ -30,6 +33,10 @@ http:
30 loadBalancer:
33 loadBalancer:
31 servers:
34 servers:
32 - url: http://oauth2-proxy:4180
35 - url: http://oauth2-proxy:4180
36+ dex-service:
37+ loadBalancer:
38+ servers:
39+ - url: http://dex:5556
33
40
34 middlewares:
41 middlewares:
35 auth-headers:
42 auth-headers:
contrib/local-environment/traefik/traefik.yaml+2
@@ -5,3 +5,5 @@ log:
5providers:
5providers:
6 file:
6 file:
7 filename: /etc/traefik/dynamic.yaml
7 filename: /etc/traefik/dynamic.yaml
8+ # Routes are static for this example; disabling watch avoids host inotify limits.
9+ watch: false
docs/docs/configuration/alpha_config.md+12โˆ’1
@@ -167,6 +167,17 @@ injectResponseHeaders:
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
168* `secretSource` - `value` (base64), `fromFile` (file path)
168* `secretSource` - `value` (base64), `fromFile` (file path)
169
169
170+#### Claim value serialization
171+
172+`claimSource` converts the selected claim into one or more string header values. Scalars
173+produce one value, while arrays produce one value per element. Values that cannot be
174+converted directly to strings, such as objects, are JSON-encoded individually.
175+
176+Each non-empty value is added separately under the configured header name. An array of
177+objects therefore produces multiple header values containing one JSON object each, not a
178+single JSON array. HTTP libraries and intermediaries may expose repeated values as a
179+comma-joined string, so consumers should retrieve and parse each header value separately.
180+
170**Request option:** `preserveRequestValue: true` retains existing header values
181**Request option:** `preserveRequestValue: true` retains existing header values
171
182
172**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
183**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
@@ -678,7 +689,7 @@ Requests will be proxied to this upstream if the path matches the request path.
678| Field | Type | Description |
689| Field | Type | Description |
679| ----- | ---- | ----------- |
690| ----- | ---- | ----------- |
680| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
691| `id` | _string_ | ID should be a unique identifier for the upstream.<br/>This value is required for all upstreams. |
681โˆ’| `path` | _string_ | Path is used to map requests to the upstream server.<br/>The closest match will take precedence and all Paths must be unique.<br/>Path can also take a pattern when used with RewriteTarget.<br/>Path segments can be captured and matched using regular experessions.<br/>Eg:<br/>- `^/foo$`: Match only the explicit path `/foo`<br/>- `^/bar/$`: Match any path prefixed with `/bar/`<br/>- `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget |
692+| `path` | _string_ | Path is used to map requests to the upstream server.<br/>The closest match will take precedence and all Paths must be unique.<br/>Without RewriteTarget, Path follows gorilla/mux route syntax.<br/>Eg:<br/>- `/foo`: Match only the explicit path `/foo`<br/>- `/bar/`: Match any path prefixed with `/bar/`<br/>- `/baz/{path:.*}`: Match any path prefixed with `/baz/`<br/>When RewriteTarget is set, Path is treated as a regular expression.<br/>Eg: `^/baz/(.*)$` matches any path prefixed with `/baz` and captures the<br/>remaining path for use with RewriteTarget. |
682| `rewriteTarget` | _string_ | RewriteTarget allows users to rewrite the request path before it is sent to<br/>the upstream server (for an HTTP/HTTPS upstream) or mapped to the filesystem<br/>(for a `file:` upstream).<br/>Use the Path to capture segments for reuse within the rewrite target.<br/>Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite<br/>the request `/baz/abc/123` to `/foo/abc/123` before proxying to the<br/>upstream server. Or if the upstream were `file:///app`, a request for<br/>`/baz/info.html` would return the contents of the file `/app/foo/info.html`. |
693| `rewriteTarget` | _string_ | RewriteTarget allows users to rewrite the request path before it is sent to<br/>the upstream server (for an HTTP/HTTPS upstream) or mapped to the filesystem<br/>(for a `file:` upstream).<br/>Use the Path to capture segments for reuse within the rewrite target.<br/>Eg: With a Path of `^/baz/(.*)`, a RewriteTarget of `/foo/$1` would rewrite<br/>the request `/baz/abc/123` to `/foo/abc/123` before proxying to the<br/>upstream server. Or if the upstream were `file:///app`, a request for<br/>`/baz/info.html` would return the contents of the file `/app/foo/info.html`. |
683| `uri` | _string_ | The URI of the upstream server. This may be an HTTP(S) server of a File<br/>based URL. It may include a path, in which case all requests will be served<br/>under that path.<br/>Eg:<br/>- http://localhost:8080<br/>- https://service.localhost<br/>- https://service.localhost/path<br/>- file://host/path<br/>If the URI's path is "/base" and the incoming request was for "/dir",<br/>the upstream request will be for "/base/dir". |
694| `uri` | _string_ | The URI of the upstream server. This may be an HTTP(S) server of a File<br/>based URL. It may include a path, in which case all requests will be served<br/>under that path.<br/>Eg:<br/>- http://localhost:8080<br/>- https://service.localhost<br/>- https://service.localhost/path<br/>- file://host/path<br/>If the URI's path is "/base" and the incoming request was for "/dir",<br/>the upstream request will be for "/base/dir". |
684| `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.<br/>This option is insecure and will allow potential Man-In-The-Middle attacks<br/>between OAuth2 Proxy and the upstream server.<br/>Defaults to false. |
695| `insecureSkipTLSVerify` | _bool_ | InsecureSkipTLSVerify will skip TLS verification of upstream HTTPS hosts.<br/>This option is insecure and will allow potential Man-In-The-Middle attacks<br/>between OAuth2 Proxy and the upstream server.<br/>Defaults to false. |
docs/docs/configuration/alpha_config.md.tmpl+11
@@ -167,6 +167,17 @@ injectResponseHeaders:
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
167* `claimSource` - `claim` (session claims either from id token or from profile URL)
168* `secretSource` - `value` (base64), `fromFile` (file path)
168* `secretSource` - `value` (base64), `fromFile` (file path)
169
169
170+#### Claim value serialization
171+
172+`claimSource` converts the selected claim into one or more string header values. Scalars
173+produce one value, while arrays produce one value per element. Values that cannot be
174+converted directly to strings, such as objects, are JSON-encoded individually.
175+
176+Each non-empty value is added separately under the configured header name. An array of
177+objects therefore produces multiple header values containing one JSON object each, not a
178+single JSON array. HTTP libraries and intermediaries may expose repeated values as a
179+comma-joined string, so consumers should retrieve and parse each header value separately.
180+
170**Request option:** `preserveRequestValue: true` retains existing header values
181**Request option:** `preserveRequestValue: true` retains existing header values
171
182
172**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
183**Incompatibility:** Remove legacy flags `pass-user-headers`, `set-xauthrequest`
docs/docs/configuration/integrations/nginx.md+27โˆ’11
@@ -60,23 +60,23 @@ server {
60 auth_request_set $auth_cookie $upstream_http_set_cookie;
60 auth_request_set $auth_cookie $upstream_http_set_cookie;
61 add_header Set-Cookie $auth_cookie;
61 add_header Set-Cookie $auth_cookie;
62
62
63โˆ’ # When using the --set-authorization-header flag, some provider's cookies can exceed the 4kb
63+ # Large OIDC tokens can make the session cookie exceed the 4KB limit,
64โˆ’ # limit and so the OAuth2 Proxy splits these into multiple parts.
64+ # causing OAuth2 Proxy to split the cookie into multiple parts.
65 # Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
65 # Nginx normally only copies the first `Set-Cookie` header from the auth_request to the response,
66โˆ’ # so if your cookies are larger than 4kb, you will need to extract additional cookies manually.
66+ # so extract the second part when using the default cookie name, `_oauth2_proxy`.
67โˆ’ auth_request_set $auth_cookie_name_upstream_1 $upstream_cookie_auth_cookie_name_1;
67+ auth_request_set $auth_cookie_upstream_1 $upstream_cookie__oauth2_proxy_1;
68
68
69 # Extract the Cookie attributes from the first Set-Cookie header and append them
69 # Extract the Cookie attributes from the first Set-Cookie header and append them
70 # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
70 # to the second part ($upstream_cookie_* variables only contain the raw cookie content)
71 if ($auth_cookie ~* "(; .*)") {
71 if ($auth_cookie ~* "(; .*)") {
72โˆ’ set $auth_cookie_name_0 $auth_cookie;
72+ set $auth_cookie_0 $auth_cookie;
73โˆ’ set $auth_cookie_name_1 "auth_cookie_name_1=$auth_cookie_name_upstream_1$1";
73+ set $auth_cookie_1 "_oauth2_proxy_1=$auth_cookie_upstream_1$1";
74 }
74 }
75
75
76 # Send both Set-Cookie headers now if there was a second part
76 # Send both Set-Cookie headers now if there was a second part
77โˆ’ if ($auth_cookie_name_upstream_1) {
77+ if ($auth_cookie_upstream_1) {
78โˆ’ add_header Set-Cookie $auth_cookie_name_0;
78+ add_header Set-Cookie $auth_cookie_0;
79โˆ’ add_header Set-Cookie $auth_cookie_name_1;
79+ add_header Set-Cookie $auth_cookie_1;
80 }
80 }
81
81
82 proxy_pass http://backend/;
82 proxy_pass http://backend/;
@@ -91,6 +91,24 @@ server {
91}
91}
92```
92```
93
93
94+### Multi-part session cookies
95+
96+The example above is ready to use with the default `--cookie-name=_oauth2_proxy` setting and handles a session split into two cookies. If you configure a custom cookie name, update both references to the second cookie part:
97+
98+| Cookie configuration | Nginx upstream cookie variable | `Set-Cookie` name |
99+| --- | --- | --- |
100+| Default (`_oauth2_proxy`) | `$upstream_cookie__oauth2_proxy_1` | `_oauth2_proxy_1=` |
101+| `--cookie-name=my_cookie` | `$upstream_cookie_my_cookie_1` | `my_cookie_1=` |
102+
103+For example, with `--cookie-name=my_cookie`, change the relevant lines to:
104+
105+```nginx
106+auth_request_set $auth_cookie_upstream_1 $upstream_cookie_my_cookie_1;
107+set $auth_cookie_1 "my_cookie_1=$auth_cookie_upstream_1$1";
108+```
109+
110+If a session is split into more than two cookies, repeat the extraction for each additional numeric suffix. For consistently large sessions, prefer `--session-store-type=redis` so only a small session ticket is stored in the browser cookie.
111+
94### Understanding the `error_page` redirect pattern
112### Understanding the `error_page` redirect pattern
95
113
96The `auth_request` directive expects the authentication endpoint (`/oauth2/auth`) to return:
114The `auth_request` directive expects the authentication endpoint (`/oauth2/auth`) to return:
@@ -167,8 +185,6 @@ It is recommended to use `--session-store-type=redis` when expecting large sessi
167For a complete example of integrating oauth2-proxy with Kubernetes Dashboard on AKS using Azure Entra ID, including RBAC configuration and troubleshooting, see the [Kubernetes Dashboard on AKS](../providers/ms_entra_id.md#kubernetes-dashboard-on-aks) section in the Microsoft Entra ID provider documentation.
185For a complete example of integrating oauth2-proxy with Kubernetes Dashboard on AKS using Azure Entra ID, including RBAC configuration and troubleshooting, see the [Kubernetes Dashboard on AKS](../providers/ms_entra_id.md#kubernetes-dashboard-on-aks) section in the Microsoft Entra ID provider documentation.
168:::
186:::
169
187
170โˆ’You have to substitute *name* with the actual cookie name you configured via --cookie-name parameter. If you don't set a custom cookie name the variable should be "$upstream_cookie__oauth2_proxy_1" instead of "$upstream_cookie_name_1" and the new cookie-name should be "_oauth2_proxy_1=" instead of "name_1=".
171โˆ’
172:::note
188:::note
173If you set up your OAuth2 provider to rotate your client secret, you can use the `client-secret-file` option to reload the secret when it is updated.
189If you set up your OAuth2 provider to rotate your client secret, you can use the `client-secret-file` option to reload the secret when it is updated.
174:::
190:::
docs/docs/configuration/providers/atlassian.mdnew file+26
@@ -0,0 +1,26 @@
1+---
2+id: atlassian
3+title: Atlassian Cloud
4+---
5+
6+1. Create an OAuth 2.0 integration in the [Atlassian developer console](https://developer.atlassian.com/console/myapps/).
7+2. Under **Permissions**, add the User identity API with the `read:me` scope.
8+3. Under **Authorization**, set the callback URL to `https://<proxy_host>/oauth2/callback`.
9+4. Copy the client ID and secret from **Settings**.
10+5. Run the proxy with:
11+
12+```shell
13+ --provider=atlassian
14+ --client-id=<client ID>
15+ --client-secret=<secret>
16+ --redirect-url=https://<proxy_host>/oauth2/callback
17+ --cookie-secret=<cookie secret>
18+```
19+
20+A new app works only for the account that created it. Enable sharing under **Distribution** to let other users sign in.
21+Atlassian then admits only accounts with access to the Atlassian site the app is installed on; others are refused at
22+the consent screen.
23+
24+The proxy reads the user's email from `https://api.atlassian.com/me`. Restricting access further is supported by
25+[email](index.md#email-authentication). With `--cookie-refresh` set, the proxy re-validates the token against `/me` on
26+that interval.
docs/docs/configuration/providers/dtt_sso.mdnew file+45
@@ -0,0 +1,45 @@
1+---
2+id: dtt_sso
3+title: DTT SSO
4+---
5+
6+Signs users in with the DTT SSO (auth-server) using the authorization code flow and its HS256 id_token.
7+
8+1. Register a client in the auth-server config, one per protected service, with the proxy's callback as its
9+ only redirect URI:
10+
11+ ```yaml
12+ clients:
13+ redash-gate:
14+ secret: <random secret>
15+ redirect_uri: https://redash.example.com/oauth2/callback
16+ display_name: Redash
17+ ```
18+
19+2. Run the proxy with:
20+
21+ ```shell
22+ --provider=dtt-sso
23+ --client-id=redash-gate
24+ --client-secret-file=/run/secrets/client_secret
25+ --redirect-url=https://redash.example.com/oauth2/callback
26+ --cookie-secret-file=/run/secrets/cookie_secret
27+ --cookie-name=_dtt_sso_redash
28+ --cookie-refresh=5m
29+ ```
30+
31+ Give each instance its own `--cookie-name`: another oauth2-proxy setting its cookie on a parent domain
32+ (the Atlassian proxy uses `_oauth2_proxy` on `.thebestagent.pro`) would otherwise shadow this one.
33+
34+The endpoints default to `https://auth.thebestagent.pro`. For another SSO host set `--login-url`, `--redeem-url`
35+and `--validate-url` to its `/oauth/authorize`, `/oauth/token` and `/oauth/check_token`; the id_token issuer must
36+equal the login URL's origin (the SSO's `site_host`).
37+
38+The scope must stay `openid`, the only scope the SSO accepts.
39+
40+The session carries the SSO `email`, the user UUID as the user, and the SSO `role` as the only group, so
41+`--allowed-group=dtt_admin` restricts access by role. `--authenticated-emails-file` and `--email-domain` restrict
42+by email as usual.
43+
44+With `--cookie-refresh` set, the proxy refreshes the tokens on that interval and re-reads the email and role; a user
45+deleted or signed out in the SSO loses access at the next refresh.
docs/docs/configuration/providers/index.md+2
@@ -9,10 +9,12 @@ with Redirect URI(s) for the domain you intend to run `oauth2-proxy` on.
9Valid providers are :
9Valid providers are :
10
10
11- [ADFS](adfs.md)
11- [ADFS](adfs.md)
12+- [Atlassian Cloud](atlassian.md)
12- [Bitbucket](bitbucket.md)
13- [Bitbucket](bitbucket.md)
13- [Cidaas](cidaas.md)
14- [Cidaas](cidaas.md)
14- [CiscoDuo](cisco_duo.md)
15- [CiscoDuo](cisco_duo.md)
15- [DigitalOcean](digitalocean.md)
16- [DigitalOcean](digitalocean.md)
17+- [DTT SSO](dtt_sso.md)
16- [Facebook](facebook.md)
18- [Facebook](facebook.md)
17- [Gitea](gitea.md)
19- [Gitea](gitea.md)
18- [GitHub](github.md)
20- [GitHub](github.md)
docs/docs/installation.md+1โˆ’1
@@ -5,7 +5,7 @@ title: Installation
5
5
61. Choose how to deploy:
61. Choose how to deploy:
7
7
8โˆ’ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.3`)
8+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.4`)
9
9
10 b. Using Go to install the latest release
10 b. Using Go to install the latest release
11 ```bash
11 ```bash
docs/sidebars.js+2
@@ -46,11 +46,13 @@ const sidebars = {
46 },
46 },
47 items: [
47 items: [
48 "configuration/providers/adfs",
48 "configuration/providers/adfs",
49+ "configuration/providers/atlassian",
49 "configuration/providers/azure",
50 "configuration/providers/azure",
50 "configuration/providers/bitbucket",
51 "configuration/providers/bitbucket",
51 "configuration/providers/cidaas",
52 "configuration/providers/cidaas",
52 "configuration/providers/cisco_duo",
53 "configuration/providers/cisco_duo",
53 "configuration/providers/digitalocean",
54 "configuration/providers/digitalocean",
55+ "configuration/providers/dtt_sso",
54 "configuration/providers/facebook",
56 "configuration/providers/facebook",
55 "configuration/providers/gitea",
57 "configuration/providers/gitea",
56 "configuration/providers/github",
58 "configuration/providers/github",
docs/versioned_docs/version-7.15.x/installation.md+1โˆ’1
@@ -5,7 +5,7 @@ title: Installation
5
5
61. Choose how to deploy:
61. Choose how to deploy:
7
7
8โˆ’ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.3`)
8+ a. Using a [Prebuilt Binary](https://github.com/oauth2-proxy/oauth2-proxy/releases) (current release is `v7.15.4`)
9
9
10 b. Using Go to install the latest release
10 b. Using Go to install the latest release
11 ```bash
11 ```bash
pkg/apis/options/providers.go+6
@@ -151,6 +151,9 @@ const (
151 // ADFSProvider is the provider type for ADFS
151 // ADFSProvider is the provider type for ADFS
152 ADFSProvider ProviderType = "adfs"
152 ADFSProvider ProviderType = "adfs"
153
153
154+ // AtlassianProvider is the provider type for Atlassian Cloud
155+ AtlassianProvider ProviderType = "atlassian"
156+
154 // AzureProvider is the provider type for Azure
157 // AzureProvider is the provider type for Azure
155 AzureProvider ProviderType = "azure"
158 AzureProvider ProviderType = "azure"
156
159
@@ -166,6 +169,9 @@ const (
166 // DigitalOceanProvider is the provider type for DigitalOcean
169 // DigitalOceanProvider is the provider type for DigitalOcean
167 DigitalOceanProvider ProviderType = "digitalocean"
170 DigitalOceanProvider ProviderType = "digitalocean"
168
171
172+ // DTTSSOProvider is the provider type for the DTT SSO
173+ DTTSSOProvider ProviderType = "dtt-sso"
174+
169 // FacebookProvider is the provider type for Facebook
175 // FacebookProvider is the provider type for Facebook
170 FacebookProvider ProviderType = "facebook"
176 FacebookProvider ProviderType = "facebook"
171
177
pkg/apis/options/upstreams.go+7โˆ’5
@@ -62,12 +62,14 @@ type Upstream struct {
62
62
63 // Path is used to map requests to the upstream server.
63 // Path is used to map requests to the upstream server.
64 // The closest match will take precedence and all Paths must be unique.
64 // The closest match will take precedence and all Paths must be unique.
65โˆ’ // Path can also take a pattern when used with RewriteTarget.
65+ // Without RewriteTarget, Path follows gorilla/mux route syntax.
66โˆ’ // Path segments can be captured and matched using regular experessions.
67 // Eg:
66 // Eg:
68โˆ’ // - `^/foo$`: Match only the explicit path `/foo`
67+ // - `/foo`: Match only the explicit path `/foo`
69โˆ’ // - `^/bar/$`: Match any path prefixed with `/bar/`
68+ // - `/bar/`: Match any path prefixed with `/bar/`
69+ // - `/baz/{path:.*}`: Match any path prefixed with `/baz/`
70+ // When RewriteTarget is set, Path is treated as a regular expression.
71+ // Eg: `^/baz/(.*)$` matches any path prefixed with `/baz` and captures the
70โˆ’ // - `^/baz/(.*)$`: Match any path prefixed with `/baz` and capture the remaining path for use with RewriteTarget
72+ // remaining path for use with RewriteTarget.
71 Path string `yaml:"path,omitempty"`
73 Path string `yaml:"path,omitempty"`
72
74
73 // RewriteTarget allows users to rewrite the request path before it is sent to
75 // RewriteTarget allows users to rewrite the request path before it is sent to
providers/atlassian-cloud.gonew file+99
@@ -0,0 +1,99 @@
1+package providers
2+
3+import (
4+ "context"
5+ "errors"
6+ "net/url"
7+
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
9+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
10+)
11+
12+// AtlassianProvider represents an Atlassian Cloud based Identity Provider
13+type AtlassianProvider struct {
14+ *ProviderData
15+}
16+
17+var _ Provider = (*AtlassianProvider)(nil)
18+
19+const (
20+ atlassianProviderName = "Atlassian"
21+ atlassianDefaultScope = "read:me"
22+ atlassianPrompt = "consent"
23+ atlassianAudience = "api.atlassian.com"
24+)
25+
26+var (
27+ atlassianDefaultLoginURL = &url.URL{
28+ Scheme: "https",
29+ Host: "auth.atlassian.com",
30+ Path: "/authorize",
31+ }
32+
33+ atlassianDefaultRedeemURL = &url.URL{
34+ Scheme: "https",
35+ Host: "auth.atlassian.com",
36+ Path: "/oauth/token",
37+ }
38+
39+ // atlassianDefaultProfileURL returns the signed-in user's profile.
40+ atlassianDefaultProfileURL = &url.URL{
41+ Scheme: "https",
42+ Host: "api.atlassian.com",
43+ Path: "/me",
44+ }
45+)
46+
47+// NewAtlassianProvider initiates a new AtlassianProvider
48+func NewAtlassianProvider(p *ProviderData) *AtlassianProvider {
49+ p.setProviderDefaults(providerDefaults{
50+ name: atlassianProviderName,
51+ loginURL: atlassianDefaultLoginURL,
52+ redeemURL: atlassianDefaultRedeemURL,
53+ profileURL: atlassianDefaultProfileURL,
54+ validateURL: atlassianDefaultProfileURL,
55+ scope: atlassianDefaultScope,
56+ })
57+ return &AtlassianProvider{ProviderData: p}
58+}
59+
60+// GetLoginURL adds the audience and prompt parameters Atlassian requires.
61+func (p *AtlassianProvider) GetLoginURL(redirectURI, state, _ string, extraParams url.Values) string {
62+ params := url.Values{}
63+ for k, v := range extraParams {
64+ params[k] = v
65+ }
66+ params.Del("approval_prompt")
67+ if params.Get("prompt") == "" {
68+ params.Set("prompt", atlassianPrompt)
69+ }
70+ params.Set("audience", atlassianAudience)
71+
72+ loginURL := makeLoginURL(p.ProviderData, redirectURI, state, params)
73+ return loginURL.String()
74+}
75+
76+// EnrichSession sets the session's email from the Atlassian profile.
77+func (p *AtlassianProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
78+ var profile struct {
79+ Email string `json:"email"`
80+ }
81+ err := requests.New(p.ProfileURL.String()).
82+ WithContext(ctx).
83+ WithHeaders(makeOIDCHeader(s.AccessToken)).
84+ Do().
85+ UnmarshalInto(&profile)
86+ if err != nil {
87+ return err
88+ }
89+ if profile.Email == "" {
90+ return errors.New("no email in Atlassian profile")
91+ }
92+ s.Email = profile.Email
93+ return nil
94+}
95+
96+// ValidateSession validates the AccessToken
97+func (p *AtlassianProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
98+ return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
99+}
providers/atlassian-cloud_test.gonew file+93
@@ -0,0 +1,93 @@
1+package providers
2+
3+import (
4+ "context"
5+ "net/http"
6+ "net/http/httptest"
7+ "net/url"
8+ "testing"
9+
10+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11+ . "github.com/onsi/gomega"
12+)
13+
14+func testAtlassianBackend(status int, body string) *httptest.Server {
15+ return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16+ if r.URL.Path != "/me" || r.Header.Get("Authorization") != "Bearer imaginary_access_token" {
17+ w.WriteHeader(http.StatusUnauthorized)
18+ return
19+ }
20+ w.WriteHeader(status)
21+ _, _ = w.Write([]byte(body))
22+ }))
23+}
24+
25+func testAtlassianProvider(backend *httptest.Server) *AtlassianProvider {
26+ p := NewAtlassianProvider(&ProviderData{})
27+ u, _ := url.Parse(backend.URL)
28+ updateURL(p.ProfileURL, u.Host)
29+ updateURL(p.ValidateURL, u.Host)
30+ return p
31+}
32+
33+func TestNewAtlassianProvider(t *testing.T) {
34+ g := NewWithT(t)
35+
36+ providerData := NewAtlassianProvider(&ProviderData{}).Data()
37+ g.Expect(providerData.ProviderName).To(Equal("Atlassian"))
38+ g.Expect(providerData.LoginURL.String()).To(Equal("https://auth.atlassian.com/authorize"))
39+ g.Expect(providerData.RedeemURL.String()).To(Equal("https://auth.atlassian.com/oauth/token"))
40+ g.Expect(providerData.ProfileURL.String()).To(Equal("https://api.atlassian.com/me"))
41+ g.Expect(providerData.ValidateURL.String()).To(Equal("https://api.atlassian.com/me"))
42+ g.Expect(providerData.Scope).To(Equal("read:me"))
43+}
44+
45+func TestAtlassianProviderGetLoginURL(t *testing.T) {
46+ g := NewWithT(t)
47+ p := NewAtlassianProvider(&ProviderData{ClientID: "client"})
48+
49+ loginURL, err := url.Parse(p.GetLoginURL("https://proxy/oauth2/callback", "state", "",
50+ url.Values{"approval_prompt": {"force"}}))
51+ g.Expect(err).ToNot(HaveOccurred())
52+
53+ q := loginURL.Query()
54+ g.Expect(q.Get("audience")).To(Equal("api.atlassian.com"))
55+ g.Expect(q.Get("prompt")).To(Equal("consent"))
56+ g.Expect(q.Has("approval_prompt")).To(BeFalse())
57+ g.Expect(q.Get("scope")).To(Equal("read:me"))
58+
59+ loginURL, err = url.Parse(p.GetLoginURL("https://proxy/oauth2/callback", "state", "",
60+ url.Values{"prompt": {"login"}}))
61+ g.Expect(err).ToNot(HaveOccurred())
62+ g.Expect(loginURL.Query().Get("prompt")).To(Equal("login"))
63+}
64+
65+func TestAtlassianProviderEnrichSession(t *testing.T) {
66+ g := NewWithT(t)
67+ b := testAtlassianBackend(http.StatusOK, `{"account_id":"abc","email":"user@example.com"}`)
68+ defer b.Close()
69+ p := testAtlassianProvider(b)
70+
71+ session := CreateAuthorizedSession()
72+ g.Expect(p.EnrichSession(context.Background(), session)).To(Succeed())
73+ g.Expect(session.Email).To(Equal("user@example.com"))
74+}
75+
76+func TestAtlassianProviderEnrichSessionWithoutEmail(t *testing.T) {
77+ g := NewWithT(t)
78+ b := testAtlassianBackend(http.StatusOK, `{"account_id":"abc"}`)
79+ defer b.Close()
80+ p := testAtlassianProvider(b)
81+
82+ g.Expect(p.EnrichSession(context.Background(), CreateAuthorizedSession())).ToNot(Succeed())
83+}
84+
85+func TestAtlassianProviderValidateSession(t *testing.T) {
86+ g := NewWithT(t)
87+ b := testAtlassianBackend(http.StatusOK, `{"email":"user@example.com"}`)
88+ defer b.Close()
89+ p := testAtlassianProvider(b)
90+
91+ g.Expect(p.ValidateSession(context.Background(), CreateAuthorizedSession())).To(BeTrue())
92+ g.Expect(p.ValidateSession(context.Background(), &sessions.SessionState{AccessToken: "wrong"})).To(BeFalse())
93+}
providers/dtt_sso.gonew file+202
@@ -0,0 +1,202 @@
1+package providers
2+
3+import (
4+ "bytes"
5+ "context"
6+ "encoding/base64"
7+ "errors"
8+ "fmt"
9+ "net/http"
10+ "net/url"
11+ "time"
12+
13+ "github.com/golang-jwt/jwt/v5"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
16+)
17+
18+// DTTSSOProvider signs users in with the DTT SSO (auth-server).
19+type DTTSSOProvider struct {
20+ *ProviderData
21+}
22+
23+var _ Provider = (*DTTSSOProvider)(nil)
24+
25+const (
26+ dttSSOProviderName = "DTT SSO"
27+ dttSSODefaultScope = "openid"
28+)
29+
30+var (
31+ dttSSODefaultLoginURL = &url.URL{
32+ Scheme: "https",
33+ Host: "auth.thebestagent.pro",
34+ Path: "/oauth/authorize",
35+ }
36+
37+ dttSSODefaultRedeemURL = &url.URL{
38+ Scheme: "https",
39+ Host: "auth.thebestagent.pro",
40+ Path: "/oauth/token",
41+ }
42+
43+ dttSSODefaultValidateURL = &url.URL{
44+ Scheme: "https",
45+ Host: "auth.thebestagent.pro",
46+ Path: "/oauth/check_token",
47+ }
48+)
49+
50+// NewDTTSSOProvider initiates a new DTTSSOProvider
51+func NewDTTSSOProvider(p *ProviderData) *DTTSSOProvider {
52+ p.setProviderDefaults(providerDefaults{
53+ name: dttSSOProviderName,
54+ loginURL: dttSSODefaultLoginURL,
55+ redeemURL: dttSSODefaultRedeemURL,
56+ profileURL: nil,
57+ validateURL: dttSSODefaultValidateURL,
58+ scope: dttSSODefaultScope,
59+ })
60+ return &DTTSSOProvider{ProviderData: p}
61+}
62+
63+// dttSSOTokenResponse is the SSO's /oauth/token response.
64+type dttSSOTokenResponse struct {
65+ AccessToken string `json:"access_token"`
66+ RefreshToken string `json:"refresh_token"`
67+ ExpiresIn int64 `json:"expires_in"`
68+ IDToken string `json:"id_token"`
69+}
70+
71+// dttSSOClaims holds the id_token claims the provider uses.
72+type dttSSOClaims struct {
73+ jwt.RegisteredClaims
74+ Email string `json:"email"`
75+ Role string `json:"role"`
76+ UserID string `json:"user_id"`
77+}
78+
79+// Redeem exchanges the authorization code for a session.
80+func (p *DTTSSOProvider) Redeem(ctx context.Context, redirectURL, code, _ string) (*sessions.SessionState, error) {
81+ if code == "" {
82+ return nil, ErrMissingCode
83+ }
84+ return p.requestTokens(ctx, url.Values{
85+ "grant_type": {"authorization_code"},
86+ "code": {code},
87+ "redirect_uri": {redirectURL},
88+ })
89+}
90+
91+// RefreshSession renews the tokens and re-reads the email and role.
92+func (p *DTTSSOProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
93+ if s == nil || s.RefreshToken == "" {
94+ return false, nil
95+ }
96+ refreshed, err := p.requestTokens(ctx, url.Values{
97+ "grant_type": {"refresh_token"},
98+ "refresh_token": {s.RefreshToken},
99+ })
100+ if err != nil {
101+ return false, fmt.Errorf("unable to redeem refresh token: %v", err)
102+ }
103+ s.AccessToken = refreshed.AccessToken
104+ s.RefreshToken = refreshed.RefreshToken
105+ s.IDToken = refreshed.IDToken
106+ s.Email = refreshed.Email
107+ s.User = refreshed.User
108+ s.Groups = refreshed.Groups
109+ s.CreatedAt = refreshed.CreatedAt
110+ s.ExpiresOn = refreshed.ExpiresOn
111+ return true, nil
112+}
113+
114+// ValidateSession checks the access token with the SSO's check_token endpoint.
115+func (p *DTTSSOProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
116+ return validateToken(ctx, p, s.AccessToken, makeOIDCHeader(s.AccessToken))
117+}
118+
119+// requestTokens calls the token endpoint and builds a session from the verified id_token.
120+//
121+// The SSO compares the Basic credentials byte for byte, so they are sent unescaped
122+// rather than through golang.org/x/oauth2, which URL-escapes them.
123+func (p *DTTSSOProvider) requestTokens(ctx context.Context, params url.Values) (*sessions.SessionState, error) {
124+ clientSecret, err := p.GetClientSecret()
125+ if err != nil {
126+ return nil, err
127+ }
128+ credentials := base64.StdEncoding.EncodeToString([]byte(p.ClientID + ":" + clientSecret))
129+
130+ result := requests.New(p.RedeemURL.String()).
131+ WithContext(ctx).
132+ WithMethod(http.MethodPost).
133+ WithBody(bytes.NewBufferString(params.Encode())).
134+ SetHeader("Content-Type", "application/x-www-form-urlencoded").
135+ SetHeader("Authorization", "Basic "+credentials).
136+ Do()
137+ if result.Error() != nil {
138+ return nil, result.Error()
139+ }
140+ if result.StatusCode() != http.StatusOK {
141+ return nil, fmt.Errorf("token request failed: status %d - %s", result.StatusCode(), result.Body())
142+ }
143+
144+ var token dttSSOTokenResponse
145+ if err := result.UnmarshalInto(&token); err != nil {
146+ return nil, err
147+ }
148+ if token.AccessToken == "" {
149+ return nil, errors.New("token response carried no access_token")
150+ }
151+ claims, err := p.verifyIDToken(token.IDToken, clientSecret)
152+ if err != nil {
153+ return nil, err
154+ }
155+
156+ s := &sessions.SessionState{
157+ AccessToken: token.AccessToken,
158+ RefreshToken: token.RefreshToken,
159+ IDToken: token.IDToken,
160+ Email: claims.Email,
161+ User: claims.UserID,
162+ }
163+ if claims.Role != "" {
164+ s.Groups = []string{claims.Role}
165+ }
166+ s.CreatedAtNow()
167+ if token.ExpiresIn > 0 {
168+ s.ExpiresIn(time.Duration(token.ExpiresIn) * time.Second)
169+ }
170+ return s, nil
171+}
172+
173+// verifyIDToken checks the id_token's HS256 signature (keyed on the client secret),
174+// issuer, audience and expiry.
175+//
176+// go-jose is not used: it rejects HMAC keys under 32 bytes, and SSO client secrets
177+// can be shorter.
178+func (p *DTTSSOProvider) verifyIDToken(raw, clientSecret string) (*dttSSOClaims, error) {
179+ if raw == "" {
180+ return nil, errors.New("token response carried no id_token; the scope must be exactly \"openid\"")
181+ }
182+ var claims dttSSOClaims
183+ _, err := jwt.ParseWithClaims(raw, &claims,
184+ func(*jwt.Token) (any, error) { return []byte(clientSecret), nil },
185+ jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
186+ jwt.WithIssuer(p.issuer()),
187+ jwt.WithAudience(p.ClientID),
188+ jwt.WithExpirationRequired(),
189+ )
190+ if err != nil {
191+ return nil, fmt.Errorf("verifying id_token: %v", err)
192+ }
193+ if claims.Email == "" {
194+ return nil, errors.New("id_token carries no email")
195+ }
196+ return &claims, nil
197+}
198+
199+// issuer is the SSO's site host, the origin of the login URL.
200+func (p *DTTSSOProvider) issuer() string {
201+ return p.LoginURL.Scheme + "://" + p.LoginURL.Host
202+}
providers/dtt_sso_test.gonew file+237
@@ -0,0 +1,237 @@
1+package providers
2+
3+import (
4+ "context"
5+ "encoding/base64"
6+ "encoding/json"
7+ "net/http"
8+ "net/http/httptest"
9+ "net/url"
10+ "testing"
11+ "time"
12+
13+ "github.com/golang-jwt/jwt/v5"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15+ . "github.com/onsi/gomega"
16+)
17+
18+const (
19+ dttSSOTestClientID = "redash-gate"
20+ // Shorter than 32 bytes, and contains characters golang.org/x/oauth2 would
21+ // URL-escape in Basic auth.
22+ dttSSOTestSecret = "s3cr+t/abc=="
23+)
24+
25+// dttSSOTestServer mimics the SSO's token and check_token endpoints.
26+type dttSSOTestServer struct {
27+ *httptest.Server
28+ claims map[string]interface{}
29+ signingKey []byte
30+ lastForm url.Values
31+ validTokens map[string]bool
32+ tokenCounter int
33+}
34+
35+func newDTTSSOTestServer() *dttSSOTestServer {
36+ s := &dttSSOTestServer{
37+ signingKey: []byte(dttSSOTestSecret),
38+ validTokens: map[string]bool{},
39+ }
40+ s.Server = httptest.NewServer(http.HandlerFunc(s.handle))
41+ u, _ := url.Parse(s.URL)
42+ s.claims = map[string]interface{}{
43+ "iss": "http://" + u.Host,
44+ "aud": dttSSOTestClientID,
45+ "sub": "0b5c1e1a-1111-2222-3333-444455556666",
46+ "user_id": "0b5c1e1a-1111-2222-3333-444455556666",
47+ "email": "admin@example.com",
48+ "role": "dtt_admin",
49+ "exp": time.Now().Add(time.Hour).Unix(),
50+ }
51+ return s
52+}
53+
54+func (s *dttSSOTestServer) handle(w http.ResponseWriter, r *http.Request) {
55+ switch r.URL.Path {
56+ case "/oauth/token":
57+ want := "Basic " + base64.StdEncoding.EncodeToString([]byte(dttSSOTestClientID+":"+dttSSOTestSecret))
58+ if r.Header.Get("Authorization") != want {
59+ w.WriteHeader(http.StatusUnauthorized)
60+ return
61+ }
62+ _ = r.ParseForm()
63+ s.lastForm = r.PostForm
64+ if r.PostForm.Get("grant_type") == "refresh_token" && !s.validTokens["refresh-"+r.PostForm.Get("refresh_token")] {
65+ w.WriteHeader(http.StatusBadRequest)
66+ _, _ = w.Write([]byte(`{"error":"invalid_grant","error_description":"Invalid refresh token"}`))
67+ return
68+ }
69+ s.tokenCounter++
70+ access := "access-" + string(rune('0'+s.tokenCounter))
71+ refresh := "refresh-" + string(rune('0'+s.tokenCounter))
72+ s.validTokens[access] = true
73+ s.validTokens["refresh-"+refresh] = true
74+ w.Header().Set("Content-Type", "application/json")
75+ _ = json.NewEncoder(w).Encode(map[string]interface{}{
76+ "access_token": access,
77+ "token_type": "bearer",
78+ "expires_in": 3600,
79+ "refresh_token": refresh,
80+ "id_token": s.idToken(),
81+ "scope": "openid",
82+ })
83+ case "/oauth/check_token":
84+ token := r.Header.Get("Authorization")
85+ if len(token) > len("Bearer ") && s.validTokens[token[len("Bearer "):]] {
86+ _, _ = w.Write([]byte(`{"user_id":"0b5c1e1a-1111-2222-3333-444455556666","real_user_id":null,"issued_by":"redash-gate"}`))
87+ return
88+ }
89+ w.WriteHeader(http.StatusUnauthorized)
90+ _, _ = w.Write([]byte("Invalid access token"))
91+ default:
92+ w.WriteHeader(http.StatusNotFound)
93+ }
94+}
95+
96+func (s *dttSSOTestServer) idToken() string {
97+ if s.claims == nil {
98+ return ""
99+ }
100+ raw, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(s.claims)).SignedString(s.signingKey)
101+ if err != nil {
102+ panic(err)
103+ }
104+ return raw
105+}
106+
107+func testDTTSSOProvider(server *dttSSOTestServer) *DTTSSOProvider {
108+ p := NewDTTSSOProvider(&ProviderData{ClientID: dttSSOTestClientID, ClientSecret: dttSSOTestSecret})
109+ u, _ := url.Parse(server.URL)
110+ updateURL(p.LoginURL, u.Host)
111+ updateURL(p.RedeemURL, u.Host)
112+ updateURL(p.ValidateURL, u.Host)
113+ return p
114+}
115+
116+func TestNewDTTSSOProvider(t *testing.T) {
117+ g := NewWithT(t)
118+
119+ providerData := NewDTTSSOProvider(&ProviderData{}).Data()
120+ g.Expect(providerData.ProviderName).To(Equal("DTT SSO"))
121+ g.Expect(providerData.LoginURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/authorize"))
122+ g.Expect(providerData.RedeemURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/token"))
123+ g.Expect(providerData.ValidateURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/check_token"))
124+ g.Expect(providerData.Scope).To(Equal("openid"))
125+}
126+
127+func TestDTTSSOProviderRedeem(t *testing.T) {
128+ g := NewWithT(t)
129+ server := newDTTSSOTestServer()
130+ defer server.Close()
131+ p := testDTTSSOProvider(server)
132+
133+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
134+ g.Expect(err).ToNot(HaveOccurred())
135+ g.Expect(s.Email).To(Equal("admin@example.com"))
136+ g.Expect(s.User).To(Equal("0b5c1e1a-1111-2222-3333-444455556666"))
137+ g.Expect(s.Groups).To(Equal([]string{"dtt_admin"}))
138+ g.Expect(s.AccessToken).To(Equal("access-1"))
139+ g.Expect(s.RefreshToken).To(Equal("refresh-1"))
140+ g.Expect(s.IDToken).ToNot(BeEmpty())
141+ g.Expect(s.ExpiresOn).ToNot(BeNil())
142+ g.Expect(*s.ExpiresOn).To(BeTemporally("~", time.Now().Add(time.Hour), time.Minute))
143+
144+ g.Expect(server.lastForm.Get("grant_type")).To(Equal("authorization_code"))
145+ g.Expect(server.lastForm.Get("code")).To(Equal("the-code"))
146+ g.Expect(server.lastForm.Get("redirect_uri")).To(Equal("https://redash.example.com/oauth2/callback"))
147+}
148+
149+func TestDTTSSOProviderRedeemRejectsBadIDTokens(t *testing.T) {
150+ testCases := map[string]func(s *dttSSOTestServer){
151+ "signed with another secret": func(s *dttSSOTestServer) { s.signingKey = []byte("some-other-client-secret") },
152+ "other audience": func(s *dttSSOTestServer) { s.claims["aud"] = "frappe-gate" },
153+ "other issuer": func(s *dttSSOTestServer) { s.claims["iss"] = "https://evil.example.com" },
154+ "expired": func(s *dttSSOTestServer) { s.claims["exp"] = time.Now().Add(-time.Minute).Unix() },
155+ "no email": func(s *dttSSOTestServer) { delete(s.claims, "email") },
156+ "no id_token": func(s *dttSSOTestServer) { s.claims = nil },
157+ }
158+ for name, tamper := range testCases {
159+ t.Run(name, func(t *testing.T) {
160+ g := NewWithT(t)
161+ server := newDTTSSOTestServer()
162+ defer server.Close()
163+ tamper(server)
164+
165+ _, err := testDTTSSOProvider(server).Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
166+ g.Expect(err).To(HaveOccurred())
167+ })
168+ }
169+}
170+
171+func TestDTTSSOProviderRedeemRejectsUnsignedIDToken(t *testing.T) {
172+ g := NewWithT(t)
173+ server := newDTTSSOTestServer()
174+ defer server.Close()
175+ p := testDTTSSOProvider(server)
176+
177+ header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
178+ payload, _ := json.Marshal(server.claims)
179+ unsigned := header + "." + base64.RawURLEncoding.EncodeToString(payload) + "."
180+
181+ _, err := p.verifyIDToken(unsigned, dttSSOTestSecret)
182+ g.Expect(err).To(HaveOccurred())
183+}
184+
185+func TestDTTSSOProviderRefreshSession(t *testing.T) {
186+ g := NewWithT(t)
187+ server := newDTTSSOTestServer()
188+ defer server.Close()
189+ p := testDTTSSOProvider(server)
190+
191+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
192+ g.Expect(err).ToNot(HaveOccurred())
193+ nonce := []byte("keep-me")
194+ s.Nonce = nonce
195+
196+ server.claims["role"] = "agent"
197+ server.claims["email"] = "renamed@example.com"
198+ refreshed, err := p.RefreshSession(context.Background(), s)
199+ g.Expect(err).ToNot(HaveOccurred())
200+ g.Expect(refreshed).To(BeTrue())
201+ g.Expect(s.AccessToken).To(Equal("access-2"))
202+ g.Expect(s.RefreshToken).To(Equal("refresh-2"))
203+ g.Expect(s.Groups).To(Equal([]string{"agent"}))
204+ g.Expect(s.Email).To(Equal("renamed@example.com"))
205+ g.Expect(s.Nonce).To(Equal(nonce))
206+ g.Expect(server.lastForm.Get("grant_type")).To(Equal("refresh_token"))
207+ g.Expect(server.lastForm.Get("refresh_token")).To(Equal("refresh-1"))
208+}
209+
210+func TestDTTSSOProviderRefreshSessionFailures(t *testing.T) {
211+ g := NewWithT(t)
212+ server := newDTTSSOTestServer()
213+ defer server.Close()
214+ p := testDTTSSOProvider(server)
215+
216+ refreshed, err := p.RefreshSession(context.Background(), &sessions.SessionState{})
217+ g.Expect(err).ToNot(HaveOccurred())
218+ g.Expect(refreshed).To(BeFalse())
219+
220+ refreshed, err = p.RefreshSession(context.Background(), &sessions.SessionState{RefreshToken: "revoked"})
221+ g.Expect(err).To(HaveOccurred())
222+ g.Expect(refreshed).To(BeFalse())
223+}
224+
225+func TestDTTSSOProviderValidateSession(t *testing.T) {
226+ g := NewWithT(t)
227+ server := newDTTSSOTestServer()
228+ defer server.Close()
229+ p := testDTTSSOProvider(server)
230+
231+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
232+ g.Expect(err).ToNot(HaveOccurred())
233+ g.Expect(p.ValidateSession(context.Background(), s)).To(BeTrue())
234+
235+ delete(server.validTokens, s.AccessToken)
236+ g.Expect(p.ValidateSession(context.Background(), s)).To(BeFalse())
237+}
providers/providers.go+7โˆ’3
@@ -40,6 +40,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
40 switch providerConfig.Type {
40 switch providerConfig.Type {
41 case options.ADFSProvider:
41 case options.ADFSProvider:
42 return NewADFSProvider(providerData, providerConfig), nil
42 return NewADFSProvider(providerData, providerConfig), nil
43+ case options.AtlassianProvider:
44+ return NewAtlassianProvider(providerData), nil
43 case options.AzureProvider:
45 case options.AzureProvider:
44 return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
46 return NewAzureProvider(providerData, providerConfig.AzureConfig), nil
45 case options.MicrosoftEntraIDProvider:
47 case options.MicrosoftEntraIDProvider:
@@ -50,6 +52,8 @@ func NewProvider(providerConfig options.Provider) (Provider, error) {
50 return NewCIDAASProvider(providerData, providerConfig), nil
52 return NewCIDAASProvider(providerData, providerConfig), nil
51 case options.DigitalOceanProvider:
53 case options.DigitalOceanProvider:
52 return NewDigitalOceanProvider(providerData), nil
54 return NewDigitalOceanProvider(providerData), nil
55+ case options.DTTSSOProvider:
56+ return NewDTTSSOProvider(providerData), nil
53 case options.FacebookProvider:
57 case options.FacebookProvider:
54 return NewFacebookProvider(providerData), nil
58 return NewFacebookProvider(providerData), nil
55 case options.GitHubProvider:
59 case options.GitHubProvider:
@@ -189,9 +193,9 @@ func parseCodeChallengeMethod(providerConfig options.Provider) string {
189
193
190func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
194func providerRequiresOIDCProviderVerifier(providerType options.ProviderType) (bool, error) {
191 switch providerType {
195 switch providerType {
192โˆ’ case options.BitbucketProvider, options.DigitalOceanProvider, options.FacebookProvider, options.GitHubProvider,
196+ case options.AtlassianProvider, options.BitbucketProvider, options.DigitalOceanProvider, options.DTTSSOProvider,
193โˆ’ options.GoogleProvider, options.KeycloakProvider, options.LinkedInProvider, options.LoginGovProvider,
197+ options.FacebookProvider, options.GitHubProvider, options.GoogleProvider, options.KeycloakProvider,
194โˆ’ options.NextCloudProvider, options.SourceHutProvider:
198+ options.LinkedInProvider, options.LoginGovProvider, options.NextCloudProvider, options.SourceHutProvider:
195 return false, nil
199 return false, nil
196 case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
200 case options.OIDCProvider, options.ADFSProvider, options.AzureProvider, options.CidaasProvider,
197 options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider:
201 options.GitLabProvider, options.KeycloakOIDCProvider, options.MicrosoftEntraIDProvider: