Skip to content
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

Merged
merged 5 commits into from
Sep 17, 2017
Merged

Conversation

DimaSalakhov
Copy link
Contributor

Issue #604

}

func credsAreValid(givenUser, givenPass, requiredUser, requiredPass string) bool {
// Equalize lengths of supplied and required credentials
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it important to make the lengths equal? Are you trying to preclude subtle.ConstantTimeCompare from divulging the difference in length by way of it short-circuiting the rest of the comparison?

Though highly unlikely, it looks like you're risking accepting a potential hash collision in trade for defending against a timing attack.

// by hashing them
givenUserBytes := sha256.Sum256([]byte(givenUser))
givenPassBytes := sha256.Sum256([]byte(givenPass))
requiredUserBytes := sha256.Sum256([]byte(requiredUser))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the required user name and password values are fixed at line 85 below, if you decide to retain this hashing technique, consider computing the hash on these two values once just before line 77, and reusing the hash instead of the raw values here.

return http.StatusUnauthorized
}

// Error is an implemntation of Error interface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/implemntation of Error/implementation of the Error/
  • Punctuate this sentence.

httptransport "github.com/go-kit/kit/transport/http"
)

// AuthError represents generic Authorization error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/represents generic Authorization/represents an authoriation/
  • Punctuate this sentence.

Realm string
}

// StatusCode is an implemntation of StatusCoder interface in go-kit/http
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/implemntation of StatusCoder/implementation of the StatusCoder/
  • Punctuate this sentence.

return http.StatusText(http.StatusUnauthorized)
}

// Headers is an implemntation of Headerer interface in go-kit/http
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/an implemntation of Headerer/an implementation of the Headerer/
  • Punctuate this sentence.

"WWW-Authenticate": []string{fmt.Sprintf(`Basic realm=%q`, e.Realm)}}
}

func credsAreValid(givenUser, givenPass, requiredUser, requiredPass string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Given that the currency of your implementation is byte vectors, consider demanding byte vectors as parameters.
    You could then return byte vectors from your parseBasicAuth function below, rather than converting the byte vectors resulting from decoding the base64-encoded values to strings at line 67, only to convert them back—with allocation and copying cost—here.

return cs[:s], cs[s+1:], true
}

// AuthMiddleware returns a Basic Authentication middleware for a particular user and password
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Punctuate this sentence.

@@ -0,0 +1,16 @@
`package auth/basic` provides a basic auth middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/basic auth/Basic Authorization/
  • Punctuate this sentence.

)
```

For AuthMiddleware to be able to pick up Authentication header from a http request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/up Authentication/up the "Authentication"/
  • s/a http request/an HTTP request,/
  • Punctuate this sentence, or s/like/like the following:/.

@DimaSalakhov
Copy link
Contributor Author

@seh Thanks for your review, everything was on point, I very much appreciate it!

In regards to the hash sum calculation sha256.Sum256() according to my very limited knowledge on the topic of timing attack, there are 2 suggestions to prevent them:

  • use constant time algorithm to prevent bruteforcing the exact position of the first difference in byte arrays. That's exactly what subtle/ConstantTimeCompare does
  • use byte arrays of the same length for the similar reason. In our case subtle/ConstantTimeCompare short circuit if arrays have a different length.
    I might be wrong in that, but this was a premise for me to compare hashes rather than slices. You're right about a potential collision in hashes, but my assumption is that the chances that person would successfully use that for bruteforce are higher than a chance of hash collision. However, I don't have a data to prove it and would be happy to hear a security advice.

@seh
Copy link
Contributor

seh commented Aug 28, 2017

I agree with the first benefit of using subtle.ConstantTimeCompare. Note, though, that computing the hash requires various length-dependent operations, so while we eliminate length difference detection threat from the final byte comparison, we still face timing differences in computing the hashes.

However, if you compute the hashes for the expected user name and password values ahead of time, as I had suggested earlier, then an attacker wouldn't be able to detect any relationship between computing the hashes of the expected and supplied values. You would then face only the threat of a hash collision yielding a false positive.

If the two hashes differ, we know that the supplied values do not match the expected values. If we wanted to be completely sure that a match in the hashes means the underlying values match, we'd then have to compare those underlying values directly—which brings us back to divulging differences in length via timing attack (even with subtle.ConstantTimeCompare), and wastes the utility of using the hash function.

I too would like to hear from others whether we should worry about the likelihood of a hash collision here.

@@ -13,49 +14,32 @@ import (
httptransport "github.com/go-kit/kit/transport/http"
)

// AuthError represents generic Authorization error
// AuthError represents an authoriation error.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/authoriation/authorization/

type AuthError struct {
Realm string
}

// StatusCode is an implemntation of StatusCoder interface in go-kit/http
// StatusCode is an iimplementation of the StatusCoder interface in go-kit/http.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • s/iimplementation/implementation/

func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware {
requiredUserBytes := sha256.Sum256([]byte(requiredUser))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Convert this array to a slice here, rather than down at line 77 each time you use it.

func AuthMiddleware(requiredUser, requiredPassword, realm string) endpoint.Middleware {
requiredUserBytes := sha256.Sum256([]byte(requiredUser))
requiredPassBytes := sha256.Sum256([]byte(requiredPassword))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Convert this array to a slice here, rather than down at line 78 each time you use it.
  • Consider using four extra characters to name the variable "requiredPasswordBytes."

For AuthMiddleware to be able to pick up the Authentication header from a HTTP request we need to pass it through the context with something like ```httptransport.ServerBefore(httptransport.PopulateRequestContext)```.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • We could go either way, but in American English, it sounds better to my ear to write "an HTTP."

@DimaSalakhov
Copy link
Contributor Author

@seh I agree with your statement about creating and comparing hashes.
As for the hash collision, I'm leaning more and more towards calling it possible, but unrealistic with the state of the modern hardware. Here is an article from Google about collision they've found in sha128: https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html
From what I see, it takes 1year, 110GPU and right algorithm to achieve it. SHA256 is twice the size and it would be a very long and expensive attack.

Copy link
Contributor

@seh seh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These recent changes alleviate my stated concerns.


return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string)
givenUser, givenPass, ok := parseBasicAuth(auth)
givenUser, givenPassword, ok := parseBasicAuth(auth)
if !ok {
return nil, AuthError{realm}
}

// Equalize lengths of supplied and required credentials by hashing them.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Since this comment points out what is happening but not why, consider removing this comment.

@@ -0,0 +1,16 @@
`package auth/basic` provides a Basic Authentication middleware [Mozilla article](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
Copy link
Member

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,

This package provides a Basic Authentication middleware.
Here is a [Mozilla article](...) with details.

import httptransport "github.com/go-kit/kit/transport/http"

httptransport.NewServer(
endpoint.Chain(AuthMiddleware(cfg.auth.user, cfg.auth.password, "Example Realm"))(makeUppercaseEndpoint()),
Copy link
Member

Choose a reason for hiding this comment

The 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?

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)}}
Copy link
Member

Choose a reason for hiding this comment

The 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.


return func(next endpoint.Endpoint) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
auth := ctx.Value(httptransport.ContextKeyRequestAuthorization).(string)
Copy link
Member

@peterbourgon peterbourgon Aug 31, 2017

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

@peterbourgon
Copy link
Member

With those changes, this looks good to me. Thanks for the initiative and sorry for my delays!

@peterbourgon
Copy link
Member

Yep! That works 👍

@peterbourgon peterbourgon merged commit 7862b3f into go-kit:master Sep 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants