Merge pull request #605 from DimaSalakhov/basicAuth
Add basic auth middleware
Peter Bourgon authored 6 years ago
GitHub committed 6 years ago
0 | This package provides a Basic Authentication middleware. | |
1 | ||
2 | It'll try to compare credentials from Authentication request header to a username/password pair in middleware constructor. | |
3 | ||
4 | More details about this type of authentication can be found in [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). | |
5 | ||
6 | ## Usage | |
7 | ||
8 | ```go | |
9 | import httptransport "github.com/go-kit/kit/transport/http" | |
10 | ||
11 | httptransport.NewServer( | |
12 | AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm")(makeUppercaseEndpoint()), | |
13 | decodeMappingsRequest, | |
14 | httptransport.EncodeJSONResponse, | |
15 | httptransport.ServerBefore(httptransport.PopulateRequestContext), | |
16 | ) | |
17 | ``` | |
18 | ||
19 | For AuthMiddleware to be able to pick up the Authentication header from an HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```.⏎ |
0 | package basic | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "context" | |
5 | "crypto/sha256" | |
6 | "crypto/subtle" | |
7 | "encoding/base64" | |
8 | "fmt" | |
9 | "net/http" | |
10 | "strings" | |
11 | ||
12 | "github.com/go-kit/kit/endpoint" | |
13 | httptransport "github.com/go-kit/kit/transport/http" | |
14 | ) | |
15 | ||
16 | // AuthError represents an authorization error. | |
17 | type AuthError struct { | |
18 | Realm string | |
19 | } | |
20 | ||
21 | // StatusCode is an implementation of the StatusCoder interface in go-kit/http. | |
22 | func (AuthError) StatusCode() int { | |
23 | return http.StatusUnauthorized | |
24 | } | |
25 | ||
26 | // Error is an implementation of the Error interface. | |
27 | func (AuthError) Error() string { | |
28 | return http.StatusText(http.StatusUnauthorized) | |
29 | } | |
30 | ||
31 | // Headers is an implementation of the Headerer interface in go-kit/http. | |
32 | func (e AuthError) Headers() http.Header { | |
33 | return http.Header{ | |
34 | "Content-Type": []string{"text/plain; charset=utf-8"}, | |
35 | "X-Content-Type-Options": []string{"nosniff"}, | |
36 | "WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}, | |
37 | } | |
38 | } | |
39 | ||
40 | // parseBasicAuth parses an HTTP Basic Authentication string. | |
41 | // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true). | |
42 | func parseBasicAuth(auth string) (username, password []byte, ok bool) { | |
43 | const prefix = "Basic " | |
44 | if !strings.HasPrefix(auth, prefix) { | |
45 | return | |
46 | } | |
47 | c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) | |
48 | if err != nil { | |
49 | return | |
50 | } | |
51 | ||
52 | s := bytes.IndexByte(c, ':') | |
53 | if s < 0 { | |
54 | return | |
55 | } | |
56 | return c[:s], c[s+1:], true | |
57 | } | |
58 | ||
59 | // Returns a hash of a given slice. | |
60 | func toHashSlice(s []byte) []byte { | |
61 | hash := sha256.Sum256(s) | |
62 | return hash[:] | |
63 | } | |
64 | ||
65 | // AuthMiddleware returns a Basic Authentication middleware for a particular user and password. | |
66 | func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { | |
67 | requiredUserBytes := toHashSlice([]byte(requiredUser)) | |
68 | requiredPasswordBytes := toHashSlice([]byte(requiredPassword)) | |
69 | ||
70 | return func(next endpoint.Endpoint) endpoint.Endpoint { | |
71 | return func(ctx context.Context, request interface{}) (interface{}, error) { | |
72 | auth, ok := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) | |
73 | if !ok { | |
74 | return nil, AuthError{realm} | |
75 | } | |
76 | ||
77 | givenUser, givenPassword, ok := parseBasicAuth(auth) | |
78 | if !ok { | |
79 | return nil, AuthError{realm} | |
80 | } | |
81 | ||
82 | givenUserBytes := toHashSlice(givenUser) | |
83 | givenPasswordBytes := toHashSlice(givenPassword) | |
84 | ||
85 | if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 || | |
86 | subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 { | |
87 | return nil, AuthError{realm} | |
88 | } | |
89 | ||
90 | return next(ctx, request) | |
91 | } | |
92 | } | |
93 | } |
0 | package basic | |
1 | ||
2 | import ( | |
3 | "context" | |
4 | "encoding/base64" | |
5 | "fmt" | |
6 | "testing" | |
7 | ||
8 | httptransport "github.com/go-kit/kit/transport/http" | |
9 | ) | |
10 | ||
11 | func TestWithBasicAuth(t *testing.T) { | |
12 | requiredUser := "test-user" | |
13 | requiredPassword := "test-pass" | |
14 | realm := "test realm" | |
15 | ||
16 | type want struct { | |
17 | result interface{} | |
18 | err error | |
19 | } | |
20 | tests := []struct { | |
21 | name string | |
22 | authHeader interface{} | |
23 | want want | |
24 | }{ | |
25 | {"Isn't valid with nil header", nil, want{nil, AuthError{realm}}}, | |
26 | {"Isn't valid with non-string header", 42, want{nil, AuthError{realm}}}, | |
27 | {"Isn't valid without authHeader", "", want{nil, AuthError{realm}}}, | |
28 | {"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}}, | |
29 | {"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}}, | |
30 | {"Is valid for correct creds", makeAuthString(requiredUser, requiredPassword), want{true, nil}}, | |
31 | } | |
32 | for _, tt := range tests { | |
33 | t.Run(tt.name, func(t *testing.T) { | |
34 | ctx := context.WithValue(context.TODO(), httptransport.ContextKeyRequestAuthorization, tt.authHeader) | |
35 | ||
36 | result, err := AuthMiddleware(requiredUser, requiredPassword, realm)(passedValidation)(ctx, nil) | |
37 | if result != tt.want.result || err != tt.want.err { | |
38 | t.Errorf("WithBasicAuth() = result: %v, err: %v, want result: %v, want error: %v", result, err, tt.want.result, tt.want.err) | |
39 | } | |
40 | }) | |
41 | } | |
42 | } | |
43 | ||
44 | func makeAuthString(user string, password string) string { | |
45 | data := []byte(fmt.Sprintf("%s:%s", user, password)) | |
46 | return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(data)) | |
47 | } | |
48 | ||
49 | func passedValidation(ctx context.Context, request interface{}) (response interface{}, err error) { | |
50 | return true, nil | |
51 | } |