595 lines
12 KiB
Go
595 lines
12 KiB
Go
// 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()
|
|
}
|