use hash of password rather than plaintext password

This commit is contained in:
Dan Laine 2020-06-30 09:37:36 -04:00
parent e144b1087e
commit fe3e1a319e
3 changed files with 53 additions and 45 deletions

View File

@ -1,6 +1,7 @@
package auth
import (
"bytes"
"errors"
"fmt"
"io"
@ -10,6 +11,8 @@ import (
"sync"
"time"
"github.com/ava-labs/gecko/utils/hashing"
"github.com/ava-labs/gecko/utils/timer"
jwt "github.com/dgrijalva/jwt-go"
)
@ -34,11 +37,11 @@ var (
// Auth handles HTTP API authorization for this node
type Auth struct {
lock sync.RWMutex // Prevent race condition when accessing password
Enabled bool // True iff API calls need auth token
clock timer.Clock // Tells the time. Can be faked for testing
Password string // The password. Can be changed via API call.
revoked []string // List of tokens that have been revoked
lock sync.RWMutex // Prevent race condition when accessing password
Enabled bool // True iff API calls need auth token
clock timer.Clock // Tells the time. Can be faked for testing
HashedPassword []byte // Hash of the password. Can be changed via API call.
revoked []string // List of tokens that have been revoked
}
// Custom claim type used for API access token
@ -70,7 +73,7 @@ func getToken(r *http.Request) (string, error) {
func (auth *Auth) newToken(password string, endpoints []string) (string, error) {
auth.lock.RLock()
defer auth.lock.RUnlock()
if password != auth.Password {
if !bytes.Equal(hashing.ComputeHash256([]byte(password)), auth.HashedPassword) {
return "", errWrongPassword
}
canAccessAll := false
@ -91,7 +94,7 @@ func (auth *Auth) newToken(password string, endpoints []string) (string, error)
claims.Endpoints = endpoints
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(auth.Password)) // Sign the token and return its string repr.
return token.SignedString(auth.HashedPassword) // Sign the token and return its string repr.
}
@ -104,12 +107,12 @@ func (auth *Auth) newToken(password string, endpoints []string) (string, error)
func (auth *Auth) revokeToken(tokenStr string, password string) error {
auth.lock.Lock()
defer auth.lock.Unlock()
if auth.Password != password {
if !bytes.Equal(auth.HashedPassword, hashing.ComputeHash256([]byte(password))) {
return errWrongPassword
}
token, err := jwt.Parse(tokenStr, func(*jwt.Token) (interface{}, error) { // See if token is well-formed and signature is right
return []byte(auth.Password), nil
return auth.HashedPassword, nil
})
if err == nil && token.Valid { // Only need to revoke if the token is valid
auth.revoked = append(auth.revoked, tokenStr)
@ -124,14 +127,14 @@ func (auth *Auth) revokeToken(tokenStr string, password string) error {
func (auth *Auth) changePassword(oldPassword, newPassword string) error {
auth.lock.Lock()
defer auth.lock.Unlock()
if auth.Password != oldPassword {
if !bytes.Equal(auth.HashedPassword, hashing.ComputeHash256([]byte(oldPassword))) {
return errWrongPassword
} else if len(newPassword) == 0 || len(newPassword) > maxPasswordLen {
return fmt.Errorf("new password length exceeds maximum length, %d", maxPasswordLen)
} else if oldPassword == newPassword {
return errors.New("new password can't be same as old password")
}
auth.Password = newPassword
auth.HashedPassword = hashing.ComputeHash256([]byte(newPassword))
auth.revoked = []string{} // All the revoked tokens are now invalid; no need to mark specifically as revoked
return nil
}
@ -162,7 +165,7 @@ func (auth *Auth) WrapHandler(h http.Handler) http.Handler {
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, func(*jwt.Token) (interface{}, error) { // See if token is well-formed and signature is right
auth.lock.RLock()
defer auth.lock.RUnlock()
return []byte(auth.Password), nil
return auth.HashedPassword, nil
})
if err != nil { // Probably because signature wrong
w.WriteHeader(http.StatusUnauthorized)

View File

@ -1,6 +1,7 @@
package auth
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
@ -9,13 +10,16 @@ import (
"testing"
"time"
"github.com/ava-labs/gecko/utils/hashing"
"github.com/ava-labs/gecko/utils/timer"
jwt "github.com/dgrijalva/jwt-go"
)
const (
password = "password"
var (
password = "password"
hashedPassword = hashing.ComputeHash256([]byte(password))
)
var (
@ -25,8 +29,8 @@ var (
func TestNewTokenWrongPassword(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
if _, err := auth.newToken("", []string{"endpoint1, endpoint2"}); err == nil {
t.Fatal("should have failed because password is wrong")
@ -37,8 +41,8 @@ func TestNewTokenWrongPassword(t *testing.T) {
func TestNewTokenHappyPath(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
now := time.Now()
auth.clock.Set(now)
@ -54,7 +58,7 @@ func TestNewTokenHappyPath(t *testing.T) {
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, func(*jwt.Token) (interface{}, error) {
auth.lock.RLock()
defer auth.lock.RUnlock()
return []byte(auth.Password), nil
return auth.HashedPassword, nil
})
if err != nil {
t.Fatalf("couldn't parse new token: %s", err)
@ -74,8 +78,8 @@ func TestNewTokenHappyPath(t *testing.T) {
func TestTokenHasWrongSig(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token
@ -106,8 +110,8 @@ func TestTokenHasWrongSig(t *testing.T) {
func TestChangePassword(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
password2 := "password2"
@ -121,7 +125,7 @@ func TestChangePassword(t *testing.T) {
t.Fatal("should have succeeded")
}
if auth.Password != password2 {
if !bytes.Equal(auth.HashedPassword, hashing.ComputeHash256([]byte(password2))) {
t.Fatal("password should have been changed")
}
@ -161,8 +165,8 @@ func TestGetToken(t *testing.T) {
func TestRevokeToken(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token
@ -181,8 +185,8 @@ func TestRevokeToken(t *testing.T) {
func TestWrapHandlerHappyPath(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token
@ -207,8 +211,8 @@ func TestWrapHandlerHappyPath(t *testing.T) {
func TestWrapHandlerRevokedToken(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token
@ -236,9 +240,9 @@ func TestWrapHandlerRevokedToken(t *testing.T) {
func TestWrapHandlerExpiredToken(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
clock: timer.Clock{},
Enabled: true,
HashedPassword: hashedPassword,
clock: timer.Clock{},
}
auth.clock.Set(time.Now().Add(-2 * TokenLifespan))
@ -264,8 +268,8 @@ func TestWrapHandlerExpiredToken(t *testing.T) {
func TestWrapHandlerNoAuthToken(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics"}
@ -282,8 +286,8 @@ func TestWrapHandlerNoAuthToken(t *testing.T) {
func TestWrapHandlerUnauthorizedEndpoint(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token
@ -309,8 +313,8 @@ func TestWrapHandlerUnauthorizedEndpoint(t *testing.T) {
func TestWrapHandlerAuthEndpoint(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token
@ -332,8 +336,8 @@ func TestWrapHandlerAuthEndpoint(t *testing.T) {
func TestWrapHandlerAccessAll(t *testing.T) {
auth := Auth{
Enabled: true,
Password: password,
Enabled: true,
HashedPassword: hashedPassword,
}
// Make a token that allows access to all endpoints
@ -357,8 +361,8 @@ func TestWrapHandlerAccessAll(t *testing.T) {
func TestWrapHandlerAuthDisabled(t *testing.T) {
auth := Auth{
Enabled: false,
Password: password,
Enabled: false,
HashedPassword: hashedPassword,
}
endpoints := []string{"/ext/info", "/ext/bc/X", "/ext/metrics", "", "/foo", "/ext/foo/info", "/ext/auth"}

View File

@ -19,6 +19,7 @@ import (
"github.com/ava-labs/gecko/api/auth"
"github.com/ava-labs/gecko/snow"
"github.com/ava-labs/gecko/snow/engine/common"
"github.com/ava-labs/gecko/utils/hashing"
"github.com/ava-labs/gecko/utils/logging"
)
@ -52,8 +53,8 @@ func (s *Server) Initialize(log logging.Logger, factory logging.Factory, host st
s.listenAddress = fmt.Sprintf("%s:%d", host, port)
s.router = newRouter()
s.auth = &auth.Auth{
Enabled: authEnabled,
Password: authPassword,
Enabled: authEnabled,
HashedPassword: hashing.ComputeHash256([]byte(authPassword)),
}
if authEnabled { // only create auth service if token authorization is required
s.log.Info("API authorization is enabled. Auth token must be passed in header of API requests (except requests to auth service.)")