// Package auth provides interfaces and types required for implementing an authenticaor. package auth import ( "encoding/json" "errors" "strconv" "time" "github.com/tinode/chat/server/store/types" ) // Level is the type for authentication levels. type Level int // Authentication levels const ( // LevelNone is undefined/not authenticated LevelNone Level = iota * 10 // LevelAnon is anonymous user/light authentication LevelAnon // LevelAuth is fully authenticated user LevelAuth // LevelRoot is a superuser (currently unused) LevelRoot ) // String implements Stringer interface: gets human-readable name for a numeric authentication level. func (a Level) String() string { s, err := a.MarshalText() if err != nil { return "unkn" } return string(s) } // ParseAuthLevel parses authentication level from a string. func ParseAuthLevel(name string) Level { switch name { case "anon", "ANON": return LevelAnon case "auth", "AUTH": return LevelAuth case "root", "ROOT": return LevelRoot default: return LevelNone } } // MarshalText converts Level to a slice of bytes with the name of the level. func (a Level) MarshalText() ([]byte, error) { switch a { case LevelNone: return []byte(""), nil case LevelAnon: return []byte("anon"), nil case LevelAuth: return []byte("auth"), nil case LevelRoot: return []byte("root"), nil default: return nil, errors.New("auth.Level: invalid level value") } } // UnmarshalText parses authentication level from a string. func (a *Level) UnmarshalText(b []byte) error { switch string(b) { case "": *a = LevelNone return nil case "anon", "ANON": *a = LevelAnon return nil case "auth", "AUTH": *a = LevelAuth return nil case "root", "ROOT": *a = LevelRoot return nil default: return errors.New("auth.Level: unrecognized") } } // MarshalJSON converts Level to a quoted string. func (a Level) MarshalJSON() ([]byte, error) { res, err := a.MarshalText() if err != nil { return nil, err } return append(append([]byte{'"'}, res...), '"'), nil } // UnmarshalJSON reads Level from a quoted string. func (a *Level) UnmarshalJSON(b []byte) error { if b[0] != '"' || b[len(b)-1] != '"' { return errors.New("syntax error") } return a.UnmarshalText(b[1 : len(b)-1]) } // Feature is a bitmap of authenticated features, such as validated/not validated. type Feature uint16 const ( // FeatureValidated bit is set if user's credentials are already validated (V). FeatureValidated Feature = 1 << iota // FeatureNoLogin is set if the token should not be used to permanently authenticate a session (L). FeatureNoLogin ) // MarshalText converts Feature to ASCII byte slice. func (f Feature) MarshalText() ([]byte, error) { res := []byte{} for i, chr := range []byte{'V', 'L'} { if (f & (1 << uint(i))) != 0 { res = append(res, chr) } } return res, nil } // UnmarshalText parses Feature string as byte slice. func (f *Feature) UnmarshalText(b []byte) error { var f0 int var err error if len(b) > 0 { if b[0] >= '0' && b[0] <= '9' { f0, err = strconv.Atoi(string(b)) } else { Loop: for i := range b { switch b[i] { case 'V', 'v': f0 |= int(FeatureValidated) case 'L', 'l': f0 |= int(FeatureNoLogin) default: err = errors.New("Feature: invalid character '" + string(b[i]) + "'") break Loop } } } } *f = Feature(f0) return err } // String Featureto a string representation. func (f Feature) String() string { res, err := f.MarshalText() if err != nil { return "" } return string(res) } // MarshalJSON converts Feature to a quoted string. func (f Feature) MarshalJSON() ([]byte, error) { res, err := f.MarshalText() if err != nil { return nil, err } return append(append([]byte{'"'}, res...), '"'), nil } // UnmarshalJSON reads Feature from a quoted string or an integer. func (f *Feature) UnmarshalJSON(b []byte) error { if b[0] == '"' && b[len(b)-1] == '"' { return f.UnmarshalText(b[1 : len(b)-1]) } return f.UnmarshalText(b) } // Duration is identical to time.Duration except it can be sanely unmarshallend from JSON. type Duration time.Duration // AuthContext is server-owned context for an authentication operation. // TenantID must be resolved before invoking an authenticator. type AuthContext struct { TenantID types.TenantID RemoteAddr string } // IsValid reports whether the authentication context is bound to a tenant. func (ctx AuthContext) IsValid() bool { return !ctx.TenantID.IsZero() } // BindRecord validates the authentication context and binds a record to it. // A record which is already bound to another tenant is rejected. func BindRecord(ctx AuthContext, rec *Rec) error { if !ctx.IsValid() || rec == nil { return types.ErrMalformed } if !rec.TenantID.IsZero() && rec.TenantID != ctx.TenantID { return types.ErrFailed } rec.TenantID = ctx.TenantID return nil } // UnmarshalJSON handles the cases where duration is specified in JSON as a "5000s" string or just plain seconds. func (d *Duration) UnmarshalJSON(b []byte) error { var v any if err := json.Unmarshal(b, &v); err != nil { return err } switch value := v.(type) { case float64: *d = Duration(time.Duration(value) * time.Second) return nil case string: d0, err := time.ParseDuration(value) if err != nil { return err } *d = Duration(d0) return nil default: return errors.New("invalid duration") } } // Rec is an authentication record. type Rec struct { // Tenant which owns this authentication record. TenantID types.TenantID `json:"tenant_id,omitempty"` // User ID. Uid types.Uid `json:"uid,omitempty"` // Authentication level. AuthLevel Level `json:"authlvl,omitempty"` // Lifetime of this record. Lifetime Duration `json:"lifetime,omitempty"` // Bitmap of features. Currently 'validated'/'not validated' only. Features Feature `json:"features,omitempty"` // Tags generated by this authentication record. Tags []string `json:"tags,omitempty"` // User account state received or read by the authenticator. State types.ObjState // Credential 'method:value' associated with this record. Credential string `json:"cred,omitempty"` // AuthVersion invalidates previously issued login tokens when incremented. AuthVersion int `json:"auth_version,omitempty"` // Authenticator may request the server to create a new account. // These are the account parameters which can be used for creating the account. DefAcs *types.DefaultAccess `json:"defacs,omitempty"` Public any `json:"public,omitempty"` Private any `json:"private,omitempty"` } // AuthHandler is the interface which auth providers must implement. type AuthHandler interface { // Init initializes the handler taking config string and logical name as parameters. Init(jsonconf json.RawMessage, name string) error // IsInitialized returns true if the handler is initialized. IsInitialized() bool // AddRecord adds persistent authentication record to the database. // Returns: updated auth record, error AddRecord(ctx AuthContext, rec *Rec, secret []byte) (*Rec, error) // UpdateRecord updates existing record with new credentials. // Returns updated auth record, error. UpdateRecord(ctx AuthContext, rec *Rec, secret []byte) (*Rec, error) // Authenticate: given a user-provided authentication secret (such as "login:password"), either // return user's record (ID, time when the secret expires, etc), or issue a challenge to // continue the authentication process to the next step, or return an error code. // The remoteAddr (i.e. the IP address of the client) can be used by custom authenticators for // additional validation. The stock authenticators don't use it. // store.Users.GetAuthRecord("scheme", "unique") // Returns: user auth record, challenge, error. Authenticate(ctx AuthContext, secret []byte) (*Rec, []byte, error) // AsTag converts search token into prefixed tag or an empty string if it // cannot be represented as a prefixed tag. AsTag(token string) string // IsUnique verifies if the provided secret can be considered unique by the auth scheme // E.g. if login is unique. It also may check for policy compliance, i.e. not too short, etc. IsUnique(ctx AuthContext, secret []byte) (bool, error) // GenSecret generates a new secret, if appropriate. GenSecret(rec *Rec) ([]byte, time.Time, error) // DelRecords deletes (or disables) all authentication records for the given user. DelRecords(tenantID types.TenantID, uid types.Uid) error // RestrictedTags returns the tag namespaces (prefixes) which are restricted by this authenticator. RestrictedTags() ([]string, error) // GetResetParams returns authenticator parameters passed to password reset handler // for the provided user id. // Returns: map of params. GetResetParams(tenantID types.TenantID, uid types.Uid) (map[string]any, error) // GetRealName returns the hardcoded name of the authenticator. GetRealName() string }