2023-03-20 16:40:08 +01:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"html/template"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
_ "embed"
|
|
|
|
|
2023-09-29 07:41:01 +02:00
|
|
|
"forge.cadoles.com/arcad/edge/pkg/jwtutil"
|
2023-03-20 16:40:08 +01:00
|
|
|
"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 {
|
2023-09-29 07:41:01 +02:00
|
|
|
router chi.Router
|
|
|
|
key jwk.Key
|
|
|
|
signingAlgorithm jwa.SignatureAlgorithm
|
|
|
|
getCookieDomain GetCookieDomainFunc
|
|
|
|
cookieDuration time.Duration
|
|
|
|
accounts map[string]LocalAccount
|
2023-03-20 16:40:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(ctx, "could not execute login page template", logger.CapturedE(errors.WithStack(err)))
|
2023-03-20 16:40:08 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *LocalHandler) handleForm(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(ctx, "could not parse form", logger.CapturedE(errors.WithStack(err)))
|
2023-03-20 16:40:08 +01:00
|
|
|
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 {
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(ctx, "could not execute login page template", logger.CapturedE(errors.WithStack(err)))
|
2023-03-20 16:40:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(ctx, "could not authenticate account", logger.CapturedE(errors.WithStack(err)))
|
2023-03-20 16:40:08 +01:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-04-18 17:57:16 +02:00
|
|
|
account.Claims[auth.ClaimIssuer] = "local"
|
|
|
|
|
2023-09-29 07:41:01 +02:00
|
|
|
token, err := jwtutil.SignedToken(h.key, h.signingAlgorithm, account.Claims)
|
2023-03-20 16:40:08 +01:00
|
|
|
if err != nil {
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(ctx, "could not generate signed token", logger.CapturedE(errors.WithStack(err)))
|
2023-03-20 16:40:08 +01:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-04-05 15:19:22 +02:00
|
|
|
cookieDomain, err := h.getCookieDomain(r)
|
|
|
|
if err != nil {
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(ctx, "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err)))
|
2023-04-05 15:19:22 +02:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-20 16:40:08 +01:00
|
|
|
cookie := http.Cookie{
|
|
|
|
Name: auth.CookieName,
|
|
|
|
Value: string(token),
|
2023-04-05 15:19:22 +02:00
|
|
|
Domain: cookieDomain,
|
2023-03-20 16:40:08 +01:00
|
|
|
HttpOnly: false,
|
2023-03-28 20:37:57 +02:00
|
|
|
Expires: time.Now().Add(h.cookieDuration),
|
2023-03-20 16:40:08 +01:00
|
|
|
Path: "/",
|
|
|
|
}
|
|
|
|
|
|
|
|
http.SetCookie(w, &cookie)
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *LocalHandler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
2023-04-05 15:19:22 +02:00
|
|
|
cookieDomain, err := h.getCookieDomain(r)
|
|
|
|
if err != nil {
|
2023-10-19 21:47:09 +02:00
|
|
|
logger.Error(r.Context(), "could not retrieve cookie domain", logger.CapturedE(errors.WithStack(err)))
|
2023-04-05 15:19:22 +02:00
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-03-20 16:40:08 +01:00
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
Name: auth.CookieName,
|
|
|
|
Value: "",
|
|
|
|
HttpOnly: false,
|
|
|
|
Expires: time.Unix(0, 0),
|
2023-04-05 15:19:22 +02:00
|
|
|
Domain: cookieDomain,
|
2023-03-20 16:40:08 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-09-29 07:41:01 +02:00
|
|
|
func NewLocalHandler(key jwk.Key, signingAlgorithm jwa.SignatureAlgorithm, funcs ...LocalHandlerOptionFunc) *LocalHandler {
|
2023-03-20 16:40:08 +01:00
|
|
|
opts := defaultLocalHandlerOptions()
|
|
|
|
for _, fn := range funcs {
|
|
|
|
fn(opts)
|
|
|
|
}
|
|
|
|
|
|
|
|
handler := &LocalHandler{
|
2023-09-29 07:41:01 +02:00
|
|
|
key: key,
|
|
|
|
signingAlgorithm: signingAlgorithm,
|
|
|
|
accounts: toAccountsMap(opts.Accounts),
|
|
|
|
getCookieDomain: opts.GetCookieDomain,
|
|
|
|
cookieDuration: opts.CookieDuration,
|
2023-03-20 16:40:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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{}
|