Files
im_server/server/auth/token/auth_token.go
T

246 lines
7.0 KiB
Go

// Package token implements authentication by HMAC-signed security token.
package token
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/json"
"errors"
"time"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
// authenticator is a singleton instance of the authenticator.
type authenticator struct {
name string
hmacSalt []byte
lifetime time.Duration
serialNumber int
}
const tokenVersion = uint8(3)
// tokenLayout defines positioning of various bytes in token v3.
// [1:version][8:tenant][8:UID][4:expires][2:authLevel][2:serial][2:features][4:authVersion][32:signature] = 63 bytes.
type tokenLayout struct {
// Token format version.
Version uint8
// Tenant ID.
TenantID uint64
// User ID.
Uid uint64
// Token expiration time.
Expires uint32
// User's authentication level.
AuthLevel uint16
// Serial number - to invalidate all tokens if needed.
SerialNumber uint16
// Bitmap with feature bits.
Features uint16
// User-scoped auth version.
AuthVersion uint32
}
// Init initializes the authenticator: parses the config and sets salt, serial number and lifetime.
func (ta *authenticator) Init(jsonconf json.RawMessage, name string) error {
if name == "" {
return errors.New("auth_token: authenticator name cannot be blank")
}
if ta.name != "" {
return errors.New("auth_token: already initialized as " + ta.name + "; " + name)
}
type configType struct {
// Key for signing tokens
Key []byte `json:"key"`
// Datatabase or other serial number, to invalidate all issued tokens at once.
SerialNum int `json:"serial_num"`
// Token expiration time
ExpireIn int `json:"expire_in"`
}
var config configType
if err := json.Unmarshal(jsonconf, &config); err != nil {
return errors.New("auth_token: failed to parse config: " + err.Error() + "(" + string(jsonconf) + ")")
}
if len(config.Key) < sha256.Size {
return errors.New("auth_token: the key is missing or too short")
}
if config.ExpireIn <= 0 {
return errors.New("auth_token: invalid expiration value")
}
ta.name = name
ta.hmacSalt = config.Key
ta.lifetime = time.Duration(config.ExpireIn) * time.Second
ta.serialNumber = config.SerialNum
return nil
}
// IsInitialized returns true if the handler is initialized.
func (ta *authenticator) IsInitialized() bool {
return ta.name != ""
}
// AddRecord is not supported, will produce an error.
func (authenticator) AddRecord(ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*auth.Rec, error) {
return nil, types.ErrUnsupported
}
// UpdateRecord is not supported, will produce an error.
func (authenticator) UpdateRecord(ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*auth.Rec, error) {
return nil, types.ErrUnsupported
}
// Authenticate checks validity of provided token.
func (ta *authenticator) Authenticate(ctx auth.AuthContext, token []byte) (*auth.Rec, []byte, error) {
var tl tokenLayout
dataSize := binary.Size(&tl)
if len(token) != dataSize+sha256.Size {
return nil, nil, types.ErrMalformed
}
buf := bytes.NewBuffer(token)
err := binary.Read(buf, binary.LittleEndian, &tl)
if err != nil {
return nil, nil, types.ErrMalformed
}
if tl.Version != tokenVersion || tl.TenantID == 0 {
return nil, nil, types.ErrFailed
}
if ctx.IsValid() && types.TenantID(tl.TenantID) != ctx.TenantID {
return nil, nil, types.ErrFailed
}
hbuf := new(bytes.Buffer)
binary.Write(hbuf, binary.LittleEndian, &tl)
// Check signature.
hasher := hmac.New(sha256.New, ta.hmacSalt)
hasher.Write(hbuf.Bytes())
if !hmac.Equal(token[dataSize:dataSize+sha256.Size], hasher.Sum(nil)) {
return nil, nil, types.ErrFailed
}
// Check authentication level for validity.
if auth.Level(tl.AuthLevel) > auth.LevelRoot {
return nil, nil, types.ErrMalformed
}
// Check serial number.
if int(tl.SerialNumber) != ta.serialNumber {
return nil, nil, types.ErrFailed
}
// Check token expiration time.
expires := time.Unix(int64(tl.Expires), 0).UTC()
if expires.Before(time.Now().Add(1 * time.Second)) {
return nil, nil, types.ErrExpired
}
features := auth.Feature(tl.Features)
if features&auth.FeatureNoLogin == 0 {
if revoked, enabled, err := store.IsLoginTokenRevoked(types.TenantID(tl.TenantID), token); err != nil {
return nil, nil, err
} else if enabled && revoked {
return nil, nil, types.ErrFailed
}
if store.Security == nil {
return nil, nil, types.ErrInternal
}
currentVersion, err := store.Security.GetAuthVersion(types.TenantID(tl.TenantID), types.Uid(tl.Uid))
if err != nil {
return nil, nil, err
}
if int(tl.AuthVersion) != currentVersion {
return nil, nil, types.ErrFailed
}
}
return &auth.Rec{
TenantID: types.TenantID(tl.TenantID),
Uid: types.Uid(tl.Uid),
AuthLevel: auth.Level(tl.AuthLevel),
Lifetime: auth.Duration(time.Until(expires)),
Features: features,
State: types.StateUndefined,
AuthVersion: int(tl.AuthVersion),
}, nil, nil
}
// GenSecret generates a new token.
func (ta *authenticator) GenSecret(rec *auth.Rec) ([]byte, time.Time, error) {
if rec == nil || rec.TenantID.IsZero() || rec.Uid.IsZero() {
return nil, time.Time{}, types.ErrMalformed
}
if rec.Lifetime == 0 {
rec.Lifetime = auth.Duration(ta.lifetime)
} else if rec.Lifetime < 0 {
return nil, time.Time{}, types.ErrExpired
}
expires := time.Now().Add(time.Duration(rec.Lifetime)).UTC().Round(time.Millisecond)
tl := tokenLayout{
Version: tokenVersion,
TenantID: uint64(rec.TenantID),
Uid: uint64(rec.Uid),
Expires: uint32(expires.Unix()),
AuthLevel: uint16(rec.AuthLevel),
SerialNumber: uint16(ta.serialNumber),
Features: uint16(rec.Features),
AuthVersion: uint32(rec.AuthVersion),
}
buf := new(bytes.Buffer)
binary.Write(buf, binary.LittleEndian, &tl)
hasher := hmac.New(sha256.New, ta.hmacSalt)
hasher.Write(buf.Bytes())
binary.Write(buf, binary.LittleEndian, hasher.Sum(nil))
return buf.Bytes(), expires, nil
}
// AsTag is not supported, will produce an empty string.
func (authenticator) AsTag(token string) string {
return ""
}
// IsUnique is not supported, will produce an error.
func (authenticator) IsUnique(ctx auth.AuthContext, token []byte) (bool, error) {
return false, types.ErrUnsupported
}
// DelRecords adds disabled user ID to a stop list.
func (authenticator) DelRecords(tenantID types.TenantID, uid types.Uid) error {
return nil
}
// RestrictedTags returns tag namespaces restricted by this authenticator (none for token).
func (authenticator) RestrictedTags() ([]string, error) {
return nil, nil
}
// GetResetParams returns authenticator parameters passed to password reset handler
// (none for token).
func (authenticator) GetResetParams(tenantID types.TenantID, uid types.Uid) (map[string]any, error) {
return nil, nil
}
const realName = "token"
// GetRealName returns the hardcoded name of the authenticator.
func (authenticator) GetRealName() string {
return realName
}
func init() {
store.RegisterAuthScheme(realName, &authenticator{})
}