Tikti is a multi-tenant identity service written in Go. It stores all state in Redis, issues HS256 idTokens for primary authentication, and exchanges those idTokens for RS256 access tokens consumed by downstream services. Tikti authenticates users through two paths: a credential-based path (/signIn, /signInWithPassword) and a SAML 2.0 federation path where Tikti acts as Service Provider and delegates authentication to an external Identity Provider. Both paths produce the same HS256 idToken with identical claims, so downstream token exchange and JWKS verification work without modification regardless of how the user authenticated.
Tikti ships with four binaries (server, CLI, migration tool, installation bootstrap), a Helm chart for Kubernetes deployment, and a full technical specification under docs/.
The server reads its runtime settings from a YAML file. The default path is config/tikti.yaml. The file contains the HTTP listen port, Redis address, the HS256 signing secret, an API key for protected admin routes, the issuer base URL embedded in token claims, the default audience, and the RS256 private key used to sign access tokens. A minimal configuration looks like this:
port: 8080
redisAddr: localhost:6379
jwtSecret: supersecret
apiKey: my_api_key
issuerBaseUrl: http://localhost:8080
defaultAudience: tikti
jwksPrivateKey: |
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
jwksKeyId: tikti-local-1When SAML federation is enabled, the configuration file includes an additional saml block that controls the SP entity, assertion consumer service URL, signing and encryption key paths, and validation parameters:
saml:
enabled: true
sp:
entityID: "${issuerBaseUrl}/saml"
acsURL: "${issuerBaseUrl}/saml/acs"
sloURL: "${issuerBaseUrl}/saml/slo"
signingKeyPath: "/etc/tikti/saml/sp.key"
signingCertPath: "/etc/tikti/saml/sp.crt"
encryptionKeyPath: "/etc/tikti/saml/sp.key"
encryptionCertPath: "/etc/tikti/saml/sp.crt"
keyBits: 2048
clockSkewSeconds: 120
requestTTLSeconds: 300
allowedSigAlgs: ["rsa-sha256"]
allowedDigestAlgs: ["sha256"]
canonicalization: "xml-exc-c14n"
requireAssertionSigned: true
requireEncryptedAssertion: false
acs:
postLoginURL: "/dashboard"
deliveryMode: "cookie"
cookieName: "tikti_idt"
cookieSameSite: "Lax"
cookieSecure: true
cookieHTTPOnly: true
idp:
refreshIntervalHours: 24
backgroundRefresh: true
discover:
enabled: true
emailDomainIndexKey: "saml:discover:domain"
metrics:
namespace: "tikti"
subsystem: "saml"Per-tenant IdP records are not stored in YAML. They live in Redis and are managed through the CLI, keeping the configuration file static at deploy time and the trust table mutable at runtime.
To start the server, pass the config file path:
go run ./cmd/tikti -f config/tikti.yamlThe server binary (tikti) runs the HTTP API. The CLI binary (tikti-cli) provides admin commands for tenant management, token operations, and SAML federation. The migration binary (tikti-migrate) handles schema transitions for the Redis keyspace. The installation-only tikti-bootstrap binary reconciles bounded initial identity state from a controlled deployment job.
go build -o tikti ./cmd/tikti
go build -o tikti-cli ./cmd/tikti-cli
go build -o tikti-migrate ./cmd/tikti-migrate
go build -o tikti-bootstrap ./cmd/tikti-bootstrapAn installation that already has an active password ADMIN in a workload
tenant can recover the reserved local-tenant MASTER without receiving or
replacing that administrator's credential. Set
TIKTI_BOOTSTRAP_EXISTING_USER_ONLY=true, use the exact MASTER identity
(local-tenant, Code Foundry) and the code-admin-api audience, and omit all
password and password-hash inputs. This mode preserves the user record and
home tenant; it creates or verifies the MASTER tenant, an exact tenant-safe
ADMIN role, the managed Code Admin audience, and one direct assignment. Any
missing administrator or conflicting object stops the job before authority is
granted.
The CLI stores connection profiles in ~/.tikti/config.yaml. Each profile holds a base URL, API key, and preferred tenant.
Install from source (requires go and git):
curl -fsSL https://raw.githubusercontent.com/osvaldoandrade/tikti/main/install.sh | shOn Windows, run the same command from Git Bash or WSL. To pin a version, set TIKTI_REF=v0.2.1 before the curl. To change the install directory, set TIKTI_BIN_DIR=$HOME/.local/bin.
Install via npm (requires node/npm):
npm install -g @osvaldoandrade/tikti-cliUpgrade to the latest release:
npm install -g @osvaldoandrade/tikti-cli@latestAfter installation, initialize a profile and begin issuing commands:
./tikti-cli init --base-url http://localhost:8080 --api-key my_api_key --tenant local-tenant
./tikti-cli auth login --email admin@example.com
./tikti-cli token exchange --audience codeq-worker --event-types render_video
./tikti-cli token show --type worker
./tikti-cli tenant create --name "Acme" --slug acme
./tikti-cli membership add --tenant <tenantId> --email user@example.com --roles COMPANY_EMPLOYEE
./tikti-cli membership remove --tenant <tenantId> --email user@example.com
./tikti-cli role create --tenant <tenantId> --name ops --permissions codeq:claim,codeq:result
./tikti-cli client create --tenant <tenantId> --client-id codeq-worker --grant token_exchange
./tikti-cli jwksThe CLI includes a saml command group for managing SAML federation. These commands register and inspect IdP trust relationships, rotate SP keys, and map email domains to tenants for discovery.
Print the SP metadata XML that you provide to an external IdP during onboarding:
./tikti-cli saml metadata [--out FILE]Register an IdP for a tenant by fetching its metadata URL. The CLI downloads the IdP's EntityDescriptor, extracts the SSO URL and signing certificates, and writes a saml:idp:{tid} record to Redis:
./tikti-cli saml idp register --tid <tenantId> --metadata-url <URL> [--attr-map FILE]Inspect the stored IdP configuration for a tenant:
./tikti-cli saml idp show --tid <tenantId> [--json]Rotate SP signing and encryption keys using a two-step process. The --prepare step publishes both old and new certificates in the SP metadata, giving IdPs time to refresh. The --commit step removes the old certificate:
./tikti-cli saml keys rotate --prepare
./tikti-cli saml keys rotate --commitAdditional SAML CLI commands include saml idp update, saml idp remove, saml idp list, saml idp fetch (force metadata refresh), saml test (emit a test AuthnRequest URL), saml domain add, and saml domain remove.
Tikti issues two token types. The idToken is signed with HS256 using the jwtSecret from configuration. The credential-based path (/signIn, /signInWithPassword) and the SAML federation path (/saml/acs) both produce this idToken. When authentication occurs through SAML, the idToken carries an amr: ["saml"] claim; the credential path sets amr: ["pwd"]. All other claims (sub, tid, iat, exp) remain identical across both paths.
The access token is signed with RS256 using the private key specified by jwksPrivateKey. Clients obtain it by calling /token/exchange with a valid idToken. The access token carries iss, aud, scope, tid, eventTypes, and ver claims. Relying parties verify access tokens offline by fetching the public key from /.well-known/jwks.json.
Protected routes accept the service API key only in X-API-Key. A key query
parameter is rejected, even when the correct header is also present, so the
credential cannot leak through URLs, browser history, proxy telemetry, or access
logs. Administrative mutations additionally require the scoped RS256 bearer
described by the endpoint contract; legacy HS256 identity tokens are not
administrative authority.
Tikti acts as a SAML 2.0 Service Provider. Each tenant binds to one external Identity Provider (Azure AD, Okta, Ping, ADFS, Google Workspace, OneLogin, Keycloak, or any SAML 2.0-compliant IdP). The federation uses the Web Browser SSO Profile with HTTP-Redirect binding for outbound AuthnRequests and HTTP-POST binding for inbound Responses.
The browser requests GET /saml/login/{tid}. Tikti loads the IdP record for that tenant from Redis (saml:idp:{tid}), builds a signed AuthnRequest (RSA-SHA256), stores a request correlation record in Redis with a 300-second TTL (saml:req:{id}), and redirects the browser to the IdP's SSO URL with the deflated, base64-encoded SAMLRequest, RelayState, SigAlg, and Signature query parameters.
The IdP authenticates the user and returns a signed SAML Response via HTTP-POST to /saml/acs. Tikti validates the response through a 10-step pipeline: verify InResponseTo correlation, check Destination matches the ACS URL, confirm top-level Status is Success, verify the Response signature against the pinned IdP certificate, decrypt EncryptedAssertion if present, verify the Assertion signature, check Issuer matches the configured IdP entityID, confirm Audience contains the Tikti SP entityID, validate NotBefore/NotOnOrAfter within the 120-second clock skew, and verify SubjectConfirmationData. The first failing step rejects the response.
After validation, Tikti deletes the request correlation record, writes a replay guard (saml:seen:{assertionID}, TTL 3600 seconds), maps assertion attributes to user fields using the tenant's attribute map, JIT-provisions or updates the user record, and calls auth.issueIDToken with amr=["saml"]. The resulting HS256 idToken is delivered to the browser via a cookie (tikti_idt), and the browser is redirected to the RelayState URL. From this point, the token exchange path proceeds identically to the credential-based flow.
Tikti supports SP-initiated and IdP-initiated Single Logout. For SP-initiated logout, GET /saml/logout/{tid} builds a signed LogoutRequest containing the user's NameID and SessionIndex, then redirects to the IdP's SLO URL. The IdP processes the logout and redirects back to /saml/slo with a LogoutResponse. Tikti verifies the response, deletes the session and SAML index records from Redis, and redirects to the post-logout URL.
For IdP-initiated logout, the IdP POSTs a LogoutRequest to /saml/slo. Tikti verifies the signature, extracts the NameID and SessionIndex, deletes the corresponding session and index records, and returns a signed LogoutResponse to the IdP.
The tid is extracted from the URL path in /saml/login/{tid}, never from the SAML assertion, to prevent tenant escalation via a compromised IdP. For deployments with tenant-specific hostnames, the host header maps to a tid via an alias table. The /saml/discover endpoint provides email-domain-based tenant lookup: the browser submits an email address, Tikti resolves the domain to a tid, and redirects to /saml/login/{tid}.
| Endpoint | Method | Description |
|---|---|---|
/admin/identity/directory/users |
GET, POST | Search the bounded safe directory or create a temporary-password user (X-API-Key and RS256 authority required) |
/admin/identity/directory/groups |
GET, POST | Search reusable global groups or create one (X-API-Key; writes are flag-protected) |
/admin/identity/tenants/:tenantId/access-assignments |
GET | List direct assignments for the explicit target tenant (X-API-Key and RS256 authority required) |
/accounts/changeTemporaryPassword |
POST | Replace an administrator-issued temporary password before normal session issuance |
/accounts/signIn |
POST | Authenticate and receive an HS256 idToken |
/accounts/signInWithPassword |
POST | Password authentication (X-API-Key) |
/accounts/lookup |
POST | Look up an account (X-API-Key) |
/accounts/token/exchange |
POST | Exchange an HS256 idToken for an RS256 access token (X-API-Key) |
/workloads/token/exchange |
POST | Exchange a projected Kubernetes ServiceAccount JWT for a tenant-bound provider token |
/workloads/bindings |
POST | Create or replace a workload-to-tenant authorization binding (X-API-Key) |
/workloads/bindings/revoke |
POST | Revoke a workload binding (X-API-Key) |
/.well-known/jwks.json |
GET | Publish RS256 public keys for offline verification |
| Endpoint | Method | Description |
|---|---|---|
/admin/identity/tenant-inventory |
GET | List the authoritative MASTER-first tenant inventory (X-API-Key and scoped RS256 bearer required) |
/admin/identity/tenants/:tenantId |
GET, PUT | Read or ensure an explicit tenant target (X-API-Key and scoped RS256 bearer required) |
/admin/identity/tenants/:tenantId/access-assignments/users/:userId |
GET, PUT, DELETE | Read or mutate one direct user assignment with ETag/If-Match |
/admin/identity/tenants/:tenantId/access-assignments/groups/:groupId |
PUT, DELETE | Mutate one group assignment with ETag/If-Match |
/admin/tenants/:tenantId/roles/:roleName |
PUT | Create-if-absent tenant role using X-API-Key and a scoped RS256 bearer |
/admin/tenants/:tenantId/roles/:roleName |
GET | Read one exact tenant role using X-API-Key and a scoped RS256 bearer |
/admin/tenants/:tenantId/roles |
GET | List tenant roles by name using X-API-Key and a scoped RS256 bearer |
/admin/tenants/:tenantId/clients |
GET, POST | List or register clients for an explicit tenant (X-API-Key and scoped RS256 bearer required) |
| Endpoint | Method | Description |
|---|---|---|
/accounts/status |
POST | Set account status (X-API-Key and platform-admin RS256 bearer required) |
/accounts/revoke |
POST | Revoke an account's tokens (X-API-Key and platform-admin RS256 bearer required) |
/accounts/validate |
POST | Validate an account's credentials (X-API-Key and platform-admin RS256 bearer required) |
| Endpoint | Method | Description |
|---|---|---|
/saml/metadata |
GET | Emit the SP EntityDescriptor XML |
/saml/login/{tid} |
GET | Build a signed AuthnRequest and redirect to the IdP |
/saml/acs |
POST | Consume the IdP's SAML Response and issue an idToken |
/saml/logout/{tid} |
GET | Build a signed LogoutRequest and redirect to the IdP |
/saml/slo |
GET, POST | Handle SP-initiated logout response or IdP-initiated logout request |
/saml/discover |
GET | Resolve an email domain to a tid for tenant routing |
| Endpoint | Method | Description |
|---|---|---|
/healthz |
GET | Liveness check |
The migration tool moves user records from the users hash to users_v2 with a userByEmail index. It never manufactures tenant access; assignments must be explicit. Run with --dry-run first to preview changes:
./tikti-migrate --redis-addr localhost:6379 --dry-run
./tikti-migrate --redis-addr localhost:6379local-tenant is the immutable Code Foundry MASTER tenant. The obsolete tenant ID default is reserved and rejected by every runtime authority path. On startup Tikti idempotently removes a historical default entry from the tenant registry while retaining related records without authority for an explicit rollback.
When SAML federation is enabled, migration 0007_saml_user_fields adds the authSource field (values: password, saml; default: password) and the externalSubject field (default: empty string) to every user record. Existing users retain authSource=password. Users provisioned through SAML JIT receive authSource=saml and an externalSubject set to the IdP's NameID.
Deploy Tikti to Kubernetes using the bundled Helm chart. The chart creates the deployment, service, config map, and secrets. Pass the Redis address, HS256 secret, API key, and RS256 private key as values:
helm upgrade --install tikti ./helm/tikti \
--set image.repository=ghcr.io/osvaldoandrade/tikti \
--set image.tag=0.1.0 \
--set-string config.redisAddr=redis:6379 \
--set-string secrets.jwtSecret=CHANGE_ME \
--set-string secrets.apiKey=CHANGE_ME \
--set-string secrets.jwksPrivateKey=CHANGE_MEFor SAML federation, add the SP signing key and certificate. The chart mounts them into the pod at the paths specified by saml.sp.signingKeyPath and saml.sp.signingCertPath. Append these flags to the helm upgrade command above:
--set saml.enabled=true \
--set-file secrets.samlSigningKey=sp.key \
--set-file secrets.samlSigningCert=sp.crt