// Package auth implements authentication services for chasquid. package auth import ( "bytes" "encoding/base64" "errors" "fmt" "math/rand" "strings" "time" "blitiri.com.ar/go/chasquid/internal/normalize" "blitiri.com.ar/go/chasquid/internal/trace" ) // Backend is the common interface for all authentication backends. type Backend interface { Authenticate(user, password string) (bool, error) Exists(user string) (bool, error) Reload() error } // NoErrorBackend is the interface for authentication backends that don't need // to emit errors. This allows backends to avoid unnecessary complexity, in // exchange for a bit more here. // They can be converted to normal Backend using WrapNoErrorBackend (defined // below). type NoErrorBackend interface { Authenticate(user, password string) bool Exists(user string) bool Reload() error } // Authenticator tracks the backends for each domain, and allows callers to // query them with a more practical API. type Authenticator struct { // Registered backends, map of domain (string) -> Backend. // Backend operations will _not_ include the domain in the username. backends map[string]Backend // Fallback backend, to use when backends[domain] (which may not exist) // did not yield a positive result. // Note that this backend gets the user with the domain included, of the // form "user@domain" (if available). Fallback Backend // How long Authenticate calls should last, approximately. // This will be applied both for successful and unsuccessful attempts. // We will increase this number by 0-20%. AuthDuration time.Duration } // NewAuthenticator returns a new Authenticator with no backends. func NewAuthenticator() *Authenticator { return &Authenticator{ backends: map[string]Backend{}, AuthDuration: 100 * time.Millisecond, } } // Register a backend to use for the given domain. func (a *Authenticator) Register(domain string, be Backend) { a.backends[domain] = be } // Authenticate the user@domain with the given password. func (a *Authenticator) Authenticate(user, domain, password string) (bool, error) { tr := trace.New("Auth.Authenticate", user+"@"+domain) defer tr.Finish() // Make sure the call takes a.AuthDuration + 0-20% regardless of the // outcome, to prevent basic timing attacks. defer func(start time.Time) { elapsed := time.Since(start) delay := a.AuthDuration - elapsed if delay > 0 { maxDelta := int64(float64(delay) * 0.2) delay += time.Duration(rand.Int63n(maxDelta)) time.Sleep(delay) } }(time.Now()) if be, ok := a.backends[domain]; ok { ok, err := be.Authenticate(user, password) tr.Debugf("Backend: %v %v", ok, err) if ok || err != nil { return ok, err } } if a.Fallback != nil { id := user if domain != "" { id = user + "@" + domain } ok, err := a.Fallback.Authenticate(id, password) tr.Debugf("Fallback: %v %v", ok, err) return ok, err } tr.Debugf("Rejected by default") return false, nil } // Exists checks that user@domain exists. func (a *Authenticator) Exists(user, domain string) (bool, error) { tr := trace.New("Auth.Exists", user+"@"+domain) defer tr.Finish() if be, ok := a.backends[domain]; ok { ok, err := be.Exists(user) tr.Debugf("Backend: %v %v", ok, err) if ok || err != nil { return ok, err } } if a.Fallback != nil { id := user if domain != "" { id = user + "@" + domain } ok, err := a.Fallback.Exists(id) tr.Debugf("Fallback: %v %v", ok, err) return ok, err } tr.Debugf("Rejected by default") return false, nil } // Reload the registered backends. func (a *Authenticator) Reload() error { msgs := []string{} for domain, be := range a.backends { tr := trace.New("Auth.Reload", domain) err := be.Reload() if err != nil { tr.Error(err) msgs = append(msgs, fmt.Sprintf("%q: %v", domain, err)) } tr.Finish() } if a.Fallback != nil { tr := trace.New("Auth.Reload", "") err := a.Fallback.Reload() if err != nil { tr.Error(err) msgs = append(msgs, fmt.Sprintf(": %v", err)) } tr.Finish() } if len(msgs) > 0 { return errors.New(strings.Join(msgs, " ; ")) } return nil } // DecodeResponse decodes a plain auth response. // // It must be a a base64-encoded string of the form: // NUL NUL // // https://tools.ietf.org/html/rfc4954#section-4.1. // // Either both IDs match, or one of them is empty. // // We split the id into user@domain, since in most cases we expect that to be // the used form, and normalize them. If there is no domain, we just return // "" for it. The rest of the stack will know how to handle it. func DecodeResponse(response string) (user, domain, passwd string, err error) { buf, err := base64.StdEncoding.DecodeString(response) if err != nil { return } bufsp := bytes.SplitN(buf, []byte{0}, 3) if len(bufsp) != 3 { err = fmt.Errorf("response pieces != 3, as per RFC") return } identity := "" passwd = string(bufsp[2]) { // We don't make the distinction between the two IDs, as long as one is // empty, or they're the same. z := string(bufsp[0]) c := string(bufsp[1]) // If neither is empty, then they must be the same. if (z != "" && c != "") && (z != c) { err = fmt.Errorf("auth IDs do not match") return } if z != "" { identity = z } if c != "" { identity = c } } if identity == "" { err = fmt.Errorf("empty identity, must be in the form user@domain") return } // Split identity into "user@domain", if possible. user = identity idsp := strings.SplitN(identity, "@", 2) if len(idsp) >= 2 { user = idsp[0] domain = idsp[1] } // Normalize the user and domain. This is so users can write the username // in their own style and still can log in. For the domain, we use IDNA // and relevant transformations to turn it to utf8 which is what we use // internally. user, err = normalize.User(user) if err != nil { return } domain, err = normalize.Domain(domain) if err != nil { return } return } // WrapNoErrorBackend wraps a NoErrorBackend, converting it into a valid // Backend. This is normally used in Auth.Register calls, to register no-error // backends. func WrapNoErrorBackend(be NoErrorBackend) Backend { return &wrapNoErrorBackend{be} } type wrapNoErrorBackend struct { be NoErrorBackend } func (w *wrapNoErrorBackend) Authenticate(user, password string) (bool, error) { return w.be.Authenticate(user, password), nil } func (w *wrapNoErrorBackend) Exists(user string) (bool, error) { return w.be.Exists(user), nil } func (w *wrapNoErrorBackend) Reload() error { return w.be.Reload() }