feat: Add authentication and personal user endpoint (#29)

* feat: Add authentication and personal user endpoint

This contribution adds a lot of scaffolding for the database fake
and testability of coderd.

A new endpoint "/user" is added to return the currently authenticated
user to the requester.

* Use TestMain to catch leak instead

* Add userpassword package

* Add WIP

* Add user auth

* Fix test

* Add comments

* Fix login response

* Fix order

* Fix generated code

* Update httpapi/httpapi.go

Co-authored-by: Bryan <bryan@coder.com>

Co-authored-by: Bryan <bryan@coder.com>
This commit is contained in:
Kyle Carberry
2022-01-20 13:46:51 +00:00
committed by GitHub
co-authored by Bryan
parent 36b7b20e2a
commit 6a919aea79
39 changed files with 2232 additions and 61 deletions
+78
View File
@@ -0,0 +1,78 @@
package userpassword
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"fmt"
"strconv"
"strings"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/xerrors"
)
const (
// This is the length of our output hash.
// bcrypt has a hash size of 59, so we rounded up to a power of 8.
hashLength = 64
// The scheme to include in our hashed password.
hashScheme = "pbkdf2-sha256"
)
// Compare checks the equality of passwords from a hashed pbkdf2 string.
// This uses pbkdf2 to ensure FIPS 140-2 compliance. See:
// https://csrc.nist.gov/csrc/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp2261.pdf
func Compare(hashed string, password string) (bool, error) {
if len(hashed) < hashLength {
return false, xerrors.Errorf("hash too short: %d", len(hashed))
}
parts := strings.SplitN(hashed, "$", 5)
if len(parts) != 5 {
return false, xerrors.Errorf("hash has too many parts: %d", len(parts))
}
if len(parts[0]) != 0 {
return false, xerrors.Errorf("hash prefix is invalid")
}
if string(parts[1]) != hashScheme {
return false, xerrors.Errorf("hash isn't %q scheme: %q", hashScheme, parts[1])
}
iter, err := strconv.Atoi(string(parts[2]))
if err != nil {
return false, xerrors.Errorf("parse iter from hash: %w", err)
}
salt, err := base64.RawStdEncoding.DecodeString(string(parts[3]))
if err != nil {
return false, xerrors.Errorf("decode salt: %w", err)
}
if subtle.ConstantTimeCompare([]byte(hashWithSaltAndIter(password, salt, iter)), []byte(hashed)) != 1 {
return false, nil
}
return true, nil
}
// Hash generates a hash using pbkdf2.
// See the Compare() comment for rationale.
func Hash(password string) (string, error) {
// bcrypt uses a salt size of 16 bytes.
salt := make([]byte, 16)
_, err := rand.Read(salt)
if err != nil {
return "", xerrors.Errorf("read random bytes for salt: %w", err)
}
// The default hash iteration is 1024 for speed.
// As this is increased, the password is hashed more.
return hashWithSaltAndIter(password, salt, 1024), nil
}
// Produces a string representation of the hash.
func hashWithSaltAndIter(password string, salt []byte, iter int) string {
hash := pbkdf2.Key([]byte(password), salt, iter, hashLength, sha256.New)
hash = []byte(base64.RawStdEncoding.EncodeToString(hash))
salt = []byte(base64.RawStdEncoding.EncodeToString(salt))
// This format is similar to bcrypt. See:
// https://en.wikipedia.org/wiki/Bcrypt#Description
return fmt.Sprintf("$%s$%d$%s$%s", hashScheme, iter, salt, hash)
}
+47
View File
@@ -0,0 +1,47 @@
package userpassword_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/userpassword"
)
func TestUserPassword(t *testing.T) {
t.Run("Legacy", func(t *testing.T) {
// Ensures legacy v1 passwords function for v2.
// This has is manually generated using a print statement from v1 code.
equal, err := userpassword.Compare("$pbkdf2-sha256$65535$z8c1p1C2ru9EImBP1I+ZNA$pNjE3Yk0oG0PmJ0Je+y7ENOVlSkn/b0BEqqdKsq6Y97wQBq0xT+lD5bWJpyIKJqQICuPZcEaGDKrXJn8+SIHRg", "tomato")
require.NoError(t, err)
require.True(t, equal)
})
t.Run("Same", func(t *testing.T) {
hash, err := userpassword.Hash("password")
require.NoError(t, err)
equal, err := userpassword.Compare(hash, "password")
require.NoError(t, err)
require.True(t, equal)
})
t.Run("Different", func(t *testing.T) {
hash, err := userpassword.Hash("password")
require.NoError(t, err)
equal, err := userpassword.Compare(hash, "notpassword")
require.NoError(t, err)
require.False(t, equal)
})
t.Run("Invalid", func(t *testing.T) {
equal, err := userpassword.Compare("invalidhash", "password")
require.False(t, equal)
require.Error(t, err)
})
t.Run("InvalidParts", func(t *testing.T) {
equal, err := userpassword.Compare("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", "test")
require.False(t, equal)
require.Error(t, err)
})
}