Skip to content

Commit 6394949

Browse files
committed
Add WIP
1 parent 129a10b commit 6394949

File tree

18 files changed

+660
-19
lines changed

18 files changed

+660
-19
lines changed

coderd/authentication.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package coderd

coderd/coderd.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ func New(options *Options) http.Handler {
2525

2626
r := chi.NewRouter()
2727
r.Route("/api/v2", func(r chi.Router) {
28+
// The base route!
2829
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
2930
httpapi.Write(w, http.StatusOK, httpapi.Response{
3031
Message: "👋",
3132
})
3233
})
34+
r.Post("/user", users.createInitialUser)
35+
// Require an API key and authenticated user for this group.
3336
r.Group(func(r chi.Router) {
34-
// Require an API key and authenticated user to call this route.
3537
r.Use(
3638
httpmw.ExtractAPIKey(options.Database, nil),
3739
httpmw.ExtractUser(options.Database),

coderd/coderdtest/coderdtest.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
package coderdtest
22

33
import (
4+
"context"
45
"net/http/httptest"
6+
"net/url"
57
"testing"
68

9+
"github.com/stretchr/testify/require"
10+
711
"cdr.dev/slog/sloggers/slogtest"
812
"github.com/coder/coder/coderd"
13+
"github.com/coder/coder/codersdk"
914
"github.com/coder/coder/database"
1015
"github.com/coder/coder/database/databasefake"
1116
)
1217

1318
type Server struct {
19+
Client *codersdk.Client
1420
Database database.Store
15-
URL string
21+
URL *url.URL
1622
}
1723

1824
func New(t *testing.T) Server {
@@ -23,9 +29,21 @@ func New(t *testing.T) Server {
2329
Database: db,
2430
})
2531
srv := httptest.NewServer(handler)
32+
u, err := url.Parse(srv.URL)
33+
require.NoError(t, err)
2634
t.Cleanup(srv.Close)
35+
36+
client := codersdk.New(u, &codersdk.Options{})
37+
_, err = client.CreateInitialUser(context.Background(), coderd.CreateUserRequest{
38+
Email: "testuser@coder.com",
39+
Username: "testuser",
40+
Password: "testpassword",
41+
})
42+
require.NoError(t, err)
43+
2744
return Server{
45+
Client: client,
2846
Database: db,
29-
URL: srv.URL,
47+
URL: u,
3048
}
3149
}

coderd/coderdtest/coderdtest_test.go

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

6-
"github.com/coder/coder/coderd/coderdtest"
76
"go.uber.org/goleak"
7+
8+
"github.com/coder/coder/coderd/coderdtest"
89
)
910

1011
func TestMain(m *testing.M) {

coderd/users.go

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,108 @@
11
package coderd
22

33
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
48
"net/http"
59
"time"
610

711
"github.com/go-chi/render"
12+
"github.com/google/uuid"
813

14+
"github.com/coder/coder/coderd/userpassword"
915
"github.com/coder/coder/database"
16+
"github.com/coder/coder/httpapi"
1017
"github.com/coder/coder/httpmw"
1118
)
1219

1320
type User struct {
14-
ID string `json:"id"`
15-
Email string `json:"email"`
16-
CreatedAt time.Time `json:"created_at"`
17-
Username string `json:"username"`
21+
ID string `json:"id" validate:"required"`
22+
Email string `json:"email" validate:"required"`
23+
CreatedAt time.Time `json:"created_at" validate:"required"`
24+
Username string `json:"username" validate:"required"`
25+
}
26+
27+
type CreateUserRequest struct {
28+
Email string `json:"email" validate:"required,email"`
29+
Username string `json:"username" validate:"required,username"`
30+
Password string `json:"password" validate:"required"`
31+
}
32+
33+
type LoginWithPasswordRequest struct {
34+
Email string `json:"email" validate:"required,email"`
35+
Password string `json:"password" validate:"required"`
36+
}
37+
38+
type LoginWithPasswordResponse struct {
39+
SessionToken string `json:"session_token" validate:"required"`
1840
}
1941

2042
type users struct {
2143
Database database.Store
2244
}
2345

46+
// Creates the initial user for a Coder deployment.
47+
func (users *users) createInitialUser(rw http.ResponseWriter, r *http.Request) {
48+
var createUser CreateUserRequest
49+
if !httpapi.Read(rw, r, &createUser) {
50+
return
51+
}
52+
userCount, err := users.Database.GetUserCount(r.Context())
53+
if err != nil {
54+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
55+
Message: fmt.Sprintf("get user count: %s", err.Error()),
56+
})
57+
return
58+
}
59+
if userCount != 0 {
60+
httpapi.Write(rw, http.StatusForbidden, httpapi.Response{
61+
Message: "the initial user has already been created",
62+
})
63+
return
64+
}
65+
user, err := users.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
66+
Email: createUser.Email,
67+
Username: createUser.Username,
68+
})
69+
if errors.Is(err, sql.ErrNoRows) {
70+
err = nil
71+
}
72+
if err != nil {
73+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
74+
Message: fmt.Sprintf("get user: %s", err.Error()),
75+
})
76+
return
77+
}
78+
hashedPassword, err := userpassword.Hash(createUser.Password)
79+
if err != nil {
80+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
81+
Message: fmt.Sprintf("hash password: %s", err.Error()),
82+
})
83+
return
84+
}
85+
86+
user, err = users.Database.InsertUser(context.Background(), database.InsertUserParams{
87+
ID: uuid.NewString(),
88+
Email: createUser.Email,
89+
HashedPassword: []byte(hashedPassword),
90+
Username: createUser.Username,
91+
LoginType: database.LoginTypeBuiltIn,
92+
CreatedAt: database.Now(),
93+
UpdatedAt: database.Now(),
94+
})
95+
if err != nil {
96+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
97+
Message: fmt.Sprintf("create user: %s", err.Error()),
98+
})
99+
return
100+
}
101+
render.Status(r, http.StatusCreated)
102+
render.JSON(rw, r, user)
103+
}
104+
105+
// Returns the currently authenticated user.
24106
func (users *users) getAuthenticatedUser(rw http.ResponseWriter, r *http.Request) {
25107
user := httpmw.User(r)
26108

@@ -31,3 +113,61 @@ func (users *users) getAuthenticatedUser(rw http.ResponseWriter, r *http.Request
31113
Username: user.Username,
32114
})
33115
}
116+
117+
func (users *users) loginWithPassword(rw http.ResponseWriter, r *http.Request) {
118+
var loginWithPassword LoginWithPasswordRequest
119+
if !httpapi.Read(rw, r, loginWithPassword) {
120+
return
121+
}
122+
user, err := users.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
123+
Email: loginWithPassword.Email,
124+
})
125+
if errors.Is(err, sql.ErrNoRows) {
126+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
127+
Message: "invalid email or password",
128+
})
129+
return
130+
}
131+
if err != nil {
132+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
133+
Message: fmt.Sprintf("get user: %s", err.Error()),
134+
})
135+
return
136+
}
137+
equal, err := userpassword.Compare(string(user.HashedPassword), loginWithPassword.Password)
138+
if err != nil {
139+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
140+
Message: fmt.Sprintf("compare: %s", err.Error()),
141+
})
142+
}
143+
if !equal {
144+
// This message is the same as above to remove ease in detecting whether
145+
// users are registered or not. Attackers still could with a timing attack.
146+
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
147+
Message: "invalid email or password",
148+
})
149+
return
150+
}
151+
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(),
166+
UserID: user.ID,
167+
ExpiresAt: database.Now().Add(24 * time.Hour),
168+
CreatedAt: database.Now(),
169+
UpdatedAt: database.Now(),
170+
HashedSecret: []byte(""),
171+
LoginType: database.LoginTypeBuiltIn,
172+
})
173+
}

coderd/users_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
11
package coderd_test
22

3-
import "testing"
3+
import (
4+
"bytes"
5+
"crypto/rand"
6+
"crypto/sha256"
7+
"encoding/base64"
8+
"fmt"
9+
"testing"
10+
11+
"golang.org/x/crypto/pbkdf2"
12+
)
413

514
func TestUsers(t *testing.T) {
6-
t.Run("wow", func(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))
730

31+
fmt.Printf("Hash: %s %s\n", hash, str)
832
})
933
}

codersdk/authentication.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package codersdk
2+
3+
func (c *Client) LoginWithPassword(email, password string) error {
4+
5+
}

0 commit comments

Comments
 (0)