package auth import ( "encoding/json" "fmt" "strings" "git.autistici.org/id/usermetadb" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" ) // Request to authenticate a user. It supports multiple methods for // authentication including challenge-response 2FA. type Request struct { Service string Username string Password []byte OTP string WebAuthnSession *webauthn.SessionData WebAuthnResponse *protocol.ParsedCredentialAssertionData DeviceInfo *usermetadb.DeviceInfo } func (r *Request) EncodeToMap(m map[string]string, prefix string) { m[prefix+"service"] = r.Service m[prefix+"username"] = r.Username m[prefix+"password"] = string(r.Password) if r.OTP != "" { m[prefix+"otp"] = r.OTP } // WebAuthN are encoded as opaque JSON blobs because they're way too // complex to safely serialize using our silly protocol. if r.WebAuthnSession != nil { m[prefix+"webauthn_session"] = encodeJSONBlob(r.WebAuthnSession) } if r.WebAuthnResponse != nil { m[prefix+"webauthn_response"] = encodeJSONBlob(r.WebAuthnResponse) } if r.DeviceInfo != nil { r.DeviceInfo.EncodeToMap(m, prefix+"device.") } } func (r *Request) DecodeFromMap(m map[string]string, prefix string) { r.Service = m[prefix+"service"] r.Username = m[prefix+"username"] r.Password = []byte(m[prefix+"password"]) r.OTP = m[prefix+"otp"] if s := m[prefix+"webauthn_session"]; s != "" { var sess webauthn.SessionData if err := decodeJSONBlob(s, &sess); err == nil { r.WebAuthnSession = &sess } } if s := m[prefix+"webauthn_response"]; s != "" { var resp protocol.ParsedCredentialAssertionData if err := decodeJSONBlob(s, &resp); err == nil { r.WebAuthnResponse = &resp } } r.DeviceInfo = usermetadb.DecodeDeviceInfoFromMap(m, prefix+"device.") } // UserInfo contains optional user information that may be useful to // authentication endpoints. type UserInfo struct { Email string Shard string Groups []string } func encodeStringList(m map[string]string, prefix string, l []string) { for i, elem := range l { m[fmt.Sprintf("%s.%d.", prefix, i)] = elem } } func decodeStringList(m map[string]string, prefix string) (out []string) { i := 0 for { s, ok := m[fmt.Sprintf("%s.%d.", prefix, i)] if !ok { break } out = append(out, s) i++ } return } func (u *UserInfo) EncodeToMap(m map[string]string, prefix string) { if u.Email != "" { m[prefix+"email"] = u.Email } if u.Shard != "" { m[prefix+"shard"] = u.Shard } encodeStringList(m, prefix+"group", u.Groups) } func decodeUserInfoFromMap(m map[string]string, prefix string) *UserInfo { u := UserInfo{ Email: m[prefix+"email"], Shard: m[prefix+"shard"], Groups: decodeStringList(m, prefix+"group"), } if u.Email == "" && u.Shard == "" && len(u.Groups) == 0 { return nil } return &u } // Response to an authentication request. type Response struct { Status Status Mechanism Mechanism AuthenticatorID string TFAMethods []TFAMethod WebAuthnSession *webauthn.SessionData WebAuthnData *protocol.CredentialAssertion UserInfo *UserInfo } // Has2FAMethod checks for the presence of a two-factor authentication // method in the Response. func (r *Response) Has2FAMethod(needle TFAMethod) bool { for _, m := range r.TFAMethods { if m == needle { return true } } return false } func encodeTFAMethodList(m map[string]string, prefix string, l []TFAMethod) { if len(l) == 0 { return } tmp := make([]string, 0, len(l)) for _, el := range l { tmp = append(tmp, string(el)) } encodeStringList(m, prefix, tmp) } func decodeTFAMethodList(m map[string]string, prefix string) []TFAMethod { l := decodeStringList(m, prefix) if len(l) == 0 { return nil } out := make([]TFAMethod, 0, len(l)) for _, el := range l { out = append(out, TFAMethod(el)) } return out } func (r *Response) EncodeToMap(m map[string]string, prefix string) { m[prefix+"status"] = r.Status.String() if r.Mechanism != "" { m[prefix+"mechanism"] = string(r.Mechanism) } if r.AuthenticatorID != "" { m[prefix+"authenticator_id"] = r.AuthenticatorID } encodeTFAMethodList(m, prefix+"2fa_methods", r.TFAMethods) if r.WebAuthnSession != nil { m[prefix+"webauthn_session"] = encodeJSONBlob(r.WebAuthnSession) } if r.WebAuthnData != nil { m[prefix+"webauthn_data"] = encodeJSONBlob(r.WebAuthnData) } if r.UserInfo != nil { r.UserInfo.EncodeToMap(m, prefix+"user.") } } func (r *Response) DecodeFromMap(m map[string]string, prefix string) { r.Status = parseAuthStatus(m[prefix+"status"]) r.TFAMethods = decodeTFAMethodList(m, prefix+"2fa_methods") r.Mechanism = Mechanism(m[prefix+"mechanism"]) r.AuthenticatorID = m[prefix+"authenticator_id"] if s := m[prefix+"webauthn_session"]; s != "" { var sess webauthn.SessionData if err := decodeJSONBlob(s, &sess); err == nil { r.WebAuthnSession = &sess } } if s := m[prefix+"webauthn_data"]; s != "" { var data protocol.CredentialAssertion if err := decodeJSONBlob(s, &data); err == nil { r.WebAuthnData = &data } } r.UserInfo = decodeUserInfoFromMap(m, prefix+"user.") } // Mechanism is an enum for the possible authentication mechanisms. type Mechanism string // Possible methods that were used to achieve successful authentication. const ( MechanismPassword = "password" MechanismASP = "asp" MechanismOTP = "otp" MechanismWebAuthN = "webauthn" ) func (m Mechanism) String() string { return string(m) } // TFAMethod is a hint provided to the caller with the type of 2FA // method that is available for authentication. type TFAMethod string // Known second-factor auth methods. const ( TFAMethodNone = "" TFAMethodOTP = "otp" TFAMethodU2F = "u2f" ) // Status of an authentication request. type Status int // Possible response statuses. const ( StatusOK = iota StatusInsufficientCredentials StatusError ) func (s Status) String() string { switch s { case StatusOK: return "ok" case StatusInsufficientCredentials: return "insufficient_credentials" case StatusError: return "error" default: return fmt.Sprintf("unknown(%d)", int(s)) } } func parseAuthStatus(s string) Status { switch s { case "ok": return StatusOK case "insufficient_credentials": return StatusInsufficientCredentials default: return StatusError } } func (s Status) Error() string { return s.String() } // Miscellaneous serializers for external objects. func encodeJSONBlob(obj interface{}) string { data, err := json.Marshal(obj) if err != nil { panic(err) } return string(data) } func decodeJSONBlob(s string, obj interface{}) error { return json.NewDecoder(strings.NewReader(s)).Decode(obj) }