mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -0,0 +1,167 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
// AuthCookie represents the name of the cookie the API key is stored in.
|
||||
const AuthCookie = "session_token"
|
||||
|
||||
// OAuth2Config contains a subset of functions exposed from oauth2.Config.
|
||||
// It is abstracted for simple testing.
|
||||
type OAuth2Config interface {
|
||||
TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource
|
||||
}
|
||||
|
||||
type apiKeyContextKey struct{}
|
||||
|
||||
// APIKey returns the API key from the ExtractAPIKey handler.
|
||||
func APIKey(r *http.Request) database.APIKey {
|
||||
apiKey, ok := r.Context().Value(apiKeyContextKey{}).(database.APIKey)
|
||||
if !ok {
|
||||
panic("developer error: apikey middleware not provided")
|
||||
}
|
||||
return apiKey
|
||||
}
|
||||
|
||||
// ExtractAPIKey requires authentication using a valid API key.
|
||||
// It handles extending an API key if it comes close to expiry,
|
||||
// updating the last used time in the database.
|
||||
func ExtractAPIKey(db database.Store, oauthConfig OAuth2Config) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(AuthCookie)
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("%q cookie must be provided", AuthCookie),
|
||||
})
|
||||
return
|
||||
}
|
||||
parts := strings.Split(cookie.Value, "-")
|
||||
// APIKeys are formatted: ID-SECRET
|
||||
if len(parts) != 2 {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("invalid %q cookie api key format", AuthCookie),
|
||||
})
|
||||
return
|
||||
}
|
||||
id := parts[0]
|
||||
secret := parts[1]
|
||||
// Ensuring key lengths are valid.
|
||||
if len(id) != 10 {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("invalid %q cookie api key id", AuthCookie),
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(secret) != 22 {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("invalid %q cookie api key secret", AuthCookie),
|
||||
})
|
||||
return
|
||||
}
|
||||
key, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "api key is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("get api key by id: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
hashed := sha256.Sum256([]byte(secret))
|
||||
|
||||
// Checking to see if the secret is valid.
|
||||
if subtle.ConstantTimeCompare(key.HashedSecret, hashed[:]) != 1 {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: "api key secret is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
now := database.Now()
|
||||
// Tracks if the API key has properties updated!
|
||||
changed := false
|
||||
|
||||
if key.LoginType == database.LoginTypeOIDC {
|
||||
// Check if the OIDC token is expired!
|
||||
if key.OIDCExpiry.Before(now) && !key.OIDCExpiry.IsZero() {
|
||||
// If it is, let's refresh it from the provided config!
|
||||
token, err := oauthConfig.TokenSource(r.Context(), &oauth2.Token{
|
||||
AccessToken: key.OIDCAccessToken,
|
||||
RefreshToken: key.OIDCRefreshToken,
|
||||
Expiry: key.OIDCExpiry,
|
||||
}).Token()
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("couldn't refresh expired oauth token: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
key.OIDCAccessToken = token.AccessToken
|
||||
key.OIDCRefreshToken = token.RefreshToken
|
||||
key.OIDCExpiry = token.Expiry
|
||||
key.ExpiresAt = token.Expiry
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Checking if the key is expired.
|
||||
if key.ExpiresAt.Before(now) {
|
||||
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
|
||||
Message: fmt.Sprintf("api key expired at %q", key.ExpiresAt.String()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Only update LastUsed once an hour to prevent database spam.
|
||||
if now.Sub(key.LastUsed) > time.Hour {
|
||||
key.LastUsed = now
|
||||
changed = true
|
||||
}
|
||||
// Only update the ExpiresAt once an hour to prevent database spam.
|
||||
// We extend the ExpiresAt to reduce reauthentication.
|
||||
apiKeyLifetime := 24 * time.Hour
|
||||
if key.ExpiresAt.Sub(now) <= apiKeyLifetime-time.Hour {
|
||||
key.ExpiresAt = now.Add(apiKeyLifetime)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
err := db.UpdateAPIKeyByID(r.Context(), database.UpdateAPIKeyByIDParams{
|
||||
ID: key.ID,
|
||||
ExpiresAt: key.ExpiresAt,
|
||||
LastUsed: key.LastUsed,
|
||||
OIDCAccessToken: key.OIDCAccessToken,
|
||||
OIDCRefreshToken: key.OIDCRefreshToken,
|
||||
OIDCExpiry: key.OIDCExpiry,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("api key couldn't update: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), apiKeyContextKey{}, key)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/httpapi"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
func randomAPIKeyParts() (string, string) {
|
||||
id, _ := cryptorand.String(10)
|
||||
secret, _ := cryptorand.String(22)
|
||||
return id, secret
|
||||
}
|
||||
|
||||
func TestAPIKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
successHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Only called if the API key passes through the handler.
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "it worked!",
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("NoCookie", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidFormat", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: "test-wow-hello",
|
||||
})
|
||||
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidIDLength", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: "test-wow",
|
||||
})
|
||||
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidSecretLength", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: "testtestid-wow",
|
||||
})
|
||||
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("InvalidSecret", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
// Use a different secret so they don't match!
|
||||
hashed := sha256.Sum256([]byte("differentsecret"))
|
||||
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Expired", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusUnauthorized, rw.Result().StatusCode)
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
LastUsed: database.Now(),
|
||||
ExpiresAt: database.Now().AddDate(0, 0, 1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Checks that it exists on the context!
|
||||
_ = httpmw.APIKey(r)
|
||||
httpapi.Write(rw, http.StatusOK, httpapi.Response{
|
||||
Message: "it worked!",
|
||||
})
|
||||
})).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
|
||||
|
||||
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
|
||||
require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
|
||||
})
|
||||
|
||||
t.Run("ValidUpdateLastUsed", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
LastUsed: database.Now().AddDate(0, 0, -1),
|
||||
ExpiresAt: database.Now().AddDate(0, 0, 1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
|
||||
|
||||
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
|
||||
require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
|
||||
})
|
||||
|
||||
t.Run("ValidUpdateExpiry", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
LastUsed: database.Now(),
|
||||
ExpiresAt: database.Now().Add(time.Minute),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
|
||||
|
||||
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
|
||||
require.NotEqual(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
|
||||
})
|
||||
|
||||
t.Run("OIDCNotExpired", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
LastUsed: database.Now(),
|
||||
ExpiresAt: database.Now().AddDate(0, 0, 1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
|
||||
|
||||
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
|
||||
require.Equal(t, sentAPIKey.ExpiresAt, gotAPIKey.ExpiresAt)
|
||||
})
|
||||
|
||||
t.Run("OIDCRefresh", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
LoginType: database.LoginTypeOIDC,
|
||||
LastUsed: database.Now(),
|
||||
OIDCExpiry: database.Now().AddDate(0, 0, -1),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "wow",
|
||||
RefreshToken: "moo",
|
||||
Expiry: database.Now().AddDate(0, 0, 1),
|
||||
}
|
||||
httpmw.ExtractAPIKey(db, &oauth2Config{
|
||||
tokenSource: &oauth2TokenSource{
|
||||
token: func() (*oauth2.Token, error) {
|
||||
return token, nil
|
||||
},
|
||||
},
|
||||
})(successHandler).ServeHTTP(rw, r)
|
||||
require.Equal(t, http.StatusOK, rw.Result().StatusCode)
|
||||
|
||||
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, sentAPIKey.LastUsed, gotAPIKey.LastUsed)
|
||||
require.Equal(t, token.Expiry, gotAPIKey.ExpiresAt)
|
||||
require.Equal(t, token.AccessToken, gotAPIKey.OIDCAccessToken)
|
||||
})
|
||||
}
|
||||
|
||||
type oauth2Config struct {
|
||||
tokenSource *oauth2TokenSource
|
||||
}
|
||||
|
||||
func (o *oauth2Config) TokenSource(ctx context.Context, token *oauth2.Token) oauth2.TokenSource {
|
||||
return o.tokenSource
|
||||
}
|
||||
|
||||
type oauth2TokenSource struct {
|
||||
token func() (*oauth2.Token, error)
|
||||
}
|
||||
|
||||
func (o *oauth2TokenSource) Token() (*oauth2.Token, error) {
|
||||
return o.token()
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/httpapi"
|
||||
)
|
||||
|
||||
type userContextKey struct{}
|
||||
|
||||
// User returns the user from the ExtractUser handler.
|
||||
func User(r *http.Request) database.User {
|
||||
user, ok := r.Context().Value(userContextKey{}).(database.User)
|
||||
if !ok {
|
||||
panic("developer error: user middleware not provided")
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// ExtractUser consumes an API key and queries the user attached to it.
|
||||
// It attaches the user to the request context.
|
||||
func ExtractUser(db database.Store) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// The user handler depends on API Key to get the authenticated user.
|
||||
apiKey := APIKey(r)
|
||||
|
||||
user, err := db.GetUserByID(r.Context(), apiKey.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: "user not found for api key",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
|
||||
Message: fmt.Sprintf("couldn't fetch user for api key: %s", err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), userContextKey{}, user)
|
||||
next.ServeHTTP(rw, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package httpmw_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/database"
|
||||
"github.com/coder/coder/database/databasefake"
|
||||
"github.com/coder/coder/httpmw"
|
||||
)
|
||||
|
||||
func TestUser(t *testing.T) {
|
||||
t.Run("NoUser", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
_, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
UserID: "bananas",
|
||||
HashedSecret: hashed[:],
|
||||
LastUsed: database.Now(),
|
||||
ExpiresAt: database.Now().Add(time.Minute),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
httpmw.ExtractAPIKey(db, nil)(http.HandlerFunc(func(rw http.ResponseWriter, returnedRequest *http.Request) {
|
||||
r = returnedRequest
|
||||
})).ServeHTTP(rw, r)
|
||||
|
||||
httpmw.ExtractUser(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})).ServeHTTP(rw, r)
|
||||
})
|
||||
|
||||
t.Run("User", func(t *testing.T) {
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
)
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.AuthCookie,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
user, err := db.InsertUser(r.Context(), database.InsertUserParams{
|
||||
ID: "testing",
|
||||
CreatedAt: database.Now(),
|
||||
UpdatedAt: database.Now(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
UserID: user.ID,
|
||||
HashedSecret: hashed[:],
|
||||
LastUsed: database.Now(),
|
||||
ExpiresAt: database.Now().Add(time.Minute),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
httpmw.ExtractAPIKey(db, nil)(http.HandlerFunc(func(rw http.ResponseWriter, returnedRequest *http.Request) {
|
||||
r = returnedRequest
|
||||
})).ServeHTTP(rw, r)
|
||||
|
||||
httpmw.ExtractUser(db)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Makes sure the context properly adds the User!
|
||||
_ = httpmw.User(r)
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
})).ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user