Skip to content

Users and Authentication

maxlandon edited this page Jul 18, 2026 · 1 revision

Users & Authentication · [User+Dev]

This is the heart of the library: a certificate-based user registry with per-user tokens and mutual TLS. Read Introduction & Concepts → Authentication only first — the teamserver proves who is calling and nothing more; authorization is your application's job.


The identity model

A user is a registered identity for which the teamserver holds two pieces of cryptographic material:

  1. A 128-bit API token (stored hashed with SHA-256 in the database) — presented by the client on every request to answer "who is calling?".
  2. A client certificate signed by the teamserver's users CA — used for the mutual-TLS handshake so only known users can even open a connection.

The exported identity type carries no permissions:

type User struct {
    Name     string
    Online   bool
    LastSeen time.Time
    Clients  int
}

The teamserver stores only the identity and its credentials. Applications that need per-user authorization own that model themselves (typically a separate table keyed by Name) and enforce it in their own middleware.


User lifecycle (CLI)

All commands below are shown on the standalone teamserver binary; in a real tool they are myapp teamserver user, etc. See CLI Reference.

# Create a user and generate its client configuration file (saved to the CWD by default).
teamserver user --name Michael --host localhost
teamserver user --name Bob --host 172.10.0.10 --port 32333 --save ~/configs/

# Create a config for the current OS user, saved straight into the client configs dir.
teamserver user --system

# List users and their online status (via the nested client tree).
teamserver client users

# Remove a user: deletes its cert + token and immediately revokes all its live sessions.
teamserver delete Michael

teamserver user prints Generating new client certificate, please wait ... (ECC key generation) and then writes a <name>_<host>.teamclient.cfg file. Hand that file to the operator; they import it with teamclient import (see below).


User lifecycle (Go API)

The same operations are available programmatically on *server.Server:

// Create a user. Returns a ready-to-serialize *client.Config with token + TLS material.
// name must be alphanumeric (plus _ and -); host is required; port defaults to the
// server's configured daemon port when 0.
cfg, err := teamserver.UserCreate("Michael", "localhost", 0)
if err != nil {
    // ErrUserConfig (bad name/host), ErrCertificate, or ErrDatabase
}
data, _ := json.Marshal(cfg) // write to <name>.teamclient.cfg and hand to the operator

// Delete a user. Two guaranteed effects:
//  1. UsersTLSConfig() will refuse connections using the deleted user's TLS credentials.
//  2. Authenticate(token) returns ErrUnauthenticated for that user forever after.
err = teamserver.UserDelete("Michael")

// List users (name + last-seen + online, computed from the live token cache).
users, err := teamserver.Users()

Deleting a user also clears the in-memory token cache, so connected clients of that user are refused on their next request, not just at reconnect time.


The connection config file

UserCreate (and the user command) produce a client.Config — the JSON file an operator imports to reach the server:

type Config struct {
    User          string `json:"user"`  // informational; the cert CN is authoritative
    Host          string `json:"host"`
    Port          int    `json:"port"`
    Token         string `json:"token"`          // the raw 128-bit API token
    CACertificate string `json:"ca_certificate"` // users CA (to verify the server)
    PrivateKey    string `json:"private_key"`     // this user's client key
    Certificate   string `json:"certificate"`     // this user's client cert
}

Operators manage these files with the teamclient tree:

# Import a config handed out by an admin (copied into ~/.app/teamclient/configs/).
teamclient import ~/Michael_localhost.teamclient.cfg
teamclient import --default ~/Michael_localhost.teamclient.cfg  # also mark as default

# Then just use the server; the client connects automatically.
teamclient users
teamclient version

If multiple configs exist and none is selected, client.Connect() prompts the operator to pick one. To avoid the prompt entirely (e.g. for automation), construct the client with a config directly:

teamclient, _ := client.New("myapp",
    client.WithDialer(dialer),
    client.WithConfig(cfg), // no disk lookup, no prompt
)

Config helpers on *client.Client: GetConfigs(), ReadConfig(path), SaveConfig(cfg), SelectConfig(), Config().


Sharing users between teamservers (import/export)

Users live in a users Certificate Authority. You can export the whole CA (all users) from one teamserver and import it into another — handy when several teamservers should trust the same operator set.

# Export the users CA (cert + private key) to a file.
teamserver export ~/myapp-users.teamserver.ca

# Import another teamserver's users CA.
teamserver import ~/.other_app/teamserver/certs/other_app_user-ca-cert.teamserver.pem

Go API:

certPEM, keyPEM, err := teamserver.UsersGetCA() // export
teamserver.UsersSaveCA(certPEM, keyPEM)         // import

The exported file is JSON of the form {"certificate": "...", "private_key": "..."}.


The authentication primitive

This one call is the entire authentication surface. A transport middleware calls it with the raw token it extracted from the request:

// server.Authenticate hashes the token, checks it against the users database
// (and an in-memory cache), updates LastSeen, and returns the identity.
user, err := teamserver.Authenticate(rawToken)
if err != nil {
    // ErrUnauthenticated (unknown/deleted user) or ErrDatabase
    return status.Error(codes.Unauthenticated, "authentication failure")
}
// user.Name is now trustworthy. What they may DO is entirely up to you.

There is deliberately no Authorize, no roles, no permissions. Authenticate is "the single seam through which an embedding application learns who is calling."


Mutual TLS configuration

The teamserver builds a complete, ready-to-run *tls.Config for the listener side. Clients are not allowed to choose any TLS parameters.

// Server side — use this at the net.Listener / net.Conn level in your Handler.
tlsConfig, err := teamserver.UsersTLSConfig()
// RequireAndVerifyClientCert, users CA as ClientCAs/RootCAs, TLS 1.3 minimum,
// server cert auto-generated on first use.

On the client side, the dialer builds a matching config from the three credential fields in its client.Config:

// Client side — from a config's CA cert, client cert and key.
tlsConfig, err := teamclient.NewTLSConfigFrom(cfg.CACertificate, cfg.Certificate, cfg.PrivateKey)

In-memory (self-client) connections skip TLS and token auth entirely — there is no network and no untrusted peer. The example gRPC backend detects this (empty private key ⇒ in-memory) and wires an unauthenticated interceptor instead.


How it all fits in a transport

Putting the pieces together, a server-side transport middleware typically:

  1. Wraps the listener with UsersTLSConfig() for remote binds (mutual TLS gate).
  2. Extracts the bearer token from each request.
  3. Calls Authenticate(token) to get the team.User.
  4. Resolves user.Name against the application's own authorization model and injects the application's own identity object onto the request context.

The gRPC example's tokenAuthFunc is the canonical reference:

func (ts *Teamserver) tokenAuthFunc(ctx context.Context) (context.Context, error) {
    rawToken, err := grpc_auth.AuthFromMD(ctx, "Bearer")
    if err != nil {
        return nil, status.Error(codes.Unauthenticated, "Authentication failure")
    }

    // Core: verify the token, learn WHO is calling (no permissions).
    user, err := ts.Authenticate(rawToken)
    if err != nil || user.Name == "" {
        return nil, status.Error(codes.Unauthenticated, "Authentication failure")
    }

    // Application: resolve user.Name against your own model, inject YOUR identity type
    // under YOUR context key. This example just forwards the team.User.
    return context.WithValue(ctx, userKey, user), nil
}

See Writing a Transport for the full middleware wiring.


Errors you may encounter

Error Meaning
server.ErrUnauthenticated Unknown or deleted user token.
server.ErrUserConfig Invalid user name (non-alphanumeric) or empty name/host.
server.ErrCertificate Certificate infrastructure failure (critical).
client.ErrConfigNoUser A config with an empty User — not allowed even in memory.
client.ErrNoConfig No config found on disk or selected.

Next: CLI Reference → · Writing a Transport →

Clone this wiki locally