// Copyright (C) 2022 Marius Schellenberger package server import ( "net/http" "time" "git.giftfish.de/ston1th/goacc/pkg/otp" "git.giftfish.de/ston1th/jwt/v3" ) type notFoundHandler struct { s *Server } func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { newContext(w, r, nf.s).NotFound() } func jwtHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if c, err := ctx.Request.Cookie(cookieName); err == nil { t, err := jwt.DecodeToken(c.Value) if err != nil { ctx.LogSetCookie(err, "decode failed") ctx.Redirect(root, http.StatusFound) return } if err = ctx.Srv.JWT.Verify(t); err != nil { ctx.LogSetCookie(err, "verify failed") ctx.Redirect(root, http.StatusFound) return } if t.Claims.GetString(totpClaim) != "" { path := ctx.Path() switch path { case totp, login, logout: ctx.Token = *t h(ctx) default: ctx.Redirect(totp, http.StatusFound) } return } if !ctx.Srv.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(createdClaim)) { ctx.Token = *t h(ctx) return } if err = ctx.Srv.JWT.Invalidate(t); err != nil { ctx.log.Error(err, "invalidation failed") } } ctx.SetCookie(nil, 0) h(ctx) } } func totpAuthHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if ctx.Totp() != "" { h(ctx) return } ctx.Redirect(root, http.StatusFound) } } func adminAuthHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if ctx.Admin() { h(ctx) return } ctx.Forbidden() } } func userAuthHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if ctx.Admin() { h(ctx) return } if ctx.User() == ctx.Var("user") { h(ctx) return } ctx.Forbidden() } } func authHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if ctx.LoggedOn() { h(ctx) return } ctx.Forbidden() } } func indexHandler(ctx *Context) { ctx.Redirect(login, http.StatusFound) } func sessionsHandler(ctx *Context) { ctx.Redirect(uroot+"sessions/"+ctx.User(), http.StatusFound) } func loginHandler(ctx *Context) { log := ctx.log.WithName("login") var data loginData data.LoginChallenge = ctx.Query("login_challenge") ctx.Template("loginHandler") ctx.Data = webData{ Title: "Login", BodyTitle: "Login", } if ctx.LoggedOn() { rdr := sessions if data.LoginChallenge != "" { u, err := ctx.Srv.DB.GetUserWithoutSecrets(ctx.User()) if err != nil { ctx.Error(err) return } rdr, err = ctx.ApproveLogin(u, data.LoginChallenge) if err != nil { ctx.Error(err) return } } ctx.Redirect(rdr, http.StatusFound) return } switch ctx.Method() { case "GET": ctx.Data.Data = data ctx.Exec() case "POST": data.LoginChallenge = ctx.Form("login_challenge") data.User = ctx.Form("user") ctx.Data.Data = data if !ctx.CheckXsrf() { return } password := ctx.Form("password") if data.User == "" || password == "" { ctx.Error("empty username or password") return } u, err := ctx.Srv.DB.Login(data.User, password) if err != nil { log.Error(err, "login failed", "user", data.User) ctx.Error("bad username or password") return } remember := ctx.Form("remember") if u.Secret != "" { ctx.SetCookie(jwt.Claims{ totpClaim: data.User, rememberClaim: remember, }, time.Minute) rdr := totp if data.LoginChallenge != "" { rdr += "?login_challenge=" + data.LoginChallenge } ctx.Redirect(rdr, http.StatusFound) return } log.Info("user logged in", "user", data.User) ctx.Login(u, remember) rdr := sessions if data.LoginChallenge != "" { rdr, err = ctx.ApproveLogin(u, data.LoginChallenge) if err != nil { ctx.Error(err) return } } ctx.Redirect(rdr, http.StatusFound) return } } func loginTotpHandler(ctx *Context) { if ctx.LoggedOn() { ctx.Redirect(sessions, http.StatusFound) return } ctx.Template("loginTotpHandler") ctx.Data = webData{ Title: "TOTP Verification", BodyTitle: "TOTP Veriftcation", } var data loginData switch ctx.Method() { case "GET": data.LoginChallenge = ctx.Query("login_challenge") ctx.Data.Data = data ctx.Exec() case "POST": data.LoginChallenge = ctx.Form("login_challenge") ctx.Data.Data = data if !ctx.CheckXsrf() { return } pin := ctx.Form("pin") if pin == "" { ctx.Error("empty pin") return } user := ctx.Totp() log := ctx.log.WithValues("user", user) u, err := ctx.Srv.DB.Totp(user, pin) if err != nil { log.Error(err, "totp failed") ctx.Error(err) return } log.Info("totp successful") ctx.Login(u, ctx.Remember()) rdr := sessions if data.LoginChallenge != "" { rdr, err = ctx.ApproveLogin(u, data.LoginChallenge) if err != nil { ctx.Error(err) ctx.log.Error(err, "error approving login") ctx.Error("error approving login") return } } ctx.Redirect(rdr, http.StatusFound) } } func consentHandler(ctx *Context) { ctx.Template("consentHandler") ctx.Data = webData{ Title: "Authorize", BodyTitle: "Authorize", } var data consentData u, err := ctx.Srv.DB.GetUserWithoutSecrets(ctx.User()) if err != nil { ctx.Error(err) return } switch ctx.Method() { case "GET": data.ConsentChallenge = ctx.Query("consent_challenge") ctx.Data.Data = data cr, err := ctx.Consent(data.ConsentChallenge) if err != nil { ctx.log.Error(err, "error getting consent info") ctx.Error("error getting consent info") return } if cr.GetSkip() { rdr, err := ctx.ApproveConsent(u, data.ConsentChallenge) if err != nil { ctx.log.Error(err, "error approving consent") ctx.Error("error approving consent") return } ctx.Redirect(rdr, http.StatusFound) } data.Info = cr ctx.Data.Data = data ctx.Exec() case "POST": data.ConsentChallenge = ctx.Form("consent_challenge") ctx.Data.Data = data if !ctx.CheckXsrf() { return } consent := ctx.Form("consent") var err error rdr := root if consent == "approve" { rdr, err = ctx.ApproveConsent(u, data.ConsentChallenge) if err != nil { ctx.log.Error(err, "error approving consent") ctx.Error("error approving consent") return } } else { rdr, err = ctx.RejectConsent(data.ConsentChallenge) if err != nil { ctx.log.Error(err, "error rejecting consent") ctx.Error("error rejecting consent") return } } ctx.Redirect(rdr, http.StatusFound) } } func usersHandler(ctx *Context) { if !ctx.LoggedOn() { ctx.NotFound() return } ctx.Template("usersHandler") ctx.Data = webData{ Title: "Users", BodyTitle: "Users", User: ctx.User(), } us, err := ctx.Srv.DB.GetUsers() if err != nil { ctx.Data.Msg = err.Error() } ctx.Data.Data = us ctx.Exec() } func userNewHandler(ctx *Context) { ctx.Template("userNewHandler") ctx.Data = webData{ Title: "New User", BodyTitle: "New User", } switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } user := ctx.Form("user") email := ctx.Form("email") password := ctx.Form("password") repeat := ctx.Form("repeat") admin := ctx.Form("admin") == "0" disabled := ctx.Form("disabled") == "0" if user == "" || email == "" || password == "" { ctx.Error("empty user, email or password") return } if password != repeat { ctx.Error("passwords do not match") return } if err := ctx.Srv.DB.CreateUser(user, email, password, admin, disabled); err != nil { ctx.Error(err) return } ctx.log.Info("user created", "user", user, "actor", ctx.User(), "admin", admin, "disabled", disabled) ctx.Redirect(users, http.StatusFound) } } func userEditHandler(ctx *Context) { ctx.Template("userEditHandler") ctx.Data = webData{ Title: "Edit User", BodyTitle: "Edit User", } u, err := ctx.Srv.DB.GetUserWithoutSecrets(ctx.Var("user")) if err != nil { ctx.Error(err) return } ctx.Data.Data = u switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } user := ctx.Var("user") self := ctx.User() log := ctx.log.WithValues("user", user, "actor", self) password := ctx.Form("password") repeat := ctx.Form("repeat") admin := ctx.Form("admin") == "0" disabled := ctx.Form("disabled") == "0" if password != repeat { ctx.Error("passwords do not match") return } if ctx.Admin() { if err := ctx.Srv.DB.AdminUpdateUser(user, password, admin, disabled); err != nil { ctx.Error(err) return } log.Info("user updated", "admin", admin, "disabled", disabled) if user == self { logoutHandler(ctx) return } ctx.Redirect(users, http.StatusFound) return } if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil { ctx.Error(err) return } log.Info("user updated") ctx.Redirect(root, http.StatusFound) } } func userTotpHandler(ctx *Context) { ctx.Template("userTotpHandler") ctx.Data = webData{ Title: "TOTP", BodyTitle: "TOTP", } req := otp.Request{Username: ctx.Var("user")} u, err := ctx.Srv.DB.GetUser(req.Username) if err != nil { ctx.Error(err) return } if u.Secret == "" { switch ctx.Method() { case "GET": req, err = otp.New(req.Username, ctx.Srv.Config.Issuer) if err != nil { ctx.Error(err) return } case "POST": req.URL = ctx.Form("url") req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL) if err != nil { ctx.Error(err) return } } } switch ctx.Method() { case "GET": ctx.Data.Data = req ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } pin := ctx.Form("pin") if pin == "" { ctx.Error("empty pin") return } ctx.Data.Data = req secret := req.Secret if u.Secret != "" { secret = u.Secret } valid := otp.Validate(pin, secret) user := ctx.User() if user != req.Username && ctx.Admin() { valid = true } if !valid { ctx.Error("validation failed") return } if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil { ctx.Error(err) return } ctx.log.Info("totp changed", "user", user, "enabled", req.Secret != "") ctx.Redirect(uroot+"edit/"+req.Username, http.StatusFound) } } func userSessionsHandler(ctx *Context) { ctx.Template("userSessionsHandler") ctx.Data = webData{ Title: "Sessions", BodyTitle: "Sessions", } user := ctx.Var("user") log := ctx.log.WithValues("user", user) switch ctx.Method() { case "GET": s, err := ctx.Sessions(user) if err != nil { log.Error(err, "error getting sessions") ctx.Error("error getting sessions") return } ctx.Data.Data = s ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } client := ctx.Form("client") all := ctx.Form("all") == "all" err := ctx.RevokeSession(user, client, all) if err != nil { log.Error(err, "error revoking session", "client", client, "all", all) ctx.Error("error revoking session") return } sessionsHandler(ctx) } } func userUnlockHandler(ctx *Context) { user := ctx.Var("user") log := ctx.log.WithValues("user", user, "actor", ctx.User()) err := ctx.Srv.DB.UnlockUser(user) if err != nil { log.Error(err, "user unlock failed") } else { log.Info("user unlocked") } ctx.Redirect(users, http.StatusFound) } func userDelHandler(ctx *Context) { ctx.Template("userDelHandler") ctx.Data = webData{ Title: "Delete User", BodyTitle: "Delete User", } switch ctx.Method() { case "GET": user, err := ctx.Srv.DB.GetUserWithoutSecrets(ctx.Var("user")) if err != nil { ctx.Error(err) return } ctx.Data.Data = user ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } self := ctx.User() user := ctx.Var("user") err := ctx.Srv.DB.DeleteUser(user) log := ctx.log.WithValues("user", user, "actor", self) if err != nil { log.Error(err, "delete failed") } else { log.Info("delete successful") } if user == self { logoutHandler(ctx) return } ctx.Redirect(users, http.StatusFound) } } func logoutHandler(ctx *Context) { totp := false user := ctx.User() if user == "" { user = ctx.Totp() if user != "" { totp = true } } if user != "" { ctx.Srv.JWT.Invalidate(&ctx.Token) } else { user = empty } ctx.log.Info("logout", "user", user, "totp", totp) ctx.SetCookie(nil, 0) ctx.Redirect(root, http.StatusFound) } func authBasicHandler(ctx *Context) { if ctx.Method() != "GET" { ctx.PlainUnauthorized() return } username, password, ok := ctx.Request.BasicAuth() if !ok { ctx.PlainUnauthorized() return } if ctx.Srv.BasicAuth.Auth(username, password) { ctx.PlainOK() return } ctx.PlainForbidden() }