-
Notifications
You must be signed in to change notification settings - Fork 2
Users and Authentication
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
- User lifecycle (CLI)
- User lifecycle (Go API)
- The connection config file
- Sharing users between teamservers (import/export)
- The authentication primitive
- Mutual TLS configuration
- How it all fits in a transport
A user is a registered identity for which the teamserver holds two pieces of cryptographic material:
- 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?".
- 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.
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 Michaelteamserver 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).
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.
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 versionIf 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().
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.pemGo API:
certPEM, keyPEM, err := teamserver.UsersGetCA() // export
teamserver.UsersSaveCA(certPEM, keyPEM) // importThe exported file is JSON of the form {"certificate": "...", "private_key": "..."}.
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."
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.
Putting the pieces together, a server-side transport middleware typically:
- Wraps the listener with
UsersTLSConfig()for remote binds (mutual TLS gate). - Extracts the bearer token from each request.
- Calls
Authenticate(token)to get theteam.User. - Resolves
user.Nameagainst 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.
| 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 →