mirror of
https://codeberg.org/readeck/readeck.git
synced 2025-12-23 13:40:17 +00:00
TOTP authentication
This is only the first part. When a totp_secret exists for a user, the authentication then asks for the code and carries on. The totp lib can handle 6 or 8 letter codes and sha1, sha256 and sha512. For maximum compatibility with Google Authenticator though, it sticks to 6 character and sha1.
This commit is contained in:
@@ -7,12 +7,18 @@ package signin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"codeberg.org/readeck/readeck/configs"
|
||||
"codeberg.org/readeck/readeck/internal/auth/users"
|
||||
"codeberg.org/readeck/readeck/internal/server"
|
||||
"codeberg.org/readeck/readeck/pkg/forms"
|
||||
"codeberg.org/readeck/readeck/pkg/totp"
|
||||
)
|
||||
|
||||
// SetupRoutes mounts the routes for the auth domain.
|
||||
@@ -37,6 +43,8 @@ func newAuthHandler(s *server.Server) *authHandler {
|
||||
s.AddRoute("/login", r)
|
||||
r.Get("/", h.login)
|
||||
r.Post("/", h.login)
|
||||
r.Get("/mfa", h.mfa)
|
||||
r.Post("/mfa", h.mfa)
|
||||
|
||||
// Recovery
|
||||
r.With(server.WithPermission("email", "send")).Route("/recover", func(r chi.Router) {
|
||||
@@ -54,6 +62,21 @@ func newAuthHandler(s *server.Server) *authHandler {
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *authHandler) redirTo(w http.ResponseWriter, r *http.Request, redir string) {
|
||||
// Get redirection from a form "redirect" parameter
|
||||
// Since it goes to Redirect(), it will be sanitized there
|
||||
// and can only stay within the app.
|
||||
if redir == "" || strings.HasPrefix(redir, "/login") {
|
||||
redir = "/"
|
||||
}
|
||||
server.Redirect(w, r, redir)
|
||||
}
|
||||
|
||||
func (h *authHandler) redirToMFA(w http.ResponseWriter, r *http.Request, redir string) {
|
||||
v := url.Values{"r": {redir}}
|
||||
server.Redirect(w, r, "/login/mfa?"+v.Encode())
|
||||
}
|
||||
|
||||
func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
f := newLoginForm(server.Locale(r))
|
||||
|
||||
@@ -62,8 +85,12 @@ func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
f.Get("redirect").Set(r.URL.Query().Get("r"))
|
||||
|
||||
// Do we have a session already?
|
||||
if server.GetSession(r).Payload.User != 0 {
|
||||
server.Redirect(w, r, h.cleanRedir(f.Get("redirect").String()))
|
||||
if sess := server.GetSession(r); sess.Payload.User != 0 {
|
||||
if sess.Payload.RequiresMFA {
|
||||
h.redirToMFA(w, r, f.Get("redirect").String())
|
||||
return
|
||||
}
|
||||
h.redirTo(w, r, f.Get("redirect").String())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -73,17 +100,21 @@ func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if f.IsValid() {
|
||||
user := checkUser(f)
|
||||
|
||||
if user != nil {
|
||||
// User is authenticated, let's carry on
|
||||
sess := server.GetSession(r)
|
||||
sess.Payload.User = user.ID
|
||||
sess.Payload.Seed = user.Seed
|
||||
sess.Payload.RequiresMFA = user.RequiresMFA()
|
||||
sess.Save(w, r)
|
||||
|
||||
// Get redirection from a form "redirect" parameter
|
||||
// Since it goes to Redirect(), it will be sanitized there
|
||||
// and can only stay within the app.
|
||||
server.Redirect(w, r, h.cleanRedir(f.Get("redirect").String()))
|
||||
if sess.Payload.RequiresMFA {
|
||||
h.redirToMFA(w, r, f.Get("redirect").String())
|
||||
return
|
||||
}
|
||||
|
||||
h.redirTo(w, r, f.Get("redirect").String())
|
||||
return
|
||||
}
|
||||
// we must set the content type to avoid the
|
||||
@@ -99,6 +130,76 @@ func (h *authHandler) login(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *authHandler) mfa(w http.ResponseWriter, r *http.Request) {
|
||||
sess := server.GetSession(r)
|
||||
if !sess.Payload.RequiresMFA {
|
||||
server.Redirect(w, r, "/")
|
||||
return
|
||||
}
|
||||
|
||||
user := new(users.User)
|
||||
found, err := users.Users.Query().
|
||||
SelectAppend(goqu.C("totp_secret").Table("u")).
|
||||
Where(goqu.C("id").Eq(sess.Payload.User)).
|
||||
ScanStruct(user)
|
||||
if err != nil {
|
||||
server.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
server.Status(w, r, 404)
|
||||
return
|
||||
}
|
||||
|
||||
f := forms.Must(
|
||||
forms.WithTranslator(r.Context(), server.Locale(r)),
|
||||
forms.NewTextField("code", forms.Required, forms.StrLen(6, 6)),
|
||||
forms.NewTextField("redirect"),
|
||||
)
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
// Set the redirect value from the query string
|
||||
f.Get("redirect").Set(r.URL.Query().Get("r"))
|
||||
}
|
||||
|
||||
status := http.StatusOK
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
forms.Bind(f, r)
|
||||
if f.IsValid() {
|
||||
code := new(totp.Code)
|
||||
if err := configs.Keys.TOTPKey().DecodeJSON(user.TOTPSecret, code); err != nil {
|
||||
server.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := code.Verify(f.Get("code").String(), time.Now().UTC(), 1)
|
||||
if err != nil {
|
||||
server.Err(w, r, err)
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
sess.Payload.RequiresMFA = false
|
||||
sess.Save(w, r)
|
||||
|
||||
redir := f.Get("redirect").String()
|
||||
if redir == "" || strings.HasPrefix(redir, "/login") {
|
||||
redir = "/"
|
||||
}
|
||||
|
||||
server.Redirect(w, r, redir)
|
||||
return
|
||||
}
|
||||
f.Get("code").AddErrors(forms.Gettext("Invalid code"))
|
||||
}
|
||||
status = http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
server.RenderTemplate(w, r, status, "/auth/totp", server.TC{
|
||||
"Form": f,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *authHandler) logout(w http.ResponseWriter, r *http.Request) {
|
||||
// Clear session
|
||||
sess := server.GetSession(r)
|
||||
@@ -106,10 +207,3 @@ func (h *authHandler) logout(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
server.Redirect(w, r, "/login")
|
||||
}
|
||||
|
||||
func (h *authHandler) cleanRedir(redir string) string {
|
||||
if redir == "" || strings.HasPrefix(redir, "/login") {
|
||||
redir = "/"
|
||||
}
|
||||
return redir
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user