-
Notifications
You must be signed in to change notification settings - Fork 582
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
1 parent
36b7b20
commit 6a919ae
Showing
39 changed files
with
2,232 additions
and
61 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package coderdtest | ||
|
||
import ( | ||
"context" | ||
"net/http/httptest" | ||
"net/url" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"cdr.dev/slog/sloggers/slogtest" | ||
"github.com/coder/coder/coderd" | ||
"github.com/coder/coder/codersdk" | ||
"github.com/coder/coder/database/databasefake" | ||
) | ||
|
||
// Server represents a test instance of coderd. | ||
// The database is intentionally omitted from | ||
// this struct to promote data being exposed via | ||
// the API. | ||
type Server struct { | ||
Client *codersdk.Client | ||
URL *url.URL | ||
} | ||
|
||
// New constructs a new coderd test instance. | ||
func New(t *testing.T) Server { | ||
// This can be hotswapped for a live database instance. | ||
db := databasefake.New() | ||
handler := coderd.New(&coderd.Options{ | ||
Logger: slogtest.Make(t, nil), | ||
Database: db, | ||
}) | ||
srv := httptest.NewServer(handler) | ||
u, err := url.Parse(srv.URL) | ||
require.NoError(t, err) | ||
t.Cleanup(srv.Close) | ||
|
||
client := codersdk.New(u) | ||
_, err = client.CreateInitialUser(context.Background(), coderd.CreateUserRequest{ | ||
Email: "testuser@coder.com", | ||
Username: "testuser", | ||
Password: "testpassword", | ||
}) | ||
require.NoError(t, err) | ||
|
||
login, err := client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{ | ||
Email: "testuser@coder.com", | ||
Password: "testpassword", | ||
}) | ||
require.NoError(t, err) | ||
err = client.SetSessionToken(login.SessionToken) | ||
require.NoError(t, err) | ||
|
||
return Server{ | ||
Client: client, | ||
URL: u, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package coderdtest_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"go.uber.org/goleak" | ||
|
||
"github.com/coder/coder/coderd/coderdtest" | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
goleak.VerifyTestMain(m) | ||
} | ||
|
||
func TestNew(t *testing.T) { | ||
_ = coderdtest.New(t) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
} |
Oops, something went wrong.