5	cryptorand "crypto/rand"
 
13	"golang.org/x/crypto/bcrypt"
 
14	"golang.org/x/text/secure/precis"
 
16	"github.com/mjl-/mox/mlog"
 
17	"github.com/mjl-/mox/mox-"
 
18	"github.com/mjl-/mox/store"
 
21// Admin is for admin logins, with authentication by password, and sessions only
 
22// stored in memory only, with lifetime 12 hour after last use, with a maximum of
 
24var Admin SessionAuth = &adminSessionAuth{
 
25	sessions: map[store.SessionToken]adminSession{},
 
28// Good chance of fitting one working day.
 
29const adminSessionLifetime = 12 * time.Hour
 
31type adminSession struct {
 
32	sessionToken store.SessionToken
 
33	csrfToken    store.CSRFToken
 
37type adminSessionAuth struct {
 
39	sessions map[store.SessionToken]adminSession
 
42func (a *adminSessionAuth) login(ctx context.Context, log mlog.Log, username, password string) (valid, disabled bool, name string, rerr error) {
 
46	p := mox.ConfigDirPath(mox.Conf.Static.AdminPasswordFile)
 
47	buf, err := os.ReadFile(p)
 
49		return false, false, "", fmt.Errorf("reading password file: %v", err)
 
51	passwordhash := strings.TrimSpace(string(buf))
 
53	pw, err := precis.OpaqueString.String(password)
 
57	if err := bcrypt.CompareHashAndPassword([]byte(passwordhash), []byte(password)); err != nil {
 
58		return false, false, "", nil
 
61	return true, false, "(admin)", nil
 
64func (a *adminSessionAuth) add(ctx context.Context, log mlog.Log, accountName string, loginAddress string) (sessionToken store.SessionToken, csrfToken store.CSRFToken, rerr error) {
 
68	// Cleanup expired sessions.
 
69	for st, s := range a.sessions {
 
70		if time.Until(s.expires) < 0 {
 
71			delete(a.sessions, st)
 
75	// Ensure we have at most 10 sessions.
 
76	if len(a.sessions) > 10 {
 
77		var oldest *store.SessionToken
 
78		for _, s := range a.sessions {
 
79			if oldest == nil || s.expires.Before(a.sessions[*oldest].expires) {
 
80				oldest = &s.sessionToken
 
83		delete(a.sessions, *oldest)
 
86	// Generate new tokens.
 
87	var sessionData, csrfData [16]byte
 
88	if _, err := cryptorand.Read(sessionData[:]); err != nil {
 
91	if _, err := cryptorand.Read(csrfData[:]); err != nil {
 
94	sessionToken = store.SessionToken(base64.RawURLEncoding.EncodeToString(sessionData[:]))
 
95	csrfToken = store.CSRFToken(base64.RawURLEncoding.EncodeToString(csrfData[:]))
 
98	a.sessions[sessionToken] = adminSession{sessionToken, csrfToken, time.Now().Add(adminSessionLifetime)}
 
99	return sessionToken, csrfToken, nil
 
102func (a *adminSessionAuth) use(ctx context.Context, log mlog.Log, accountName string, sessionToken store.SessionToken, csrfToken store.CSRFToken) (loginAddress string, rerr error) {
 
106	s, ok := a.sessions[sessionToken]
 
108		return "", fmt.Errorf("unknown session (due to server restart or 10 new admin sessions)")
 
109	} else if time.Until(s.expires) < 0 {
 
110		return "", fmt.Errorf("session expired (after 12 hours inactivity)")
 
111	} else if csrfToken != "" && csrfToken != s.csrfToken {
 
112		return "", fmt.Errorf("mismatch between csrf and session tokens")
 
114	s.expires = time.Now().Add(adminSessionLifetime)
 
115	a.sessions[sessionToken] = s
 
119func (a *adminSessionAuth) remove(ctx context.Context, log mlog.Log, accountName string, sessionToken store.SessionToken) error {
 
123	if _, ok := a.sessions[sessionToken]; !ok {
 
124		return fmt.Errorf("unknown session")
 
126	delete(a.sessions, sessionToken)