Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit bc022fb

Browse files
brezinajntuunit
andauthored
Add possibility to encode the state param as UrlEncodedBase64 (oauth2-proxy#2312)
* Add possibility to encode the state param as UrlEncodedBase64 * Update CHANGELOG.md * Update oauthproxy.go Co-authored-by: Jan Larwig <[email protected]> --------- Co-authored-by: Jan Larwig <[email protected]>
1 parent be84906 commit bc022fb

File tree

5 files changed

+50
-9
lines changed

5 files changed

+50
-9
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -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)

Diff for: docs/docs/configuration/overview.md

+1
Original file line numberDiff line numberDiff line change
@@ -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}

Diff for: oauthproxy.go

+22-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
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

Diff for: oauthproxy_test.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -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+
32913314
func TestAuthOnlyAllowedEmails(t *testing.T) {
32923315
testCases := []struct {
32933316
name string

Diff for: pkg/apis/options/options.go

+2
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)