added IMAP, LetsEncrypt, update layout
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package webui
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"mailgoserver/internal/db"
|
||||
)
|
||||
|
||||
// mailboxWebauthnSessionCookie mirrors webauthnSessionCookie but kept separate so an
|
||||
// in-progress admin passkey ceremony and an in-progress mailbox one (e.g. different
|
||||
// browser tabs) can never collide.
|
||||
const mailboxWebauthnSessionCookie = "mailgoserver_mailbox_webauthn_session"
|
||||
|
||||
// mailboxWebauthnUser adapts a Mailbox + its stored credentials to webauthn.User,
|
||||
// mirroring webauthnUser.
|
||||
type mailboxWebauthnUser struct {
|
||||
mailbox *db.Mailbox
|
||||
creds []db.MailboxWebAuthnCredential
|
||||
}
|
||||
|
||||
func (u *mailboxWebauthnUser) WebAuthnID() []byte {
|
||||
sum := sha256.Sum256([]byte("mailbox-" + strconv.FormatInt(u.mailbox.ID, 10)))
|
||||
return sum[:]
|
||||
}
|
||||
func (u *mailboxWebauthnUser) WebAuthnName() string { return u.mailbox.Email }
|
||||
func (u *mailboxWebauthnUser) WebAuthnDisplayName() string { return u.mailbox.Email }
|
||||
func (u *mailboxWebauthnUser) WebAuthnCredentials() []webauthn.Credential {
|
||||
out := make([]webauthn.Credential, 0, len(u.creds))
|
||||
for _, c := range u.creds {
|
||||
var cred webauthn.Credential
|
||||
if err := json.Unmarshal([]byte(c.CredentialData), &cred); err == nil {
|
||||
out = append(out, cred)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *App) mailboxWebauthnUserFor(mbox *db.Mailbox) (*mailboxWebauthnUser, error) {
|
||||
creds, err := a.DB.ListMailboxWebAuthnCredentials(mbox.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mailboxWebauthnUser{mailbox: mbox, creds: creds}, nil
|
||||
}
|
||||
|
||||
func saveMailboxWebauthnSession(w http.ResponseWriter, s *webauthn.SessionData) error {
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: mailboxWebauthnSessionCookie, Value: base64.URLEncoding.EncodeToString(b),
|
||||
Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: 5 * 60,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadMailboxWebauthnSession(r *http.Request) (*webauthn.SessionData, error) {
|
||||
c, err := r.Cookie(mailboxWebauthnSessionCookie)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := base64.URLEncoding.DecodeString(c.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s webauthn.SessionData
|
||||
if err := json.Unmarshal(raw, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func clearMailboxWebauthnSession(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{Name: mailboxWebauthnSessionCookie, Value: "", Path: "/", MaxAge: -1})
|
||||
}
|
||||
|
||||
func (a *App) webmailPasskeyRegisterBegin(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
wa, err := a.buildWebAuthn()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "WebAuthn is not configured correctly: " + err.Error()})
|
||||
return
|
||||
}
|
||||
wu, err := a.mailboxWebauthnUserFor(mbox)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not load account"})
|
||||
return
|
||||
}
|
||||
creation, session, err := wa.BeginRegistration(wu)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := saveMailboxWebauthnSession(w, session); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not start registration"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, creation)
|
||||
}
|
||||
|
||||
func (a *App) webmailPasskeyRegisterFinish(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
wa, err := a.buildWebAuthn()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
session, err := loadMailboxWebauthnSession(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, M{"error": "Registration session expired — try again"})
|
||||
return
|
||||
}
|
||||
wu, err := a.mailboxWebauthnUserFor(mbox)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not load account"})
|
||||
return
|
||||
}
|
||||
cred, err := wa.FinishRegistration(wu, *session, r)
|
||||
clearMailboxWebauthnSession(w)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
data, err := json.Marshal(cred)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not save passkey"})
|
||||
return
|
||||
}
|
||||
name := r.URL.Query().Get("name")
|
||||
if name == "" {
|
||||
name = "Passkey"
|
||||
}
|
||||
if err := a.DB.CreateMailboxWebAuthnCredential(mbox.ID, name, base64.URLEncoding.EncodeToString(cred.ID), string(data)); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not save passkey"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, M{"success": true})
|
||||
}
|
||||
|
||||
func (a *App) webmailPasskeyRemove(w http.ResponseWriter, r *http.Request) {
|
||||
mbox := mailboxFromContext(r)
|
||||
if err := a.DB.DeleteMailboxWebAuthnCredential(pathID(r), mbox.ID); err != nil {
|
||||
setFlash(w, "error", "Could not remove passkey")
|
||||
} else {
|
||||
setFlash(w, "success", "Passkey removed")
|
||||
}
|
||||
http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound)
|
||||
}
|
||||
|
||||
// webmailPasskeyLoginBegin starts the passkey ceremony for the mailbox that's already
|
||||
// passed its password and is now at the MFA step.
|
||||
func (a *App) webmailPasskeyLoginBegin(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxID := pendingMailboxMFAID(r)
|
||||
if mailboxID == 0 {
|
||||
writeJSON(w, http.StatusUnauthorized, M{"error": "No login in progress"})
|
||||
return
|
||||
}
|
||||
mbox, err := a.DB.GetMailboxByID(mailboxID)
|
||||
if err != nil || mbox == nil {
|
||||
writeJSON(w, http.StatusUnauthorized, M{"error": "No login in progress"})
|
||||
return
|
||||
}
|
||||
wa, err := a.buildWebAuthn()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
wu, err := a.mailboxWebauthnUserFor(mbox)
|
||||
if err != nil || len(wu.creds) == 0 {
|
||||
writeJSON(w, http.StatusBadRequest, M{"error": "No passkeys registered"})
|
||||
return
|
||||
}
|
||||
assertion, session, err := wa.BeginLogin(wu)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := saveMailboxWebauthnSession(w, session); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not start login"})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, assertion)
|
||||
}
|
||||
|
||||
func (a *App) webmailPasskeyLoginFinish(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxID := pendingMailboxMFAID(r)
|
||||
if mailboxID == 0 {
|
||||
writeJSON(w, http.StatusUnauthorized, M{"error": "No login in progress"})
|
||||
return
|
||||
}
|
||||
mbox, err := a.DB.GetMailboxByID(mailboxID)
|
||||
if err != nil || mbox == nil {
|
||||
writeJSON(w, http.StatusUnauthorized, M{"error": "No login in progress"})
|
||||
return
|
||||
}
|
||||
wa, err := a.buildWebAuthn()
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
session, err := loadMailboxWebauthnSession(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, M{"error": "Login session expired — try again"})
|
||||
return
|
||||
}
|
||||
wu, err := a.mailboxWebauthnUserFor(mbox)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not load account"})
|
||||
return
|
||||
}
|
||||
if _, err := wa.FinishLogin(wu, *session, r); err != nil {
|
||||
clearMailboxWebauthnSession(w)
|
||||
writeJSON(w, http.StatusUnauthorized, M{"error": "Passkey verification failed"})
|
||||
return
|
||||
}
|
||||
clearMailboxWebauthnSession(w)
|
||||
|
||||
token, err := a.DB.CreateMailboxSession(mbox.ID, true, sessionTTL)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, M{"error": "Could not start session"})
|
||||
return
|
||||
}
|
||||
clearMailboxPendingMFACookie(w)
|
||||
setMailboxSessionCookie(w, token, r.TLS != nil)
|
||||
writeJSON(w, http.StatusOK, M{"success": true})
|
||||
}
|
||||
Reference in New Issue
Block a user