// Package rest provides authentication by calling a separate process over REST API (technically JSON RPC, not REST). package rest import ( "bytes" "encoding/json" "errors" "io" "net/http" "net/url" "regexp" "strings" "time" "github.com/tinode/chat/server/auth" "github.com/tinode/chat/server/logs" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) // authenticator is the type to map authentication methods to. type authenticator struct { // Logical name of this authenticator name string // URL of the server serverUrl string // Authenticator may add new accounts to local database. allowNewAccounts bool // Use separate endpoints, i.e. add request name to serverUrl path when making requests. useSeparateEndpoints bool // Cache of restricted tag prefixes (namespaces). rTagNS []string // Optional regex pattern for checking tokens. reToken *regexp.Regexp } // Request to the server. type request struct { Endpoint string `json:"endpoint"` Name string `json:"name"` TenantID types.TenantID `json:"tenant_id,omitempty"` Record *auth.Rec `json:"rec,omitempty"` Secret []byte `json:"secret,omitempty"` RemoteAddr string `json:"addr,omitempty"` } // User initialization data when creating a new user. type newAccount struct { // Default access mode Auth string `json:"auth,omitempty"` Anon string `json:"anon,omitempty"` // User's Public data Public any `json:"public,omitempty"` // User's Trusted data Trusted any `json:"trusted,omitempty"` // Per-subscription private data Private any `json:"private,omitempty"` } // Response from the server. type response struct { // Error message in case of an error. Err string `json:"err,omitempty"` // Optional auth record Record *auth.Rec `json:"rec,omitempty"` // Optional byte slice ByteVal []byte `json:"byteval,omitempty"` // Optional time value TimeVal time.Time `json:"ts,omitempty"` // Boolean value BoolVal bool `json:"boolval,omitempty"` // String slice value StrSliceVal []string `json:"strarr,omitempty"` // Account creation data NewAcc *newAccount `json:"newacc,omitempty"` } // Init initializes the handler. func (a *authenticator) Init(jsonconf json.RawMessage, name string) error { if name == "" { return errors.New("auth_rest: authenticator name cannot be blank") } if a.name != "" { return errors.New("auth_rest: already initialized as " + a.name + "; " + name) } type configType struct { // ServerUrl is the URL of the server to call. ServerUrl string `json:"server_url"` // Server may create new accounts. AllowNewAccounts bool `json:"allow_new_accounts"` // Use separate endpoints, i.e. add request name to serverUrl path when making requests. UseSeparateEndpoints bool `json:"use_separate_endpoints"` } var config configType err := json.Unmarshal(jsonconf, &config) if err != nil { return errors.New("auth_rest: failed to parse config: " + err.Error() + "(" + string(jsonconf) + ")") } serverUrl, err := url.Parse(config.ServerUrl) if err != nil || !serverUrl.IsAbs() { return errors.New("auth_rest: invalid server_url '" + string(jsonconf) + "'") } if !strings.HasSuffix(serverUrl.Path, "/") { serverUrl.Path += "/" } a.name = name a.serverUrl = serverUrl.String() a.allowNewAccounts = config.AllowNewAccounts a.useSeparateEndpoints = config.UseSeparateEndpoints return nil } // IsInitialized returns true if the handler is initialized. func (a *authenticator) IsInitialized() bool { return a.name != "" } // Execute HTTP POST to the server at the specified endpoint and with the provided payload. func (a *authenticator) callEndpoint(endpoint string, ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*response, error) { // Convert payload to json. req := &request{Endpoint: endpoint, Name: a.name, TenantID: ctx.TenantID, Record: rec, Secret: secret, RemoteAddr: ctx.RemoteAddr} content, err := json.Marshal(req) if err != nil { return nil, err } urlToCall := a.serverUrl if a.useSeparateEndpoints { epUrl, _ := url.Parse(a.serverUrl) epUrl.Path += endpoint urlToCall = epUrl.String() } // Send payload to server using default HTTP client. post, err := http.Post(urlToCall, "application/json", bytes.NewBuffer(content)) if err != nil { return nil, err } defer post.Body.Close() // Check HTTP status response. Must be 2xx. if post.StatusCode < http.StatusOK || post.StatusCode >= http.StatusMultipleChoices { return nil, errors.New("unexpected HTTP response " + post.Status) } // Read response. body, err := io.ReadAll(post.Body) if err != nil { return nil, err } // Parse response. var resp response err = json.Unmarshal(body, &resp) if err != nil { return nil, err } if resp.Err != "" { return nil, types.StoreError(resp.Err) } return &resp, nil } // AddRecord adds persistent authentication record to the database. // Returns: updated auth record, error func (a *authenticator) AddRecord(ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*auth.Rec, error) { if err := auth.BindRecord(ctx, rec); err != nil { return nil, err } resp, err := a.callEndpoint("add", ctx, rec, secret) if err != nil { return nil, err } if resp.Record == nil || resp.Record.TenantID != ctx.TenantID { return nil, types.ErrFailed } if err = auth.BindRecord(ctx, resp.Record); err != nil { return nil, err } return resp.Record, nil } // UpdateRecord updates existing record with new credentials. func (a *authenticator) UpdateRecord(ctx auth.AuthContext, rec *auth.Rec, secret []byte) (*auth.Rec, error) { if err := auth.BindRecord(ctx, rec); err != nil { return nil, err } _, err := a.callEndpoint("upd", ctx, rec, secret) return rec, err } // Authenticate: get user record by provided secret func (a *authenticator) Authenticate(ctx auth.AuthContext, secret []byte) (*auth.Rec, []byte, error) { if !ctx.IsValid() { return nil, nil, types.ErrMalformed } resp, err := a.callEndpoint("auth", ctx, nil, secret) if err != nil { return nil, nil, err } // Auth record not found. if resp.Record == nil { logs.Warn.Println("rest_auth: invalid response: missing Record") return nil, nil, types.ErrInternal } if resp.Record.TenantID != ctx.TenantID { return nil, nil, types.ErrFailed } if err = auth.BindRecord(ctx, resp.Record); err != nil { return nil, nil, err } // Check if server provided a user ID. If not, create a new account in the local database. if resp.Record.Uid.IsZero() && a.allowNewAccounts { if resp.NewAcc == nil { return nil, nil, types.ErrNotFound } // Create account, get UID, report UID back to the server. user := types.User{ TenantID: ctx.TenantID, State: resp.Record.State, Public: resp.NewAcc.Public, Trusted: resp.NewAcc.Trusted, Tags: resp.Record.Tags, } user.Access.Auth.UnmarshalText([]byte(resp.NewAcc.Auth)) user.Access.Anon.UnmarshalText([]byte(resp.NewAcc.Anon)) _, err = store.Users.Create(ctx.TenantID, &user, resp.NewAcc.Private) if err != nil { return nil, nil, err } // Report the new UID to the server. resp.Record.Uid = user.Uid() _, err = a.callEndpoint("link", ctx, resp.Record, secret) if err != nil { store.Users.Delete(ctx.TenantID, resp.Record.Uid, true) return nil, nil, err } } return resp.Record, resp.ByteVal, nil } // AsTag converts search token into prefixed tag or an empty string if it // cannot be represented as a prefixed tag. func (a *authenticator) AsTag(token string) string { if len(a.rTagNS) > 0 { if a.reToken != nil && !a.reToken.MatchString(token) { return "" } // No validation or passed validation. return a.rTagNS[0] + ":" + token } return "" } // IsUnique verifies if the provided secret can be considered unique by the auth // scheme as well as policy compliance. E.g. if login is unique and not too short/long. func (a *authenticator) IsUnique(ctx auth.AuthContext, secret []byte) (bool, error) { if !ctx.IsValid() { return false, types.ErrMalformed } resp, err := a.callEndpoint("checkunique", ctx, nil, secret) if err != nil { return false, err } return resp.BoolVal, err } // GenSecret generates a new secret, if appropriate. func (a *authenticator) GenSecret(rec *auth.Rec) ([]byte, time.Time, error) { if rec == nil || rec.TenantID.IsZero() { return nil, time.Time{}, types.ErrMalformed } resp, err := a.callEndpoint("gen", auth.AuthContext{TenantID: rec.TenantID}, rec, nil) if err != nil { return nil, time.Time{}, err } return resp.ByteVal, resp.TimeVal, err } // DelRecords deletes all authentication records for the given user. func (a *authenticator) DelRecords(tenantID types.TenantID, uid types.Uid) error { logs.Info.Println("DelRecords, initialized=", a.name != "") ctx := auth.AuthContext{TenantID: tenantID} _, err := a.callEndpoint("del", ctx, &auth.Rec{TenantID: tenantID, Uid: uid}, nil) return err } // RestrictedTags returns tag namespaces (prefixes, such as prefix:login) restricted by the server. func (a *authenticator) RestrictedTags() ([]string, error) { if a.rTagNS != nil { // Using cached prefixes. ns := make([]string, len(a.rTagNS)) // Returning a copy to prevent accidental modification of server-provided tags. copy(ns, a.rTagNS) return ns, nil } // First time use, fetch prefixes from the server. resp, err := a.callEndpoint("rtagns", auth.AuthContext{}, nil, nil) if err != nil { return nil, err } // Save valid result to cache. a.rTagNS = resp.StrSliceVal if len(resp.ByteVal) > 0 { a.reToken, err = regexp.Compile(string(resp.ByteVal)) if err != nil { logs.Warn.Println("rest_auth: invalid token regexp", string(resp.ByteVal)) } } return resp.StrSliceVal, nil } // GetResetParams returns authenticator parameters passed to password reset handler // (none for rest). func (authenticator) GetResetParams(tenantID types.TenantID, uid types.Uid) (map[string]any, error) { // TODO: route request to the server. return nil, nil } const realName = "rest" // GetRealName returns the hardcoded name of the authenticator. func (authenticator) GetRealName() string { return realName } func init() { store.RegisterAuthScheme(realName, &authenticator{}) }