Skip to content
This repository was archived by the owner on Dec 7, 2020. It is now read-only.

Commit 32d424a

Browse files
Limessdosyara
authored andcommitted
cb/sm: add option to disable csrf; encode state parameter with redirect URL in json
1 parent c1e1afe commit 32d424a

File tree

9 files changed

+77
-16
lines changed

9 files changed

+77
-16
lines changed

config.go

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func newDefaultConfig() *Config {
4343
EnableDefaultDeny: true,
4444
EnableSessionCookies: true,
4545
EnableTokenHeader: true,
46+
EnableCSRFCheck: false,
4647
HTTPOnlyCookie: true,
4748
Headers: make(map[string]string),
4849
LetsEncryptCacheDir: "./cache/",

cookies.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package main
1717

1818
import (
1919
"encoding/base64"
20+
"encoding/json"
2021
"net/http"
2122
"strconv"
2223
"strings"
@@ -119,16 +120,26 @@ func (r *oauthProxy) dropRefreshTokenCookie(req *http.Request, w http.ResponseWr
119120
r.dropCookieWithChunks(req, w, r.config.CookieRefreshName, value, duration)
120121
}
121122

123+
type StateParameter struct {
124+
Token string `json:"token"`
125+
Url string `json:"url"`
126+
}
127+
122128
// writeStateParameterCookie sets a state parameter cookie into the response
123-
func (r *oauthProxy) writeStateParameterCookie(req *http.Request, w http.ResponseWriter) string {
129+
func (r *oauthProxy) writeStateParameterCookie(req *http.Request, w http.ResponseWriter) (string, error) {
124130
uuid, err := uuid.NewV4()
125131
if err != nil {
126132
w.WriteHeader(http.StatusInternalServerError)
127133
}
128134
requestURI := base64.StdEncoding.EncodeToString([]byte(req.URL.RequestURI()))
129135
r.dropCookie(w, req.Host, requestURICookie, requestURI, 0)
130136
r.dropCookie(w, req.Host, requestStateCookie, uuid.String(), 0)
131-
return uuid.String()
137+
138+
stateParam := StateParameter{Token: uuid.String(),
139+
Url: req.URL.RequestURI()}
140+
output, err := json.Marshal(stateParam)
141+
142+
return string(output), err
132143
}
133144

134145
// clearAllCookies is just a helper function for the below

doc.go

+2
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ type Config struct {
252252
LocalhostMetrics bool `json:"localhost-metrics" yaml:"localhost-metrics" usage:"enforces the metrics page can only been requested from 127.0.0.1"`
253253
// EnableCompression enables gzip compression for response
254254
EnableCompression bool `json:"enable-compression" yaml:"enable-compression" usage:"enable gzip compression for response"`
255+
// EnableCSRFCheck enables CSRF protection between authorise/callback using cookies and state parameter
256+
EnableCSRFCheck bool `json:"enable-csrf-check" yaml:"enable-csrf-check" usage:"enable crsf check between authorise and callback"`
255257

256258
// AccessTokenDuration is default duration applied to the access token cookie
257259
AccessTokenDuration time.Duration `json:"access-token-duration" yaml:"access-token-duration" usage:"fallback cookie duration for the access token when using refresh tokens"`

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ require (
44
github.com/PuerkitoBio/purell v1.1.0
55
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
66
github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f
7+
github.com/client9/misspell v0.3.4 // indirect
78
github.com/codegangsta/negroni v1.0.0 // indirect
89
github.com/coreos/go-oidc v0.0.0-20171020180921-e860bd55bfa7
910
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f h1:SaJ6yqg936T
77
github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
88
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
99
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
10+
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
11+
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
1012
github.com/codegangsta/negroni v1.0.0 h1:+aYywywx4bnKXWvoWtRfJ91vC59NbEhEY03sZjQhbVY=
1113
github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0=
1214
github.com/coreos/go-oidc v0.0.0-20171020180921-e860bd55bfa7 h1:UeXD8Kli+SWhDlj1ikNXs9NKHsm2SR9dVnGiKq86DJ4=

handlers.go

+28-7
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,23 @@ func (r *oauthProxy) getRedirectionURL(w http.ResponseWriter, req *http.Request)
5858
redirect = r.config.RedirectionURL
5959
}
6060

61-
state, _ := req.Cookie(requestStateCookie)
62-
if state != nil && req.URL.Query().Get("state") != state.Value {
63-
r.log.Error("state parameter mismatch")
64-
w.WriteHeader(http.StatusForbidden)
65-
return ""
61+
if r.config.EnableCSRFCheck {
62+
state, _ := req.Cookie(requestStateCookie)
63+
64+
stateParameter := req.URL.Query().Get("state")
65+
stateParam := StateParameter{}
66+
if stateParameter != "" {
67+
err := json.Unmarshal([]byte(stateParameter), &stateParam)
68+
if err != nil {
69+
r.log.Warn("failed to deserialise state parameter from json")
70+
}
71+
}
72+
73+
if state != nil && stateParam.Token != state.Value {
74+
r.log.Error("state parameter mismatch")
75+
w.WriteHeader(http.StatusForbidden)
76+
return ""
77+
}
6678
}
6779
return fmt.Sprintf("%s%s", redirect, r.config.WithOAuthURI("callback"))
6880
}
@@ -209,8 +221,17 @@ func (r *oauthProxy) oauthCallbackHandler(w http.ResponseWriter, req *http.Reque
209221

210222
// step: decode the request variable
211223
redirectURI := "/"
212-
if req.URL.Query().Get("state") != "" {
213-
if encodedRequestURI, _ := req.Cookie(requestURICookie); encodedRequestURI != nil {
224+
stateParameter := req.URL.Query().Get("state")
225+
if stateParameter != "" {
226+
stateParam := StateParameter{}
227+
err := json.Unmarshal([]byte(stateParameter), &stateParam)
228+
if err != nil {
229+
r.log.Warn("failed to deserialise state parameter from json")
230+
}
231+
232+
if stateParam.Url != "" {
233+
redirectURI = stateParam.Url
234+
} else if encodedRequestURI, _ := req.Cookie(requestURICookie); encodedRequestURI != nil {
214235
decoded, _ := base64.StdEncoding.DecodeString(encodedRequestURI.Value)
215236
redirectURI = string(decoded)
216237
}

middleware_test.go

+20-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616
package main
1717

1818
import (
19+
"encoding/json"
1920
"fmt"
2021
"io/ioutil"
2122
"log"
@@ -73,6 +74,8 @@ type fakeRequest struct {
7374

7475
// advanced test cases
7576
ExpectedCookiesValidator map[string]func(string) bool
77+
78+
ExpectedStateUrl string
7679
}
7780

7881
type fakeProxy struct {
@@ -223,11 +226,24 @@ func (f *fakeProxy) RunTests(t *testing.T, requests []fakeRequest) {
223226
if c.ExpectedCode != 0 {
224227
assert.Equal(t, c.ExpectedCode, status, "case %d, expected status code: %d, got: %d", i, c.ExpectedCode, status)
225228
}
226-
if c.ExpectedLocation != "" {
229+
if c.ExpectedLocation != "" || c.ExpectedStateUrl != "" {
227230
l, _ := url.Parse(resp.Header().Get("Location"))
228-
assert.True(t, strings.Contains(l.String(), c.ExpectedLocation), "expected location to contain %s", l.String())
229-
if l.Query().Get("state") != "" {
230-
state, err := uuid.FromString(l.Query().Get("state"))
231+
if c.ExpectedLocation != "" {
232+
assert.True(t, strings.Contains(l.String(), c.ExpectedLocation), "expected location to contain %s", l.String())
233+
}
234+
stateStr := l.Query().Get("state")
235+
if stateStr != "" {
236+
stateParam := StateParameter{}
237+
err := json.Unmarshal([]byte(stateStr), &stateParam)
238+
if err != nil {
239+
assert.Fail(t, "failed to deserialise state parameter from json, got: %s with error %s", stateStr, err)
240+
}
241+
242+
if c.ExpectedStateUrl != "" {
243+
assert.Equal(t, c.ExpectedStateUrl, stateParam.Url, "expected state url to contain %s", stateParam.Url)
244+
}
245+
246+
state, err := uuid.FromString(stateParam.Token)
231247
if err != nil {
232248
assert.Fail(t, "expected state parameter with valid UUID, got: %s with error %s", state.String(), err)
233249
}

misc.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"fmt"
2121
"net/http"
22+
"net/url"
2223
"path"
2324
"strings"
2425
"time"
@@ -97,8 +98,13 @@ func (r *oauthProxy) redirectToAuthorization(w http.ResponseWriter, req *http.Re
9798
}
9899

99100
// step: add a state referrer to the authorization page
100-
uuid := r.writeStateParameterCookie(req, w)
101-
authQuery := fmt.Sprintf("?state=%s", uuid)
101+
state, err := r.writeStateParameterCookie(req, w)
102+
if err != nil {
103+
r.log.Error("failed to create state parameter")
104+
w.WriteHeader(http.StatusInternalServerError)
105+
return r.revokeProxy(w, req)
106+
}
107+
authQuery := fmt.Sprintf("?state=%s", url.QueryEscape(state))
102108

103109
// step: if verification is switched off, we can't authorization
104110
if r.config.SkipTokenVerification {

misc_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ func TestRedirectToAuthorizationUnauthorized(t *testing.T) {
3333
func TestRedirectToAuthorization(t *testing.T) {
3434
requests := []fakeRequest{
3535
{
36-
URI: "/admin",
36+
URI: "/admin?blah=1",
3737
Redirects: true,
3838
ExpectedLocation: "/oauth/authorize?state",
39+
ExpectedStateUrl: "/admin?blah=1",
3940
ExpectedCode: http.StatusSeeOther,
4041
},
4142
}

0 commit comments

Comments
 (0)