Codebase list golang-github-go-kit-kit / 7862b3f
Merge pull request #605 from DimaSalakhov/basicAuth Add basic auth middleware Peter Bourgon authored 6 years ago GitHub committed 6 years ago
3 changed file(s) with 166 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
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 }