2024-07-29 00:21:07 -04:00
|
|
|
package auth
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"net/http"
|
2024-08-10 18:21:13 -04:00
|
|
|
"strconv"
|
2024-08-15 13:28:03 -04:00
|
|
|
"strings"
|
2024-07-29 00:21:07 -04:00
|
|
|
"time"
|
|
|
|
|
2024-07-29 00:47:58 -04:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
2024-07-29 00:21:07 -04:00
|
|
|
"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"
|
|
|
|
)
|
|
|
|
|
2024-07-29 00:58:32 -04:00
|
|
|
type jwtAuth interface {
|
|
|
|
// Authenticated returns whether the request is authenticated. It also returns the claims.
|
|
|
|
Authenticated(r *http.Request) (claims, bool)
|
|
|
|
|
|
|
|
// Login attempts to return a JWT for the provided user and password.
|
|
|
|
Login(ctx context.Context, username, password string) (token string, err error)
|
|
|
|
|
|
|
|
// InstallVerifyMiddleware installs the JWT verifier middleware to the provided chi Router.
|
2024-08-03 00:05:02 -04:00
|
|
|
VerifyMiddleware() func(http.Handler) http.Handler
|
2024-07-29 00:58:32 -04:00
|
|
|
|
|
|
|
// InstallAuthMiddleware installs the JWT authenticator middleware to the provided chi Router.
|
2024-08-03 00:05:02 -04:00
|
|
|
AuthMiddleware() func(http.Handler) http.Handler
|
2024-07-29 00:58:32 -04:00
|
|
|
|
2024-08-10 18:21:13 -04:00
|
|
|
// PublicRoutes installs the auth route to the provided chi Router.
|
2024-08-03 00:16:23 -04:00
|
|
|
PublicRoutes(chi.Router)
|
2024-08-10 18:21:13 -04:00
|
|
|
|
|
|
|
// PublicRoutes installs the refresh route to the provided chi Router.
|
|
|
|
PrivateRoutes(chi.Router)
|
2024-07-29 00:58:32 -04:00
|
|
|
}
|
|
|
|
|
2024-07-29 00:21:07 -04:00
|
|
|
type claims map[string]interface{}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) Authenticated(r *http.Request) (claims, bool) {
|
2024-07-29 00:21:07 -04:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) VerifyMiddleware() func(http.Handler) http.Handler {
|
2024-08-03 00:05:02 -04:00
|
|
|
return jwtauth.Verifier(a.jwt)
|
2024-07-29 00:21:07 -04:00
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler {
|
2024-08-03 00:05:02 -04:00
|
|
|
return jwtauth.Authenticator(a.jwt)
|
2024-07-29 00:21:07 -04:00
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) Login(ctx context.Context, username, password string) (token string, err error) {
|
2024-07-29 00:21:07 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-29 00:47:58 -04:00
|
|
|
return a.newToken(found.ID), nil
|
2024-07-29 00:21:07 -04:00
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) newToken(uid int32) string {
|
2024-07-29 00:21:07 -04:00
|
|
|
claims := claims{
|
2024-08-10 18:21:13 -04:00
|
|
|
"sub": strconv.Itoa(int(uid)),
|
2024-07-29 00:21:07 -04:00
|
|
|
}
|
|
|
|
jwtauth.SetExpiryIn(claims, time.Hour*24*30) // one month
|
|
|
|
_, tokenString, err := a.jwt.Encode(claims)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return tokenString
|
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) PublicRoutes(r chi.Router) {
|
2024-08-04 00:55:28 -04:00
|
|
|
r.Post("/login", a.routeAuth)
|
2024-07-29 00:21:07 -04:00
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) PrivateRoutes(r chi.Router) {
|
2024-08-10 18:21:13 -04:00
|
|
|
r.Get("/refresh", a.routeRefresh)
|
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) allowInsecureCookie(r *http.Request) bool {
|
2024-08-15 13:28:03 -04:00
|
|
|
host := strings.Split(r.Host, ":")
|
|
|
|
v, has := a.cfg.AllowInsecure[host[0]]
|
2024-08-04 10:56:46 -04:00
|
|
|
return has && v
|
2024-08-04 08:55:12 -04:00
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) routeRefresh(w http.ResponseWriter, r *http.Request) {
|
2024-08-11 13:46:43 -04:00
|
|
|
jwToken, _, err := jwtauth.FromContext(r.Context())
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "Invalid token", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
existingSubjectUID := jwToken.Subject()
|
2024-08-10 18:21:13 -04:00
|
|
|
if existingSubjectUID == "" {
|
|
|
|
http.Error(w, "Invalid token", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
uid, err := strconv.Atoi(existingSubjectUID)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Str("sub", existingSubjectUID).Err(err).Msg("atoi uid for token refresh")
|
|
|
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tok := a.newToken(int32(uid))
|
|
|
|
|
|
|
|
cookie := &http.Cookie{
|
|
|
|
Name: "jwt",
|
|
|
|
Value: tok,
|
|
|
|
HttpOnly: true,
|
2024-08-15 13:28:03 -04:00
|
|
|
Secure: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
if a.allowInsecureCookie(r) {
|
|
|
|
cookie.Secure = false
|
2024-10-20 10:51:12 -04:00
|
|
|
cookie.SameSite = http.SameSiteLaxMode
|
2024-08-15 13:28:03 -04:00
|
|
|
log.Debug().Msg("same site none")
|
2024-08-10 18:21:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if cookie.Secure {
|
|
|
|
cookie.Domain = a.cfg.Domain
|
|
|
|
}
|
|
|
|
http.SetCookie(w, cookie)
|
|
|
|
|
|
|
|
jr := struct {
|
|
|
|
JWT string `json:"jwt"`
|
|
|
|
}{
|
|
|
|
JWT: tok,
|
|
|
|
}
|
|
|
|
|
|
|
|
render.JSON(w, r, &jr)
|
|
|
|
}
|
|
|
|
|
2024-10-22 08:39:15 -04:00
|
|
|
func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) {
|
2024-07-29 00:21:07 -04:00
|
|
|
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 == "" {
|
2024-08-04 07:39:52 -04:00
|
|
|
http.Error(w, "blank credentials", http.StatusBadRequest)
|
2024-07-29 00:21:07 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tok, err := a.Login(r.Context(), username, password)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
}
|
2024-08-04 14:55:15 -04:00
|
|
|
cookie := &http.Cookie{
|
2024-07-29 00:21:07 -04:00
|
|
|
Name: "jwt",
|
|
|
|
Value: tok,
|
|
|
|
HttpOnly: true,
|
2024-08-15 13:28:03 -04:00
|
|
|
Secure: true,
|
2024-08-04 14:55:15 -04:00
|
|
|
}
|
|
|
|
|
2024-08-15 13:28:03 -04:00
|
|
|
if a.allowInsecureCookie(r) {
|
|
|
|
cookie.Secure = false
|
2024-10-20 10:51:12 -04:00
|
|
|
cookie.SameSite = http.SameSiteLaxMode
|
2024-08-15 13:28:03 -04:00
|
|
|
} else {
|
2024-08-04 14:55:15 -04:00
|
|
|
cookie.Domain = a.cfg.Domain
|
|
|
|
}
|
2024-08-15 13:28:03 -04:00
|
|
|
|
2024-08-04 14:55:15 -04:00
|
|
|
http.SetCookie(w, cookie)
|
2024-07-29 00:21:07 -04:00
|
|
|
|
|
|
|
jr := struct {
|
|
|
|
JWT string `json:"jwt"`
|
|
|
|
}{
|
|
|
|
JWT: tok,
|
|
|
|
}
|
|
|
|
|
|
|
|
render.JSON(w, r, &jr)
|
|
|
|
}
|