310 lines
8.8 KiB
Go
310 lines
8.8 KiB
Go
// 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.
|
|
// The selected handler resolves its account through the tenant-scoped auth store.
|
|
// 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
|
|
}
|