Files
im_server/server/auth/auth.go
T

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.
// 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
}