separation of concerns

This commit is contained in:
Daniel 2024-07-29 00:21:07 -04:00
parent 51bd6d8433
commit 992542d9c6
3 changed files with 148 additions and 0 deletions

37
pkg/gordio/auth/apikey.go Normal file
View file

@ -0,0 +1,37 @@
package auth
import (
"context"
"time"
"dynatron.me/x/stillbox/pkg/gordio/database"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
func (a *Authenticator) CheckAPIKey(ctx context.Context, key string) (*database.ApiKey, error) {
keyUuid, err := uuid.Parse(key)
if err != nil {
log.Error().Str("apikey", key).Msg("cannot parse key")
return nil, ErrBadRequest
}
db := database.FromCtx(ctx)
apik, err := db.GetAPIKey(ctx, keyUuid)
if err != nil {
if database.IsNoRows(err) {
log.Error().Str("apikey", keyUuid.String()).Msg("no such key")
return nil, ErrUnauthorized
}
return nil, ErrInternal
}
if (apik.Disabled != nil && *apik.Disabled) || (apik.Expires.Valid && time.Now().After(apik.Expires.Time)) {
log.Error().Str("key", apik.ApiKey.String()).Msg("key disabled")
return nil, ErrUnauthorized
}
return &apik, nil
}

111
pkg/gordio/auth/jwt.go Normal file
View file

@ -0,0 +1,111 @@
package auth
import (
"context"
"golang.org/x/crypto/bcrypt"
"net/http"
"time"
"dynatron.me/x/stillbox/pkg/gordio/database"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/go-chi/render"
"github.com/rs/zerolog/log"
)
type claims map[string]interface{}
func (a *Authenticator) Authenticated(r *http.Request) (claims, bool) {
// TODO: check IP against ACL, or conf.Public, and against map of routes
tok, cl, err := jwtauth.FromContext(r.Context())
return cl, err != nil && tok != nil
}
func (a *Authenticator) InstallVerifyMiddleware(r chi.Router) {
r.Use(jwtauth.Verifier(a.jwt))
}
func (a *Authenticator) InstallAuthMiddleware(r chi.Router) {
r.Use(jwtauth.Authenticator(a.jwt))
}
func (a *Authenticator) Login(ctx context.Context, username, password string) (token string, err error) {
q := database.New(database.FromCtx(ctx))
users, err := q.GetUsers(ctx)
if err != nil {
log.Error().Err(err).Msg("getUsers failed")
return "", ErrLoginFailed
}
var found *database.User
for _, u := range users {
if u.Username == username {
found = &u
}
}
if found == nil {
_ = bcrypt.CompareHashAndPassword([]byte("lol@timing"), []byte(password))
return "", ErrLoginFailed
} else {
err = bcrypt.CompareHashAndPassword([]byte(found.Password), []byte(password))
if err != nil {
return "", ErrLoginFailed
}
}
return a.NewToken(found.ID), nil
}
func (a *Authenticator) NewToken(uid int32) string {
claims := claims{
"user_id": uid,
}
jwtauth.SetExpiryIn(claims, time.Hour*24*30) // one month
_, tokenString, err := a.jwt.Encode(claims)
if err != nil {
panic(err)
}
return tokenString
}
func (a *Authenticator) InstallRoutes(r chi.Router) {
r.Post("/auth", a.routeAuth)
}
func (a *Authenticator) routeAuth(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
username, password := r.PostFormValue("username"), r.PostFormValue("password")
if username == "" || password == "" {
http.Error(w, "blank credentials", http.StatusBadRequest)
return
}
tok, err := a.Login(r.Context(), username, password)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
http.SetCookie(w, &http.Cookie{
Name: "jwt",
Value: tok,
HttpOnly: true,
Secure: true,
Domain: a.domain,
})
jr := struct {
JWT string `json:"jwt"`
}{
JWT: tok,
}
render.JSON(w, r, &jr)
}