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 three binaries (server, CLI, migration tool), 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 project produces three binaries. The 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.
go build -o tikti ./cmd/tikti
go build -o tikti-cli ./cmd/tikti-cli
go build -o tikti-migrate ./cmd/tikti-migrateThe CLI stores connection profiles in ~/.tikti/config.yaml. Each profile holds a base URL, API key, and default 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 default
./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 admin routes require the API key as a query parameter: ?key=API_KEY.
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 |
|---|---|---|
/accounts/signUp |
POST | Register a new user account |
/accounts/signIn |
POST | Authenticate and receive an HS256 idToken |
/accounts/signInWithPassword?key=... |
POST | Admin-initiated password authentication |
/accounts/lookup?key=... |
POST | Look up an account by email |
/accounts/token/exchange?key=... |
POST | Exchange an HS256 idToken for an RS256 access token |
/.well-known/jwks.json |
GET | Publish RS256 public keys for offline verification |
| Endpoint | Method | Description |
|---|---|---|
/tenants?key=... |
POST | Create a tenant |
/tenants/:id?key=... |
GET | Retrieve a tenant |
/tenants/:tenantId/users?key=... |
POST | Add a user to a tenant |
/tenants/:tenantId/roles?key=... |
POST | Create a role within a tenant |
/tenants/:tenantId/roles?key=... |
GET | List roles within a tenant |
/tenants/:tenantId/clients?key=... |
POST | Register a client for a tenant |
/tenants/:tenantId/clients?key=... |
GET | List clients for a tenant |
| Endpoint | Method | Description |
|---|---|---|
/accounts/status?key=... |
POST | Set account status (enable/disable) |
/accounts/revoke?key=... |
POST | Revoke an account's tokens |
/accounts/validate?key=... |
POST | Validate an account's credentials |
| 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 and creates default tenant memberships. Run with --dry-run first to preview changes:
./tikti-migrate --redis-addr localhost:6379 --default-tenant default --dry-run
./tikti-migrate --redis-addr localhost:6379 --default-tenant defaultWhen 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