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{}