-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add basic auth middleware #605
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
`package auth/basic` provides a Basic Authentication middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). | ||
|
||
## Usage | ||
|
||
```go | ||
import httptransport "github.com/go-kit/kit/transport/http" | ||
|
||
httptransport.NewServer( | ||
endpoint.Chain(AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm"))(makeUppercaseEndpoint()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no reason to use the Chain helper here, I don't think? |
||
decodeMappingsRequest, | ||
httptransport.EncodeJSONResponse, | ||
httptransport.ServerBefore(httptransport.PopulateRequestContext), | ||
) | ||
``` | ||
|
||
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)```. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package basic | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"crypto/sha256" | ||
"crypto/subtle" | ||
"encoding/base64" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
|
||
"github.com/go-kit/kit/endpoint" | ||
httptransport "github.com/go-kit/kit/transport/http" | ||
) | ||
|
||
// AuthError represents an authorization error. | ||
type AuthError struct { | ||
Realm string | ||
} | ||
|
||
// StatusCode is an implementation of the StatusCoder interface in go-kit/http. | ||
func (AuthError) StatusCode() int { | ||
return http.StatusUnauthorized | ||
} | ||
|
||
// Error is an implementation of the Error interface. | ||
func (AuthError) Error() string { | ||
return http.StatusText(http.StatusUnauthorized) | ||
} | ||
|
||
// Headers is an implementation of the Headerer interface in go-kit/http. | ||
func (e AuthError) Headers() http.Header { | ||
return http.Header{ | ||
"Content-Type": []string{"text/plain; charset=utf-8"}, | ||
"X-Content-Type-Options": []string{"nosniff"}, | ||
"WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please use a trailing comma and put the closing brace on the next line. |
||
} | ||
|
||
// parseBasicAuth parses an HTTP Basic Authentication string. | ||
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ([]byte("Aladdin"), []byte("open sesame"), true). | ||
func parseBasicAuth(auth string) (username, password []byte, ok bool) { | ||
const prefix = "Basic " | ||
if !strings.HasPrefix(auth, prefix) { | ||
return | ||
} | ||
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) | ||
if err != nil { | ||
return | ||
} | ||
|
||
s := bytes.IndexByte(c, ':') | ||
if s < 0 { | ||
return | ||
} | ||
return c[:s], c[s+1:], true | ||
} | ||
|
||
// Returns a hash of a given slice. | ||
func toHashSlice(s []byte) []byte { | ||
hash := sha256.Sum256(s) | ||
return hash[:] | ||
} | ||
|
||
// AuthMiddleware returns a Basic Authentication middleware for a particular user and password. | ||
func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware { | ||
requiredUserBytes := toHashSlice([]byte(requiredUser)) | ||
requiredPasswordBytes := toHashSlice([]byte(requiredPassword)) | ||
|
||
return func(next endpoint.Endpoint) endpoint.Endpoint { | ||
return func(ctx context.Context, request interface{}) (interface{}, error) { | ||
auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please check for the presence of the value in the context before type-asserting it to a string. (The type-assert should also be checked.) If there's a failure in either of those steps, the middleware should return an appropriate error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure it's necessary to check the presence of the value as type assertion will do it as well. From the authentication process view, a missing header is as invalid as empty. |
||
givenUser, givenPassword, ok := parseBasicAuth(auth) | ||
if !ok { | ||
return nil, AuthError{realm} | ||
} | ||
|
||
// Equalize lengths of supplied and required credentials by hashing them. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
givenUserBytes := toHashSlice(givenUser) | ||
givenPasswordBytes := toHashSlice(givenPassword) | ||
|
||
if subtle.ConstantTimeCompare(givenUserBytes, requiredUserBytes) == 0 || | ||
subtle.ConstantTimeCompare(givenPasswordBytes, requiredPasswordBytes) == 0 { | ||
return nil, AuthError{realm} | ||
} | ||
|
||
return next(ctx, request) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package basic | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"fmt" | ||
"testing" | ||
|
||
httptransport "github.com/go-kit/kit/transport/http" | ||
) | ||
|
||
func TestWithBasicAuth(t *testing.T) { | ||
requiredUser := "test-user" | ||
requiredPassword := "test-pass" | ||
realm := "test realm" | ||
|
||
type want struct { | ||
result interface{} | ||
err error | ||
} | ||
tests := []struct { | ||
name string | ||
authHeader string | ||
want want | ||
}{ | ||
{"Isn't valid without authHeader", "", want{nil, AuthError{realm}}}, | ||
{"Isn't valid for wrong user", makeAuthString("wrong-user", requiredPassword), want{nil, AuthError{realm}}}, | ||
{"Isn't valid for wrong password", makeAuthString(requiredUser, "wrong-password"), want{nil, AuthError{realm}}}, | ||
{"Is valid for correct creds", makeAuthString(requiredUser, requiredPassword), want{true, nil}}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
ctx := context.WithValue(context.TODO(), httptransport.ContextKeyRequestAuthorization, tt.authHeader) | ||
|
||
result, err := AuthMiddleware(requiredUser, requiredPassword, realm)(passedValidation)(ctx, nil) | ||
if result != tt.want.result || err != tt.want.err { | ||
t.Errorf("WithBasicAuth() = result: %v, err: %v, want result: %v, want error: %v", result, err, tt.want.result, tt.want.err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func makeAuthString(user string, password string) string { | ||
data := []byte(fmt.Sprintf("%s:%s", user, password)) | ||
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(data)) | ||
} | ||
|
||
func passedValidation(ctx context.Context, request interface{}) (response interface{}, err error) { | ||
return true, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make this sentence make grammatical sense :) For example,