Skip to content

Commit c652f73

Browse files
committed
Add user auth
1 parent 76a1e75 commit c652f73

File tree

11 files changed

+167
-85
lines changed

11 files changed

+167
-85
lines changed

coderd/authentication.go

Lines changed: 0 additions & 1 deletion
This file was deleted.

coderd/coderd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ func New(options *Options) http.Handler {
2626

2727
r := chi.NewRouter()
2828
r.Route("/api/v2", func(r chi.Router) {
29-
// The base route!
3029
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
3130
httpapi.Write(w, http.StatusOK, httpapi.Response{
3231
Message: "👋",
3332
})
3433
})
3534
r.Post("/user", users.createInitialUser)
35+
r.Post("/login", users.loginWithPassword)
3636
// Require an API key and authenticated user for this group.
3737
r.Group(func(r chi.Router) {
3838
r.Use(

coderd/coderdtest/coderdtest.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@ import (
1111
"cdr.dev/slog/sloggers/slogtest"
1212
"github.com/coder/coder/coderd"
1313
"github.com/coder/coder/codersdk"
14-
"github.com/coder/coder/database"
1514
"github.com/coder/coder/database/databasefake"
1615
)
1716

17+
// Server represents a test instance of coderd.
18+
// The database is intentionally omitted from
19+
// this struct to promote data being exposed via
20+
// the API.
1821
type Server struct {
19-
Client *codersdk.Client
20-
Database database.Store
21-
URL *url.URL
22+
Client *codersdk.Client
23+
URL *url.URL
2224
}
2325

26+
// New constructs a new coderd test instance.
2427
func New(t *testing.T) Server {
2528
// This can be hotswapped for a live database instance.
2629
db := databasefake.New()
@@ -41,9 +44,14 @@ func New(t *testing.T) Server {
4144
})
4245
require.NoError(t, err)
4346

47+
err = client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
48+
Email: "testuser@coder.com",
49+
Password: "testpassword",
50+
})
51+
require.NoError(t, err)
52+
4453
return Server{
45-
Client: client,
46-
Database: db,
47-
URL: u,
54+
Client: client,
55+
URL: u,
4856
}
4957
}

coderd/userpassword/userpassword_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package userpassword_test
33
import (
44
"testing"
55

6-
"github.com/coder/coder/coderd/userpassword"
76
"github.com/stretchr/testify/require"
7+
8+
"github.com/coder/coder/coderd/userpassword"
89
)
910

1011
func TestUserPassword(t *testing.T) {

coderd/users.go

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderd
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"database/sql"
67
"errors"
78
"fmt"
@@ -12,6 +13,7 @@ import (
1213
"github.com/google/uuid"
1314

1415
"github.com/coder/coder/coderd/userpassword"
16+
"github.com/coder/coder/cryptorand"
1517
"github.com/coder/coder/database"
1618
"github.com/coder/coder/httpapi"
1719
"github.com/coder/coder/httpmw"
@@ -57,7 +59,7 @@ func (users *users) createInitialUser(rw http.ResponseWriter, r *http.Request) {
5759
return
5860
}
5961
if userCount != 0 {
60-
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
62+
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
6163
Message: "the initial user has already been created",
6264
})
6365
return
@@ -116,7 +118,7 @@ func (users *users) getAuthenticatedUser(rw http.ResponseWriter, r *http.Request
116118

117119
func (users *users) loginWithPassword(rw http.ResponseWriter, r *http.Request) {
118120
var loginWithPassword LoginWithPasswordRequest
119-
if !httpapi.Read(rw, r, loginWithPassword) {
121+
if !httpapi.Read(rw, r, &loginWithPassword) {
120122
return
121123
}
122124
user, err := users.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
@@ -149,25 +151,50 @@ func (users *users) loginWithPassword(rw http.ResponseWriter, r *http.Request) {
149151
return
150152
}
151153

152-
// key, secret := (&database.APIKey{
153-
// UserID: actor.ID,
154-
// ExpiresAt: expiresAt,
155-
// LoginType: actor.LoginType,
156-
// OIDCAccessToken: oidcCfg.AccessToken,
157-
// OIDCRefreshToken: oidcCfg.RefreshToken,
158-
// OIDCIDToken: oidcCfg.IDToken,
159-
// // OIDCExpiry indicates when we need to fetch a new OIDC token.
160-
// OIDCExpiry: oidcCfg.Expiry,
161-
// DevurlToken: devurlToken,
162-
// }).Fill()
163-
164-
users.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
165-
ID: uuid.NewString(),
154+
id, secret, err := generateAPIKeyIDSecret()
155+
hashed := sha256.Sum256([]byte(secret))
156+
157+
_, err = users.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
158+
ID: id,
166159
UserID: user.ID,
167160
ExpiresAt: database.Now().Add(24 * time.Hour),
168161
CreatedAt: database.Now(),
169162
UpdatedAt: database.Now(),
170-
HashedSecret: []byte(""),
163+
HashedSecret: hashed[:],
171164
LoginType: database.LoginTypeBuiltIn,
172165
})
166+
if err != nil {
167+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
168+
Message: fmt.Sprintf("insert api key: %s", err.Error()),
169+
})
170+
return
171+
}
172+
173+
sessionToken := fmt.Sprintf("%s-%s", id, secret)
174+
http.SetCookie(rw, &http.Cookie{
175+
Name: httpmw.AuthCookie,
176+
Value: sessionToken,
177+
Path: "/",
178+
HttpOnly: true,
179+
SameSite: http.SameSiteLaxMode,
180+
})
181+
182+
render.Status(r, http.StatusCreated)
183+
render.JSON(rw, r, LoginWithPasswordResponse{
184+
SessionToken: sessionToken,
185+
})
186+
}
187+
188+
func generateAPIKeyIDSecret() (string, string, error) {
189+
// Length of an API Key ID.
190+
id, err := cryptorand.String(10)
191+
if err != nil {
192+
return "", "", err
193+
}
194+
// Length of an API Key secret.
195+
secret, err := cryptorand.String(22)
196+
if err != nil {
197+
return "", "", err
198+
}
199+
return id, secret, nil
173200
}

coderd/users_test.go

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,55 @@
11
package coderd_test
22

33
import (
4-
"bytes"
5-
"crypto/rand"
6-
"crypto/sha256"
7-
"encoding/base64"
8-
"fmt"
4+
"context"
95
"testing"
106

11-
"golang.org/x/crypto/pbkdf2"
7+
"github.com/coder/coder/coderd"
8+
"github.com/coder/coder/coderd/coderdtest"
9+
"github.com/stretchr/testify/require"
1210
)
1311

1412
func TestUsers(t *testing.T) {
15-
// t.Run("AuthenticatedUser", func(t *testing.T) {
16-
// _ = coderdtest.New(t)
17-
// })
18-
19-
t.Run("Pass", func(t *testing.T) {
20-
salt := make([]byte, 16)
21-
_, err := rand.Read(salt)
22-
if err != nil {
23-
panic("unexpected: crypto/rand.Read returned an error: " + err.Error())
24-
}
25-
hash := pbkdf2.Key([]byte("hello"), salt, 65535, 64, sha256.New)
26-
str := base64.StdEncoding.EncodeToString(hash)
27-
28-
parts := bytes.Split(hash, []byte("$"))
29-
fmt.Printf("Parts: %+v\n", len(parts))
30-
31-
fmt.Printf("Hash: %s %s\n", hash, str)
13+
t.Parallel()
14+
15+
t.Run("Authenticated", func(t *testing.T) {
16+
t.Parallel()
17+
server := coderdtest.New(t)
18+
_, err := server.Client.User(context.Background(), "")
19+
require.NoError(t, err)
20+
})
21+
22+
t.Run("CreateMultipleInitial", func(t *testing.T) {
23+
t.Parallel()
24+
server := coderdtest.New(t)
25+
_, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateUserRequest{
26+
Email: "dummy@coder.com",
27+
Username: "fake",
28+
Password: "password",
29+
})
30+
require.Error(t, err)
31+
})
32+
33+
t.Run("LoginNoEmail", func(t *testing.T) {
34+
t.Parallel()
35+
server := coderdtest.New(t)
36+
err := server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
37+
Email: "hello@io.io",
38+
Password: "wowie",
39+
})
40+
require.Error(t, err)
41+
})
42+
43+
t.Run("LoginBadPassword", func(t *testing.T) {
44+
t.Parallel()
45+
server := coderdtest.New(t)
46+
user, err := server.Client.User(context.Background(), "")
47+
require.NoError(t, err)
48+
49+
err = server.Client.LoginWithPassword(context.Background(), coderd.LoginWithPasswordRequest{
50+
Email: user.Email,
51+
Password: "bananas",
52+
})
53+
require.Error(t, err)
3254
})
3355
}

codersdk/authentication.go

Lines changed: 0 additions & 5 deletions
This file was deleted.

codersdk/client.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,20 @@ type Client struct {
4444
httpClient *http.Client
4545
}
4646

47-
func (c *Client) SessionToken() string {
48-
return c.sessionToken
49-
}
50-
51-
func (c *Client) setSessionToken(token string) {
47+
func (c *Client) setSessionToken(token string) error {
5248
if c.httpClient.Jar == nil {
53-
c.httpClient.Jar = &cookiejar.Jar{}
49+
var err error
50+
c.httpClient.Jar, err = cookiejar.New(nil)
51+
if err != nil {
52+
return err
53+
}
5454
}
5555
c.httpClient.Jar.SetCookies(c.url, []*http.Cookie{{
5656
Name: httpmw.AuthCookie,
5757
Value: token,
5858
}})
5959
c.sessionToken = token
60+
return nil
6061
}
6162

6263
func (c *Client) request(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
@@ -102,9 +103,6 @@ func readBodyAsError(res *http.Response) error {
102103
}
103104
return xerrors.Errorf("decode body: %w", err)
104105
}
105-
for _, er := range m.Errors {
106-
fmt.Printf("WE GOT THIS: %q %q\n", er.Field, er.Code)
107-
}
108106
return &Error{
109107
Response: m,
110108
statusCode: res.StatusCode,
@@ -122,5 +120,5 @@ func (e *Error) StatusCode() int {
122120
}
123121

124122
func (e *Error) Error() string {
125-
return fmt.Sprintf("")
123+
return fmt.Sprintf("status code %d: %s", e.statusCode, e.Message)
126124
}

codersdk/users.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,54 @@ package codersdk
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"net/http"
78

89
"github.com/coder/coder/coderd"
910
)
1011

11-
func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {
12-
res, err := c.request(ctx, http.MethodGet, "/api/v2/user", nil)
12+
func (c *Client) CreateInitialUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.User, error) {
13+
res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req)
1314
if err != nil {
1415
return coderd.User{}, err
1516
}
1617
defer res.Body.Close()
17-
if res.StatusCode > http.StatusOK {
18+
if res.StatusCode != http.StatusCreated {
1819
return coderd.User{}, readBodyAsError(res)
1920
}
2021
var user coderd.User
2122
return user, json.NewDecoder(res.Body).Decode(&user)
2223
}
2324

24-
func (c *Client) CreateInitialUser(ctx context.Context, req coderd.CreateUserRequest) (coderd.User, error) {
25-
res, err := c.request(ctx, http.MethodPost, "/api/v2/user", req)
25+
// User returns a user for the ID provided.
26+
// If the ID string is empty, the current user will be returned.
27+
func (c *Client) User(ctx context.Context, id string) (coderd.User, error) {
28+
res, err := c.request(ctx, http.MethodGet, "/api/v2/user", nil)
2629
if err != nil {
2730
return coderd.User{}, err
2831
}
2932
defer res.Body.Close()
30-
if res.StatusCode != http.StatusCreated {
33+
if res.StatusCode > http.StatusOK {
3134
return coderd.User{}, readBodyAsError(res)
3235
}
3336
var user coderd.User
3437
return user, json.NewDecoder(res.Body).Decode(&user)
3538
}
39+
40+
func (c *Client) LoginWithPassword(ctx context.Context, req coderd.LoginWithPasswordRequest) error {
41+
res, err := c.request(ctx, http.MethodPost, "/api/v2/login", req)
42+
if err != nil {
43+
return err
44+
}
45+
defer res.Body.Close()
46+
if res.StatusCode != http.StatusCreated {
47+
fmt.Printf("Are we reading here?\n")
48+
return readBodyAsError(res)
49+
}
50+
var resp coderd.LoginWithPasswordResponse
51+
err = json.NewDecoder(res.Body).Decode(&resp)
52+
if err != nil {
53+
return err
54+
}
55+
return c.setSessionToken(resp.SessionToken)
56+
}

codersdk/users_test.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,33 @@
11
package codersdk_test
22

33
import (
4+
"context"
5+
"net/http"
46
"testing"
57

8+
"github.com/coder/coder/coderd"
69
"github.com/coder/coder/coderd/coderdtest"
10+
"github.com/coder/coder/codersdk"
11+
"github.com/stretchr/testify/require"
712
)
813

914
func TestUsers(t *testing.T) {
10-
t.Run("Personal", func(t *testing.T) {
11-
_ = coderdtest.New(t)
15+
t.Run("MultipleInitial", func(t *testing.T) {
16+
server := coderdtest.New(t)
17+
_, err := server.Client.CreateInitialUser(context.Background(), coderd.CreateUserRequest{
18+
Email: "wowie@coder.com",
19+
Username: "tester",
20+
Password: "moo",
21+
})
22+
var cerr *codersdk.Error
23+
require.ErrorAs(t, err, &cerr)
24+
require.Equal(t, cerr.StatusCode(), http.StatusConflict)
25+
require.Greater(t, 0, cerr.Error())
26+
})
1227

28+
t.Run("Get", func(t *testing.T) {
29+
server := coderdtest.New(t)
30+
_, err := server.Client.User(context.Background(), "")
31+
require.NoError(t, err)
1332
})
1433
}

0 commit comments

Comments
 (0)