edge/pkg/module/auth/http/local_handler.go

209 lines
5.0 KiB
Go

package http
import (
"html/template"
"net/http"
"time"
_ "embed"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
"forge.cadoles.com/arcad/edge/pkg/module/auth/http/passwd"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
//go:embed templates/login.html.tmpl
var rawLoginTemplate string
var loginTemplate *template.Template
var (
errNotFound = errors.New("not found")
errInvalidPassword = errors.New("invalid password")
)
func init() {
loginTemplate = template.Must(template.New("").Parse(rawLoginTemplate))
}
type LocalHandler struct {
router chi.Router
algo jwa.KeyAlgorithm
key jwk.Key
getCookieDomain GetCookieDomainFunc
cookieDuration time.Duration
accounts map[string]LocalAccount
}
func (h *LocalHandler) initRouter(prefix string) {
router := chi.NewRouter()
router.Route(prefix, func(r chi.Router) {
r.Get("/login", h.serveForm)
r.Post("/login", h.handleForm)
r.Get("/logout", h.handleLogout)
})
h.router = router
}
type loginTemplateData struct {
URL string
Username string
Password string
Message string
}
func (h *LocalHandler) serveForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data := loginTemplateData{
URL: r.URL.String(),
Username: "",
Password: "",
Message: "",
}
if err := loginTemplate.Execute(w, data); err != nil {
logger.Error(ctx, "could not execute login page template", logger.E(errors.WithStack(err)))
}
}
func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := r.ParseForm(); err != nil {
logger.Error(ctx, "could not parse form", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
data := loginTemplateData{
URL: r.URL.String(),
Username: username,
Password: password,
Message: "",
}
account, err := h.authenticate(username, password)
if err != nil {
if errors.Is(err, errNotFound) || errors.Is(err, errInvalidPassword) {
data.Message = "Invalid username or password."
if err := loginTemplate.Execute(w, data); err != nil {
logger.Error(ctx, "could not execute login page template", logger.E(errors.WithStack(err)))
}
return
}
logger.Error(ctx, "could not authenticate account", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
account.Claims[auth.ClaimIssuer] = "local"
token, err := generateSignedToken(h.algo, h.key, account.Claims)
if err != nil {
logger.Error(ctx, "could not generate signed token", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookieDomain, err := h.getCookieDomain(r)
if err != nil {
logger.Error(ctx, "could not retrieve cookie domain", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: auth.CookieName,
Value: string(token),
Domain: cookieDomain,
HttpOnly: false,
Expires: time.Now().Add(h.cookieDuration),
Path: "/",
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (h *LocalHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
cookieDomain, err := h.getCookieDomain(r)
if err != nil {
logger.Error(r.Context(), "could not retrieve cookie domain", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: auth.CookieName,
Value: "",
HttpOnly: false,
Expires: time.Unix(0, 0),
Domain: cookieDomain,
Path: "/",
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (h *LocalHandler) authenticate(username, password string) (*LocalAccount, error) {
account, exists := h.accounts[username]
if !exists {
return nil, errors.WithStack(errNotFound)
}
matches, err := passwd.Match(account.Algo, password, account.Password)
if err != nil {
return nil, errors.WithStack(err)
}
if !matches {
return nil, errors.WithStack(errInvalidPassword)
}
return &account, nil
}
func NewLocalHandler(algo jwa.KeyAlgorithm, key jwk.Key, funcs ...LocalHandlerOptionFunc) *LocalHandler {
opts := defaultLocalHandlerOptions()
for _, fn := range funcs {
fn(opts)
}
handler := &LocalHandler{
algo: algo,
key: key,
accounts: toAccountsMap(opts.Accounts),
getCookieDomain: opts.GetCookieDomain,
cookieDuration: opts.CookieDuration,
}
handler.initRouter(opts.RoutePrefix)
return handler
}
// ServeHTTP implements http.Handler.
func (h *LocalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.router.ServeHTTP(w, r)
}
var _ http.Handler = &LocalHandler{}