git.iko.soy/typeable/oauth2-proxy
on master help sign in
Add Basic Auth session loader middleware
commit7d6f2a3f454ced4d25213ed96a3e80be53d196e4 authorJoel Speed <joel.speed@hotmail.co.uk> dateSun 5 Jul 2020, 17:24 (6 years ago) parentc81a7ed
6 files changed+358−60
pkg/middleware/basic_session.go+88 pkg/middleware/basic_session_test.go+132 pkg/middleware/jwt_session.go+2−20 pkg/middleware/jwt_session_test.go−40 pkg/middleware/session_utils.go+33 pkg/middleware/session_utils_test.go+103
pkg/middleware/basic_session.gonew file+88
@@ -0,0 +1,88 @@
1+package middleware
2+
3+import (
4+ "fmt"
5+ "net/http"
6+
7+ "github.com/justinas/alice"
8+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
10+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
11+)
12+
13+func NewBasicAuthSessionLoader(validator basic.Validator) alice.Constructor {
14+ return func(next http.Handler) http.Handler {
15+ return loadBasicAuthSession(validator, next)
16+ }
17+}
18+
19+// loadBasicAuthSession attmepts to load a session from basic auth credentials
20+// stored in an Authorization header within the request.
21+// If no authorization header is found, or the header is invalid, no session
22+// will be loaded and the request will be passed to the next handler.
23+// If a session was loaded by a previous handler, it will not be replaced.
24+func loadBasicAuthSession(validator basic.Validator, next http.Handler) http.Handler {
25+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
26+ scope := GetRequestScope(req)
27+ // If scope is nil, this will panic.
28+ // A scope should always be injected before this handler is called.
29+ if scope.Session != nil {
30+ // The session was already loaded, pass to the next handler
31+ next.ServeHTTP(rw, req)
32+ return
33+ }
34+
35+ session, err := getBasicSession(validator, req)
36+ if err != nil {
37+ logger.Printf("Error retrieving session from token in Authorization header: %v", err)
38+ }
39+
40+ // Add the session to the scope if it was found
41+ scope.Session = session
42+ next.ServeHTTP(rw, req)
43+ })
44+}
45+
46+// getBasicSession attempts to load a basic session from the request.
47+// If the credentials in the request exist within the htpasswdMap,
48+// a new session will be created.
49+func getBasicSession(validator basic.Validator, req *http.Request) (*sessionsapi.SessionState, error) {
50+ auth := req.Header.Get("Authorization")
51+ if auth == "" {
52+ // No auth header provided, so don't attempt to load a session
53+ return nil, nil
54+ }
55+
56+ user, password, err := findBasicCredentialsFromHeader(auth)
57+ if err != nil {
58+ return nil, err
59+ }
60+
61+ if validator.Validate(user, password) {
62+ logger.PrintAuthf(user, req, logger.AuthSuccess, "Authenticated via basic auth and HTpasswd File")
63+ return &sessionsapi.SessionState{User: user}, nil
64+ }
65+
66+ logger.PrintAuthf(user, req, logger.AuthFailure, "Invalid authentication via basic auth: not in Htpasswd File")
67+ return nil, nil
68+}
69+
70+// findBasicCredentialsFromHeader finds basic auth credneitals from the
71+// Authorization header of a given request.
72+func findBasicCredentialsFromHeader(header string) (string, string, error) {
73+ tokenType, token, err := splitAuthHeader(header)
74+ if err != nil {
75+ return "", "", err
76+ }
77+
78+ if tokenType != "Basic" {
79+ return "", "", fmt.Errorf("invalid Authorization header: %q", header)
80+ }
81+
82+ user, password, err := getBasicAuthCredentials(token)
83+ if err != nil {
84+ return "", "", fmt.Errorf("error decoding basic auth credentials: %v", err)
85+ }
86+
87+ return user, password, nil
88+}
pkg/middleware/basic_session_test.gonew file+132
@@ -0,0 +1,132 @@
1+package middleware
2+
3+import (
4+ "context"
5+ "fmt"
6+ "net/http"
7+ "net/http/httptest"
8+
9+ middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
10+ sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
11+ . "github.com/onsi/ginkgo"
12+ . "github.com/onsi/ginkgo/extensions/table"
13+ . "github.com/onsi/gomega"
14+)
15+
16+const (
17+ adminUser = "admin"
18+ adminPassword = "Adm1n1str$t0r"
19+ user1 = "user1"
20+ user1Password = "UsErOn3P455"
21+ user2 = "user2"
22+ user2Password = "us3r2P455W0Rd!"
23+)
24+
25+var _ = Describe("Basic Auth Session Suite", func() {
26+ Context("BasicAuthSessionLoader", func() {
27+
28+ type basicAuthSessionLoaderTableInput struct {
29+ authorizationHeader string
30+ existingSession *sessionsapi.SessionState
31+ expectedSession *sessionsapi.SessionState
32+ }
33+
34+ DescribeTable("with an authorization header",
35+ func(in basicAuthSessionLoaderTableInput) {
36+ scope := &middlewareapi.RequestScope{
37+ Session: in.existingSession,
38+ }
39+
40+ // Set up the request with the authorization header and a request scope
41+ req := httptest.NewRequest("", "/", nil)
42+ req.Header.Set("Authorization", in.authorizationHeader)
43+ contextWithScope := context.WithValue(req.Context(), requestScopeKey, scope)
44+ req = req.WithContext(contextWithScope)
45+
46+ rw := httptest.NewRecorder()
47+
48+ validator := fakeBasicValidator{
49+ users: map[string]string{
50+ adminUser: adminPassword,
51+ user1: user1Password,
52+ user2: user2Password,
53+ },
54+ }
55+
56+ // Create the handler with a next handler that will capture the session
57+ // from the scope
58+ var gotSession *sessionsapi.SessionState
59+ handler := NewBasicAuthSessionLoader(validator)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60+ gotSession = r.Context().Value(requestScopeKey).(*middlewareapi.RequestScope).Session
61+ }))
62+ handler.ServeHTTP(rw, req)
63+
64+ Expect(gotSession).To(Equal(in.expectedSession))
65+ },
66+ Entry("<no value>", basicAuthSessionLoaderTableInput{
67+ authorizationHeader: "",
68+ existingSession: nil,
69+ expectedSession: nil,
70+ }),
71+ Entry("abcdef", basicAuthSessionLoaderTableInput{
72+ authorizationHeader: "abcdef",
73+ existingSession: nil,
74+ expectedSession: nil,
75+ }),
76+ Entry("abcdef (with existing session)", basicAuthSessionLoaderTableInput{
77+ authorizationHeader: "abcdef",
78+ existingSession: &sessionsapi.SessionState{User: "user"},
79+ expectedSession: &sessionsapi.SessionState{User: "user"},
80+ }),
81+ Entry("Bearer <password>", basicAuthSessionLoaderTableInput{
82+ authorizationHeader: fmt.Sprintf("Bearer %s", adminPassword),
83+ existingSession: nil,
84+ expectedSession: nil,
85+ }),
86+ Entry("Basic <password>", basicAuthSessionLoaderTableInput{
87+ authorizationHeader: fmt.Sprintf("Basic %s", adminPassword),
88+ existingSession: nil,
89+ expectedSession: nil,
90+ }),
91+ Entry("Basic Base64(:<password>) (with existing session)", basicAuthSessionLoaderTableInput{
92+ authorizationHeader: "Basic OlVzRXJPbjNQNDU1",
93+ existingSession: &sessionsapi.SessionState{User: "user"},
94+ expectedSession: &sessionsapi.SessionState{User: "user"},
95+ }),
96+ Entry("Basic Base64(user1:<user1Password>)", basicAuthSessionLoaderTableInput{
97+ authorizationHeader: "Basic dXNlcjE6VXNFck9uM1A0NTU=",
98+ existingSession: nil,
99+ expectedSession: &sessionsapi.SessionState{User: "user1"},
100+ }),
101+ Entry("Basic Base64(user2:<user1Password>)", basicAuthSessionLoaderTableInput{
102+ authorizationHeader: "Basic dXNlcjI6VXNFck9uM1A0NTU=",
103+ existingSession: nil,
104+ expectedSession: nil,
105+ }),
106+ Entry("Basic Base64(user2:<user2Password>)", basicAuthSessionLoaderTableInput{
107+ authorizationHeader: "Basic dXNlcjI6dXMzcjJQNDU1VzBSZCE=",
108+ existingSession: nil,
109+ expectedSession: &sessionsapi.SessionState{User: "user2"},
110+ }),
111+ Entry("Basic Base64(admin:<adminPassword>)", basicAuthSessionLoaderTableInput{
112+ authorizationHeader: "Basic YWRtaW46QWRtMW4xc3RyJHQwcg==",
113+ existingSession: nil,
114+ expectedSession: &sessionsapi.SessionState{User: "admin"},
115+ }),
116+ )
117+ })
118+})
119+
120+type fakeBasicValidator struct {
121+ users map[string]string
122+}
123+
124+func (f fakeBasicValidator) Validate(user, password string) bool {
125+ if f.users == nil {
126+ return false
127+ }
128+ if realPassword, ok := f.users[user]; ok {
129+ return realPassword == password
130+ }
131+ return false
132+}
pkg/middleware/jwt_session.go+2−20
@@ -2,11 +2,9 @@ package middleware
22
33import (
44 "context"
5− "encoding/base64"
65 "fmt"
76 "net/http"
87 "regexp"
9− "strings"
108
119 "github.com/coreos/go-oidc"
1210 "github.com/justinas/alice"
@@ -115,16 +113,10 @@ func (j *jwtSessionLoader) findBearerTokenFromHeader(header string) (string, err
115113
116114// getBasicToken tries to extract a token from the basic value provided.
117115func (j *jwtSessionLoader) getBasicToken(token string) (string, error) {
118− b, err := base64.StdEncoding.DecodeString(token)
116+ user, password, err := getBasicAuthCredentials(token)
119117 if err != nil {
120− return "", fmt.Errorf("invalid basic auth token: %v", err)
121− }
122−
123− pair := strings.SplitN(string(b), ":", 2)
124− if len(pair) != 2 {
125− return "", fmt.Errorf("invalid format: %q", b)
118+ return "", err
126119 }
127− user, password := pair[0], pair[1]
128120
129121 // check user, user+password, or just password for a token
130122 if j.jwtRegex.MatchString(user) {
@@ -140,16 +132,6 @@ func (j *jwtSessionLoader) getBasicToken(token string) (string, error) {
140132 return "", fmt.Errorf("invalid basic auth token found in authorization header")
141133}
142134
143−// splitAuthHeader takes the auth header value and splits it into the token type
144−// and the token value.
145−func splitAuthHeader(header string) (string, string, error) {
146− s := strings.Split(header, " ")
147− if len(s) != 2 {
148− return "", "", fmt.Errorf("invalid authorization header: %q", header)
149− }
150− return s[0], s[1], nil
151−}
152−
153135// createSessionStateFromBearerToken is a default implementation for converting
154136// a JWT into a session state.
155137func createSessionStateFromBearerToken(ctx context.Context, rawIDToken string, idToken *oidc.IDToken) (*sessionsapi.SessionState, error) {
pkg/middleware/jwt_session_test.go−40
@@ -381,46 +381,6 @@ Nnc3a3lGVWFCNUMxQnNJcnJMTWxka1dFaHluYmI4Ongtb2F1dGgtYmFzaWM=`
381381 )
382382 })
383383
384− Context("splitAuthHeader", func() {
385− type splitAuthTableInput struct {
386− header string
387− expectedErr error
388− expectedTokenType string
389− expectedTokenValue string
390− }
391−
392− DescribeTable("with a header value",
393− func(in splitAuthTableInput) {
394− tt, tv, err := splitAuthHeader(in.header)
395− if in.expectedErr != nil {
396− Expect(err).To(MatchError(in.expectedErr))
397− } else {
398− Expect(err).ToNot(HaveOccurred())
399− }
400− Expect(tt).To(Equal(in.expectedTokenType))
401− Expect(tv).To(Equal(in.expectedTokenValue))
402− },
403− Entry("Bearer abcdef", splitAuthTableInput{
404− header: "Bearer abcdef",
405− expectedErr: nil,
406− expectedTokenType: "Bearer",
407− expectedTokenValue: "abcdef",
408− }),
409− Entry("Bearer", splitAuthTableInput{
410− header: "Bearer",
411− expectedErr: errors.New("invalid authorization header: \"Bearer\""),
412− expectedTokenType: "",
413− expectedTokenValue: "",
414− }),
415− Entry("Bearer abc def", splitAuthTableInput{
416− header: "Bearer abc def",
417− expectedErr: errors.New("invalid authorization header: \"Bearer abc def\""),
418− expectedTokenType: "",
419− expectedTokenValue: "",
420− }),
421− )
422− })
423−
424384 Context("createSessionStateFromBearerToken", func() {
425385 ctx := context.Background()
426386 expiresFuture := time.Now().Add(time.Duration(5) * time.Minute)
pkg/middleware/session_utils.gonew file+33
@@ -0,0 +1,33 @@
1+package middleware
2+
3+import (
4+ "encoding/base64"
5+ "fmt"
6+ "strings"
7+)
8+
9+// splitAuthHeader takes the auth header value and splits it into the token type
10+// and the token value.
11+func splitAuthHeader(header string) (string, string, error) {
12+ s := strings.Split(header, " ")
13+ if len(s) != 2 {
14+ return "", "", fmt.Errorf("invalid authorization header: %q", header)
15+ }
16+ return s[0], s[1], nil
17+}
18+
19+// getBasicAuthCredentials decodes a basic auth token and extracts the user
20+// and password pair.
21+func getBasicAuthCredentials(token string) (string, string, error) {
22+ b, err := base64.StdEncoding.DecodeString(token)
23+ if err != nil {
24+ return "", "", fmt.Errorf("invalid basic auth token: %v", err)
25+ }
26+
27+ pair := strings.SplitN(string(b), ":", 2)
28+ if len(pair) != 2 {
29+ return "", "", fmt.Errorf("invalid format: %q", b)
30+ }
31+ // user, password
32+ return pair[0], pair[1], nil
33+}
pkg/middleware/session_utils_test.gonew file+103
@@ -0,0 +1,103 @@
1+package middleware
2+
3+import (
4+ "errors"
5+
6+ . "github.com/onsi/ginkgo"
7+ . "github.com/onsi/ginkgo/extensions/table"
8+ . "github.com/onsi/gomega"
9+)
10+
11+var _ = Describe("Session utilities suite", func() {
12+ Context("splitAuthHeader", func() {
13+ type splitAuthTableInput struct {
14+ header string
15+ expectedErr error
16+ expectedTokenType string
17+ expectedTokenValue string
18+ }
19+
20+ DescribeTable("with a header value",
21+ func(in splitAuthTableInput) {
22+ tt, tv, err := splitAuthHeader(in.header)
23+ if in.expectedErr != nil {
24+ Expect(err).To(MatchError(in.expectedErr))
25+ } else {
26+ Expect(err).ToNot(HaveOccurred())
27+ }
28+ Expect(tt).To(Equal(in.expectedTokenType))
29+ Expect(tv).To(Equal(in.expectedTokenValue))
30+ },
31+ Entry("Bearer abcdef", splitAuthTableInput{
32+ header: "Bearer abcdef",
33+ expectedErr: nil,
34+ expectedTokenType: "Bearer",
35+ expectedTokenValue: "abcdef",
36+ }),
37+ Entry("Bearer", splitAuthTableInput{
38+ header: "Bearer",
39+ expectedErr: errors.New("invalid authorization header: \"Bearer\""),
40+ expectedTokenType: "",
41+ expectedTokenValue: "",
42+ }),
43+ Entry("Bearer abc def", splitAuthTableInput{
44+ header: "Bearer abc def",
45+ expectedErr: errors.New("invalid authorization header: \"Bearer abc def\""),
46+ expectedTokenType: "",
47+ expectedTokenValue: "",
48+ }),
49+ )
50+ })
51+
52+ Context("getBasicAuthCredentials", func() {
53+ type getBasicAuthCredentialsTableInput struct {
54+ token string
55+ expectedErr error
56+ expectedUser string
57+ expectedPassword string
58+ }
59+
60+ DescribeTable("from token",
61+ func(in getBasicAuthCredentialsTableInput) {
62+ user, password, err := getBasicAuthCredentials(in.token)
63+ if in.expectedErr != nil {
64+ Expect(err).To(MatchError(in.expectedErr))
65+ } else {
66+ Expect(err).ToNot(HaveOccurred())
67+ }
68+ Expect(user).To(Equal(in.expectedUser))
69+ Expect(password).To(Equal(in.expectedPassword))
70+ },
71+ Entry("<no value>", getBasicAuthCredentialsTableInput{
72+ token: "",
73+ expectedErr: errors.New("invalid format: \"\""),
74+ expectedUser: "",
75+ expectedPassword: "",
76+ }),
77+ Entry("invalid-base64", getBasicAuthCredentialsTableInput{
78+ token: "invalid-base64",
79+ expectedErr: errors.New("invalid basic auth token: illegal base64 data at input byte 7"),
80+ expectedUser: "",
81+ expectedPassword: "",
82+ }),
83+ Entry("Base64(some-user:some-password)", getBasicAuthCredentialsTableInput{
84+ token: "c29tZS11c2VyOnNvbWUtcGFzc3dvcmQ=",
85+ expectedErr: nil,
86+ expectedUser: "some-user",
87+ expectedPassword: "some-password",
88+ }),
89+ Entry("Base64(no-password:)", getBasicAuthCredentialsTableInput{
90+ token: "bm8tcGFzc3dvcmQ6",
91+ expectedErr: nil,
92+ expectedUser: "no-password",
93+ expectedPassword: "",
94+ }),
95+ Entry("Base64(:no-user)", getBasicAuthCredentialsTableInput{
96+ token: "Om5vLXVzZXI=",
97+ expectedErr: nil,
98+ expectedUser: "",
99+ expectedPassword: "no-user",
100+ }),
101+ )
102+ })
103+})