git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add possibility to encode the state param as UrlEncodedBase64 (#2312)

* Add possibility to encode the state param as UrlEncodedBase64 * Update CHANGELOG.md * Update oauthproxy.go Co-authored-by: Jan Larwig <jan@larwig.com> --------- Co-authored-by: Jan Larwig <jan@larwig.com>

commitbc022fbfd1bc37ba61c6d647e8bfbe799a18d302 authorJan Brezina <brezinajn@users.noreply.github.com> dateSat 20 Jan 2024, 20:08 (3 years ago) parentbe84906
5 files changed+50−9
CHANGELOG.md+1 docs/docs/configuration/overview.md+1 oauthproxy.go+22−8 oauthproxy_test.go+24−1 pkg/apis/options/options.go+2
CHANGELOG.md+1
@@ -12,6 +12,7 @@
1212- [#2269](https://github.com/oauth2-proxy/oauth2-proxy/pull/2269) Added Azure China (and other air gaped cloud) support (@mblaschke)
1313- [#2237](https://github.com/oauth2-proxy/oauth2-proxy/pull/2237) adds an option to append CA certificates (@emsixteeen)
1414- [#2128](https://github.com/oauth2-proxy/oauth2-proxy/pull/2128) Update dependencies (@vllvll)
15+- [#2239](https://github.com/oauth2-proxy/oauth2-proxy/pull/2312) Add possibility to encode the state param as UrlEncodedBase64 (@brezinajn)
1516- [#2274](https://github.com/oauth2-proxy/oauth2-proxy/pull/2274) Upgrade golang.org/x/net to v0.17.0 (@pierluigilenoci)
1617- [#2278](https://github.com/oauth2-proxy/oauth2-proxy/pull/2278) Improve the Nginx auth_request example (@akunzai)
1718- [#2282](https://github.com/oauth2-proxy/oauth2-proxy/pull/2282) Fixed checking Google Groups membership using Google Application Credentials (@kvanzuijlen)
docs/docs/configuration/overview.md+1
@@ -207,6 +207,7 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/
207207| `--version` | n/a | print version string | |
208208| `--whitelist-domain` | string \| list | allowed domains for redirection after authentication. Prefix domain with a `.` or a `*.` to allow subdomains (e.g. `.example.com`, `*.example.com`)&nbsp;\[[2](#footnote2)\] | |
209209| `--trusted-ip` | string \| list | list of IPs or CIDR ranges to allow to bypass authentication (may be given multiple times). When combined with `--reverse-proxy` and optionally `--real-client-ip-header` this will evaluate the trust of the IP stored in an HTTP header by a reverse proxy rather than the layer-3/4 remote address. WARNING: trusting IPs has inherent security flaws, especially when obtaining the IP address from an HTTP header (reverse-proxy mode). Use this option only if you understand the risks and how to manage them. | |
210+| `--encode-state` | bool | encode the state parameter as UrlEncodedBase64 | false |
210211
211212> ###### 1. Only these providers support `--cookie-refresh`: GitLab, Google and OIDC {#footnote1}
212213> ###### 2. When using the `whitelist-domain` option, any domain prefixed with a `.` or a `*.` will allow any subdomain of the specified domain as a valid redirect URL. By default, only empty ports are allowed. This translates to allowing the default port of the URLs protocol (80 for HTTP, 443 for HTTPS, etc.) since browsers omit them. To allow only a specific port, add it to the whitelisted domain: `example.com:8080`. To allow any port, use `*`: `example.com:*`. {#footnote2}
oauthproxy.go+22−8
@@ -3,6 +3,7 @@ package main
33import (
44 "context"
55 "embed"
6+ "encoding/base64"
67 "encoding/json"
78 "errors"
89 "fmt"
@@ -110,6 +111,8 @@ type OAuthProxy struct {
110111 serveMux *mux.Router
111112 redirectValidator redirect.Validator
112113 appDirector redirect.AppDirector
114+
115+ encodeState bool
113116}
114117
115118// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
@@ -239,6 +242,7 @@ func NewOAuthProxy(opts *options.Options, validator func(string) bool) (*OAuthPr
239242 upstreamProxy: upstreamProxy,
240243 redirectValidator: redirectValidator,
241244 appDirector: appDirector,
245+ encodeState: opts.EncodeState,
242246 }
243247 p.buildServeMux(opts.ProxyPrefix)
244248
@@ -796,7 +800,7 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove
796800 callbackRedirect := p.getOAuthRedirectURI(req)
797801 loginURL := p.provider.GetLoginURL(
798802 callbackRedirect,
799− encodeState(csrf.HashOAuthState(), appRedirect),
803+ encodeState(csrf.HashOAuthState(), appRedirect, p.encodeState),
800804 csrf.HashOIDCNonce(),
801805 extraParams,
802806 )
@@ -854,7 +858,7 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
854858
855859 csrf.ClearCookie(rw, req)
856860
857− nonce, appRedirect, err := decodeState(req)
861+ nonce, appRedirect, err := decodeState(req.Form.Get("state"), p.encodeState)
858862 if err != nil {
859863 logger.Errorf("Error while parsing OAuth2 state: %v", err)
860864 p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error())
@@ -1194,18 +1198,28 @@ func checkAllowedEmails(req *http.Request, s *sessionsapi.SessionState) bool {
11941198
11951199// encodedState builds the OAuth state param out of our nonce and
11961200// original application redirect
1197−func encodeState(nonce string, redirect string) string {
1198− return fmt.Sprintf("%v:%v", nonce, redirect)
1201+func encodeState(nonce string, redirect string, encode bool) string {
1202+ rawString := fmt.Sprintf("%v:%v", nonce, redirect)
1203+ if encode {
1204+ return base64.RawURLEncoding.EncodeToString([]byte(rawString))
1205+ }
1206+ return rawString
11991207}
12001208
12011209// decodeState splits the reflected OAuth state response back into
12021210// the nonce and original application redirect
1203−func decodeState(req *http.Request) (string, string, error) {
1204− state := strings.SplitN(req.Form.Get("state"), ":", 2)
1205− if len(state) != 2 {
1211+func decodeState(state string, encode bool) (string, string, error) {
1212+ toParse := state
1213+ if encode {
1214+ decoded, _ := base64.RawURLEncoding.DecodeString(state)
1215+ toParse = string(decoded)
1216+ }
1217+
1218+ parsedState := strings.SplitN(toParse, ":", 2)
1219+ if len(parsedState) != 2 {
12061220 return "", "", errors.New("invalid length")
12071221 }
1208− return state[0], state[1], nil
1222+ return parsedState[0], parsedState[1], nil
12091223}
12101224
12111225// addHeadersForProxying adds the appropriate headers the request / response for proxying
oauthproxy_test.go+24−1
@@ -413,7 +413,7 @@ func (patTest *PassAccessTokenTest) getCallbackEndpoint() (httpCode int, cookie
413413 http.MethodGet,
414414 fmt.Sprintf(
415415 "/oauth2/callback?code=callback_code&state=%s",
416− encodeState(csrf.HashOAuthState(), "%2F"),
416+ encodeState(csrf.HashOAuthState(), "%2F", false),
417417 ),
418418 strings.NewReader(""),
419419 )
@@ -3288,6 +3288,29 @@ func TestAuthOnlyAllowedEmailDomains(t *testing.T) {
32883288 }
32893289}
32903290
3291+func TestStateEncodesCorrectly(t *testing.T) {
3292+ state := "some_state_to_test"
3293+ nonce := "some_nonce_to_test"
3294+
3295+ encodedResult := encodeState(nonce, state, true)
3296+ assert.Equal(t, "c29tZV9ub25jZV90b190ZXN0OnNvbWVfc3RhdGVfdG9fdGVzdA", encodedResult)
3297+
3298+ notEncodedResult := encodeState(nonce, state, false)
3299+ assert.Equal(t, "some_nonce_to_test:some_state_to_test", notEncodedResult)
3300+}
3301+
3302+func TestStateDecodesCorrectly(t *testing.T) {
3303+ nonce, redirect, _ := decodeState("c29tZV9ub25jZV90b190ZXN0OnNvbWVfc3RhdGVfdG9fdGVzdA", true)
3304+
3305+ assert.Equal(t, "some_nonce_to_test", nonce)
3306+ assert.Equal(t, "some_state_to_test", redirect)
3307+
3308+ nonce2, redirect2, _ := decodeState("some_nonce_to_test:some_state_to_test", false)
3309+
3310+ assert.Equal(t, "some_nonce_to_test", nonce2)
3311+ assert.Equal(t, "some_state_to_test", redirect2)
3312+}
3313+
32913314func TestAuthOnlyAllowedEmails(t *testing.T) {
32923315 testCases := []struct {
32933316 name string
pkg/apis/options/options.go+2
@@ -61,6 +61,7 @@ type Options struct {
6161 SSLInsecureSkipVerify bool `flag:"ssl-insecure-skip-verify" cfg:"ssl_insecure_skip_verify"`
6262 SkipAuthPreflight bool `flag:"skip-auth-preflight" cfg:"skip_auth_preflight"`
6363 ForceJSONErrors bool `flag:"force-json-errors" cfg:"force_json_errors"`
64+ EncodeState bool `flag:"encode-state" cfg:"encode_state"`
6465 AllowQuerySemicolons bool `flag:"allow-query-semicolons" cfg:"allow_query_semicolons"`
6566
6667 SignatureKey string `flag:"signature-key" cfg:"signature_key"`
@@ -128,6 +129,7 @@ func NewFlagSet() *pflag.FlagSet {
128129 flagSet.Bool("ssl-insecure-skip-verify", false, "skip validation of certificates presented when using HTTPS providers")
129130 flagSet.Bool("skip-jwt-bearer-tokens", false, "will skip requests that have verified JWT bearer tokens (default false)")
130131 flagSet.Bool("force-json-errors", false, "will force JSON errors instead of HTTP error pages or redirects")
132+ flagSet.Bool("encode-state", false, "will encode oauth state with base64")
131133 flagSet.Bool("allow-query-semicolons", false, "allow the use of semicolons in query args")
132134 flagSet.StringSlice("extra-jwt-issuers", []string{}, "if skip-jwt-bearer-tokens is set, a list of extra JWT issuer=audience pairs (where the issuer URL has a .well-known/openid-configuration or a .well-known/jwks.json)")
133135