Documentation
¶
Overview ¶
Package identity converges the CONTENT of one Zitadel instance to a declared Spec: the project, its roles, the OIDC applications (web, SPA, native and the native-sign-in broker app), machine users and their keys, the login / password-complexity / lockout policies, SMTP, declared role members and (dev and preview only) seed users.
It is the one library behind both halves of managed identity. `forge env up` runs it against the dev instance with a bootstrap PAT; the control plane runs it against each customer instance with that instance's owner machine key. The same code path in both is what makes dev parity real.
What it deliberately is not ¶
It does not create or delete INSTANCES (that is the System API, the control plane's job) and it speaks plain JSON over HTTP. It imports no Zitadel Go package: Zitadel is AGPL-3.0 and this library must stay on the permissive side of that boundary. Stdlib plus forge packages only.
Idempotent by construction ¶
Every step reads first, creates only what is missing and repairs drift only where that is safe. A second run against a converged instance performs no write at all (Report.Changed is false), which is the property the reconciler's periodic sweep depends on. Objects are matched by their natural key — project name, role key, application name, username, email — never by an internal id, because the ids are generated and a declaration cannot name them.
Adoption ¶
An instance that already holds content (one created by hand) is converged in place. The natural-key match above is what keeps its objects: nothing is recreated because its internal id is not the one a fresh instance would have produced. Where another system holds an id (the project id is the token audience; the client id is baked into a deployed server's config), pin it with ProjectSpec.AdoptID / Application.AdoptClientID: a pin turns "the match diverged from what is deployed" into an error instead of a silent outage, and finds the object under another name (a console rename) and renames it back. Options.Adopt additionally forbids creating the project or an application at all. Options.DryRun reports the writes a pass would make, so an adoption can be reviewed before it runs.
Seams ¶
KeyStore is where credentials the library GENERATES live — machine keys, the broker's access token, client secrets — and is also its memory: a key is kept only while the store still holds it. Outputs is the write-only publication of what consumers need (issuer, audience, client ids, the JWKS snapshot, the broker token). Dev wires directories and files; the control plane wires OpenBao and its delivery path.
Index ¶
- Constants
- func AudienceScope(projectID string) string
- func BrokerTokenEntry(username string) string
- func ClientSecretEntry(app string) string
- func MachineKeyEntry(username string) string
- func ProjectRolesClaim(projectID string) string
- func ValidMachineUsername(name string) error
- type APIError
- type Action
- type AppKind
- type AppOutput
- type Application
- type BrokerAccount
- type Change
- type Credential
- type CredentialFunc
- type DirKeyStore
- type FileOutputs
- type KeyStore
- type Lockout
- type LoginPolicy
- type MachineKeyDoc
- type MachineOutput
- type MachineUser
- type MemKeyStore
- type ObjectError
- type Options
- type Output
- type Outputs
- type OutputsFunc
- type PasswordComplexity
- type Policies
- type ProjectSpec
- type Reconciler
- type Report
- type Retiring
- type Role
- type RotateTarget
- type Rotation
- type SMTP
- type SeedUser
- type SignInCode
- type Spec
Constants ¶
const RolesScope = "urn:zitadel:iam:org:projects:roles"
RolesScope is the reserved scope that asks for project roles to be asserted into a token. It is one of THREE things a roles claim needs in an ACCESS token: the project's projectRoleAssertion, this scope in the authorize request, and accessTokenRoleAssertion on the application. Omit any one and the token is well-formed, passes signature, issuer and audience checks, and carries no roles — so every operator reads as unprivileged.
const SignInCodeLifetime = time.Hour
SignInCodeLifetime is how long a minted sign-in code stays redeemable: Zitadel's default PasswordVerificationCode expiry (cmd/defaults.yaml, SecretGenerators.PasswordVerificationCode.Expiry). An instance configured otherwise makes SignInCode.ExpiresAt an estimate.
Variables ¶
This section is empty.
Functions ¶
func AudienceScope ¶
AudienceScope is the reserved scope that puts the project id into a token's `aud`. Zitadel has no arbitrary "API resource indicator": the audience IS the project's generated id, so a client must request this scope or its token names its own client id instead of the API.
func BrokerTokenEntry ¶
BrokerTokenEntry is the KeyStore name of a broker account's token record.
func ClientSecretEntry ¶
ClientSecretEntry is the KeyStore name of a web application's client secret.
func MachineKeyEntry ¶
MachineKeyEntry is the KeyStore name of a machine user's key document.
func ProjectRolesClaim ¶
ProjectRolesClaim is the claim the project's roles are asserted under. Zitadel does not mint a flat `roles` list: the claim is namespaced by project id and its value is a MAP of role key to {org id: org domain}.
func ValidMachineUsername ¶
ValidMachineUsername reports why name cannot be a machine user's username. It is deliberately narrower than Zitadel: the name becomes part of a secret name, must be a DNS-label fragment, and starting with a letter keeps it from ever equalling one of Zitadel's numeric ids.
Types ¶
type APIError ¶
type APIError struct {
Method string
Path string
Status int
Code int
ID string
Message string
Body string
}
APIError is a non-2xx answer from the instance. Message is Zitadel's own text and ID its stable error id (COMMAND-1m88i): branch on the id, never on the localized message.
type Action ¶
type Action string
Action is what a pass did (or, in a dry run, would do) to an object.
type AppKind ¶
type AppKind string
AppKind selects the OIDC application shape.
const ( // AppSPA is a browser single-page app: a public client using PKCE. AppSPA AppKind = "spa" // AppWeb is a confidential server-side web app with a client secret, // which is generated once and held in the KeyStore. AppWeb AppKind = "web" // AppNative is a mobile or desktop app: a public client using PKCE. AppNative AppKind = "native" // AppNativeBroker is the app behind native sign-in: the product's own // login form, with a server-side broker that drives the OIDC flow. It is a // public PKCE client whose login pages are LoginURI, plus a broker machine // account holding IAM_LOGIN_CLIENT and an expiring access token. AppNativeBroker AppKind = "native_broker" )
type AppOutput ¶
type AppOutput struct {
AppID string `json:"app_id"`
ClientID string `json:"client_id"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
PostLogoutURIs []string `json:"post_logout_uris,omitempty"`
LoginURI string `json:"login_uri,omitempty"`
// ClientSecret is set for a web app whose secret is in the KeyStore.
ClientSecret string `json:"-"`
// BrokerUser / BrokerToken / BrokerTokenExpires describe an
// AppNativeBroker's account and its CURRENT token. The token is the
// idp_broker_token the product's server runs with.
BrokerUser string `json:"broker_user,omitempty"`
BrokerToken string `json:"-"`
BrokerTokenID string `json:"broker_token_id,omitempty"`
BrokerTokenExpires time.Time `json:"broker_token_expires,omitempty"`
}
AppOutput is one application's published values.
type Application ¶
type Application struct {
// Name is the application's natural key.
Name string
Kind AppKind
// RedirectURIs and PostLogoutURIs are compared LITERALLY by the issuer, so
// a changed host silently breaks the callback until this converges. With
// DevMode they are globs.
RedirectURIs []string
PostLogoutURIs []string
// LoginURI is the app's own sign-in route. When set the issuer redirects
// /oauth/v2/authorize INTO the app (login V2) instead of rendering its own
// pages. It is set per application, so two apps sharing an instance each
// point at their own route. Empty restores the issuer-hosted login.
LoginURI string
// DevMode permits plain-http redirect URIs and turns them into globs. Only
// for a loopback dev IdP.
DevMode bool
// IDTokenRoleAssertion / IDTokenUserinfoAssertion put the project roles
// and the profile claims into the ID token. The access token always
// carries the roles claim.
IDTokenRoleAssertion bool
IDTokenUserinfoAssertion bool
// AdoptClientID pins the client id the application must have; see the
// package comment.
AdoptClientID string
// RegenerateSecret mints a new client secret for an AppWeb whose secret is
// not in the KeyStore. It invalidates the previous secret, so it is never
// done implicitly for an application that already existed.
RegenerateSecret bool
// Broker configures the broker account of an AppNativeBroker.
Broker *BrokerAccount
}
Application is an OIDC application registered in the project.
func (Application) BrokerUsername ¶
func (a Application) BrokerUsername() string
BrokerUsername is the machine username of an application's broker account.
type BrokerAccount ¶
type BrokerAccount struct {
// Username defaults to "broker-" plus a slug of the application name.
Username string
}
BrokerAccount is the machine account the login broker authenticates as. It is a privileged credential (IAM_LOGIN_CLIENT can complete an authorization request for any user of the instance), kept apart from the provisioning credential so the busiest surface of an app never holds the credential that administers the instance.
type Credential ¶
type Credential interface {
// contains filtered or unexported methods
}
Credential authenticates the library to the instance. Use PAT for a personal access token (the dev bootstrap) and MachineKey for an instance-owner machine key (the control plane). Both must carry IAM_OWNER: the library edits instance-level policies and SMTP as well as the project.
func MachineKey ¶
func MachineKey(raw []byte) (Credential, error)
MachineKey authenticates as a machine user through the JWT-profile grant: a signed assertion is exchanged for an access token, which is cached until shortly before it expires. raw is the key document Zitadel returned when the key was created.
func MachineKeyFile ¶
func MachineKeyFile(path string) (Credential, error)
MachineKeyFile is MachineKey for a key document on disk (a projected Secret).
func PAT ¶
func PAT(token string) Credential
PAT authenticates with a personal access token, which is already a bearer token for every API: no exchange happens.
func PATFile ¶
func PATFile(path string) Credential
PATFile reads a personal access token from path on every request, so a token the owner rotates in place is picked up without a restart.
type CredentialFunc ¶
CredentialFunc adapts a function (a token minted elsewhere, rotated by its owner) to a Credential. It is called for every request, so it should cache.
type DirKeyStore ¶
type DirKeyStore struct{ Dir string }
DirKeyStore keeps one 0600 file per entry under Dir. It is the dev store: the directory is gitignored and holds nothing a throwaway local instance would not.
type FileOutputs ¶
type FileOutputs struct{ Path string }
FileOutputs writes the non-secret Output as JSON to Path, atomically, 0644. It is the dev publication: `.forge/identity/<name>.json`.
type KeyStore ¶
type KeyStore interface {
// Load returns the stored value, or (nil, nil) when none is stored.
Load(ctx context.Context, name string) ([]byte, error)
// Save durably replaces the value. An error means the value was NOT
// stored; the library then discards what it had just issued and keeps the
// previous credential.
Save(ctx context.Context, name string, value []byte) error
}
KeyStore is where credentials the library GENERATES live, and it is also the library's memory of them: Zitadel returns a machine key's private half, a personal access token and a client secret exactly once, so the store holds the ONLY copy. A pass that finds an entry missing issues a new credential rather than failing, which is what makes an emptied store recoverable by simply running again.
Names are opaque, stable strings such as "machine-key:ops-automation". Dev maps them to files (DirKeyStore); the control plane maps them to OpenBao paths.
type LoginPolicy ¶
type LoginPolicy struct {
AllowUsernamePassword bool
AllowRegister bool
AllowExternalIDP bool
ForceMFA bool
HidePasswordReset bool
AllowDomainDiscovery bool
// PasswordlessType is "PASSWORDLESS_TYPE_ALLOWED" or
// "PASSWORDLESS_TYPE_NOT_ALLOWED"; empty means not allowed.
PasswordlessType string
// MFAInitSkipLifetime is how long a user may skip enrolling a second
// factor; zero never prompts.
MFAInitSkipLifetime time.Duration
}
LoginPolicy is the instance login policy. Fields not exposed here (check lifetimes, factor lists) keep their current values.
type MachineKeyDoc ¶
type MachineKeyDoc struct {
Type string `json:"type"`
KeyID string `json:"keyId"`
Key string `json:"key"`
UserID string `json:"userId"`
}
MachineKeyDoc is Zitadel's machine-key JSON document: the shape its own SDKs and the JWT-profile grant consume, and the value stored verbatim in a KeyStore.
func ParseMachineKey ¶
func ParseMachineKey(raw []byte) (MachineKeyDoc, error)
ParseMachineKey decodes a key document and checks it is usable.
type MachineOutput ¶
type MachineOutput struct {
UserID string `json:"user_id"`
KeyID string `json:"key_id,omitempty"`
KeyExpires time.Time `json:"key_expires,omitempty"`
TokenID string `json:"token_id,omitempty"`
TokenExpires time.Time `json:"token_expires,omitempty"`
// Token is the machine user's current personal access token, if it has one.
Token string `json:"-"`
}
MachineOutput is one machine user's published values. The key itself is in the KeyStore, not here.
type MachineUser ¶
type MachineUser struct {
// Username is the natural key and a DNS-label-shaped name: it is part of a
// secret name downstream.
Username string
Name string
Description string
// Roles are the project roles the account holds.
Roles []string
// InstanceRoles are instance-level memberships (for example
// IAM_LOGIN_CLIENT). They are only ever added, never removed.
InstanceRoles []string
// Key issues a JSON machine key into the KeyStore and keeps it rotated.
Key bool
// Token issues an expiring personal access token into the KeyStore.
Token bool
}
MachineUser is a service account.
type MemKeyStore ¶
type MemKeyStore struct {
// SaveErr, when set, makes every Save fail; tests use it to prove a failed
// delivery leaves the previous credential intact.
SaveErr error
// contains filtered or unexported fields
}
MemKeyStore is an in-memory KeyStore for tests and one-shot tools.
func (*MemKeyStore) Delete ¶
func (s *MemKeyStore) Delete(name string)
Delete drops an entry (test helper: simulates an emptied store).
type ObjectError ¶
ObjectError names the object a failure concerns. Every error leaving the package is wrapped in one, so "the pass failed" always says WHICH thing.
func (*ObjectError) Error ¶
func (e *ObjectError) Error() string
func (*ObjectError) Unwrap ¶
func (e *ObjectError) Unwrap() error
type Options ¶
type Options struct {
// BaseURL is the address this process DIALS. Required.
BaseURL string
// Host overrides the HTTP Host header. Zitadel resolves which instance a
// request belongs to from the Host, so when the address you dial (a
// Service DNS name, a compose service) differs from the domain the
// instance is registered under, set Host to the latter.
Host string
// Issuer pins the issuer string the published outputs carry and the JWT
// audience for machine-key exchange. Empty reads it from the instance's
// discovery document, which is right unless discovery is not served at the
// dialled address.
Issuer string
// Credential authenticates the library; it must hold IAM_OWNER. Required.
Credential Credential
// OrgID scopes the content to one organization. Empty resolves it: OrgName
// if set, else the credential's own organization.
OrgID string
OrgName string
// Keys remembers generated credentials. Required when the Spec generates
// any (machine keys, tokens, web client secrets, an SMTP password).
Keys KeyStore
// Outputs receives the Output at the end of every successful pass.
Outputs Outputs
Rotation Rotation
// Adopt converges an instance that already holds content: the project and
// every application must already exist and are never created. Objects are
// matched by natural key (or by a pinned id) and kept whatever their
// internal ids.
Adopt bool
// DryRun computes the Report without writing anything.
DryRun bool
// AllowSeedUsers permits Spec.SeedUsers. Local and ephemeral instances only.
AllowSeedUsers bool
// SignInCodes mints a one-time password-reset code, returned in the Report,
// for every declared member who has no credential. Use it where nothing can
// deliver mail.
SignInCodes bool
// HeldSignInCode, when set, reports whether the caller still holds an
// unexpired code it delivered for a member. Such a member is not minted
// another: minting invalidates the code they hold, so a converge loop that
// minted on every pass would change the code under someone about to use it,
// and no pass would ever be quiet. Nil: every credential-less member gets a
// fresh code on every pass. Consulted only with SignInCodes.
HeldSignInCode func(email string) bool
// KeepSuperseded disables deleting credentials that a newer one has
// replaced, once the overlap has passed. They are then only reported.
KeepSuperseded bool
// ReadyTimeout bounds the wait for the instance to answer its readiness
// probe (default 2m). RequestTimeout bounds one API call (default 15s) and
// MaxAttempts the tries one call gets (default 4).
ReadyTimeout time.Duration
RequestTimeout time.Duration
MaxAttempts int
HTTPClient *http.Client
Logger *slog.Logger
// Now is the clock credential lifetimes are judged against.
Now func() time.Time
}
Options configure a Reconciler.
type Output ¶
type Output struct {
// Issuer is the `iss` the instance mints; the validator's jwt_issuer.
Issuer string `json:"issuer"`
// OrgID is the organization the content lives in.
OrgID string `json:"org_id"`
// ProjectID is the project id, which is also the token audience.
ProjectID string `json:"project_id"`
// Audience is the value a validator must enforce as `aud` (ProjectID).
Audience string `json:"audience"`
// RolesClaim is the namespaced claim the project's roles are asserted
// under: a map of role key to org id to org domain, not a flat list.
RolesClaim string `json:"roles_claim"`
// Scopes are what a client must request for an access token that carries
// the audience and the roles claim.
Scopes []string `json:"scopes"`
// JWKS is a snapshot of the instance's public signing keys, for a validator
// that must start while the issuer is unreachable.
JWKS json.RawMessage `json:"jwks,omitempty"`
// Apps are the registered applications by name.
Apps map[string]AppOutput `json:"apps,omitempty"`
// Machine are the machine users by username.
Machine map[string]MachineOutput `json:"machine,omitempty"`
}
Output is everything a consumer of the instance is configured from.
type Outputs ¶
Outputs is the write-only publication of what consumers of the instance need. Publish is called once at the end of every successful pass, so an implementation must be idempotent; it should treat Output's secret fields (which json omits) as credentials.
type OutputsFunc ¶
OutputsFunc adapts a function to Outputs.
type PasswordComplexity ¶
type PasswordComplexity struct {
MinLength int
RequireUppercase bool
RequireLowercase bool
RequireNumber bool
RequireSymbol bool
}
PasswordComplexity is the instance password complexity policy.
type Policies ¶
type Policies struct {
Login *LoginPolicy
PasswordComplexity *PasswordComplexity
Lockout *Lockout
}
Policies are the instance-level login policies. A nil policy is left alone.
type ProjectSpec ¶
type ProjectSpec struct {
// Name is the project's natural key.
Name string
// AdoptID pins the id the project must have. See the package comment.
AdoptID string
// MustHoldRole makes Zitadel refuse tokens for users who hold no role in the
// project; ProjectCheck refuses tokens for users whose org was not granted
// the project. Both default to off.
MustHoldRole bool
ProjectCheck bool
}
ProjectSpec is the Zitadel project. Its generated id is the token audience, which is why it is an OUTPUT (Result.Audience) and not something a spec can choose.
type Reconciler ¶
type Reconciler struct {
// contains filtered or unexported fields
}
Reconciler converges one instance. It is safe to reuse across passes but not for concurrent passes against the same instance.
func New ¶
func New(opts Options) (*Reconciler, error)
New builds a Reconciler. It does not contact the instance.
func (*Reconciler) Reconcile ¶
Reconcile makes the instance match spec and reports what it changed.
Steps run in dependency order. The project is foundational and its failure ends the pass; every other step records its error and the pass continues, so one broken object does not hide the state of the rest. All errors are joined and each names its object (see ObjectError). The Output is published only when nothing failed.
func (*Reconciler) Rotate ¶
func (r *Reconciler) Rotate(ctx context.Context, spec Spec, targets ...RotateTarget) (*Report, error)
Rotate runs a full pass and additionally replaces the named credentials. The superseded ones stay valid for the configured overlap (Rotation) and are listed in Report.Retiring; a later pass deletes them once it has elapsed. The Output published at the end carries the new credentials, so a consumer that re-reads it switches over before the old one is removed.
type Report ¶
type Report struct {
// Changes lists every write. A converged instance yields none.
Changes []Change
// Warnings are things left alone on purpose: a role nobody declared, a
// credential that cannot be recovered.
Warnings []string
Retiring []Retiring
SignInCodes []SignInCode
// Credentialed lists the declared members this pass CONFIRMED hold a
// credential (filled only with Options.SignInCodes): any code a caller
// holds for them is spent or moot. A member whose check failed is in
// neither this list nor SignInCodes.
Credentialed []string
// Output is the published view, filled even when Options.Outputs is nil.
Output Output
DryRun bool
}
Report is what a pass did.
type Retiring ¶
type Retiring struct {
Username string
Kind string // "machine key" or "token"
ID string
DeleteAfter time.Time
}
Retiring is a superseded credential that still works until DeleteAfter.
type Role ¶
type Role struct {
Key string
DisplayName string
Group string
// Declared makes the holders of this role a SET converged from the spec:
// the humans in Members plus the machine users that list the role. A holder
// not in that set loses THIS role (other roles in the same grant are kept).
// Leave it false for a role whose members are managed in a console.
Declared bool
// Members are the emails that hold the role when Declared is set.
Members []string
}
Role is a project role. Roles are only ever created and relabelled, never deleted: a role may have grants, and removing it silently strips them.
type RotateTarget ¶
type RotateTarget struct {
// Application rotates the broker token of this native_broker application.
Application string
// MachineUser rotates this machine user's key (and token, if it has one).
MachineUser string
}
RotateTarget names credentials to replace NOW, whatever their remaining lifetime. The old credential keeps working for the overlap window.
type Rotation ¶
type Rotation struct {
// BrokerTokenTTL is the lifetime of a minted broker token (default 30d); a
// new one is minted once less than BrokerRenewWithin (default 10d) is left.
BrokerTokenTTL time.Duration
BrokerRenewWithin time.Duration
// BrokerOverlap is how long a superseded token keeps working (default 10d).
BrokerOverlap time.Duration
// MachineKeyTTL (default 90d), MachineKeyRenewWithin (default 30d) and
// MachineKeyOverlap (default 7d) are the same three for machine keys.
MachineKeyTTL time.Duration
MachineKeyRenewWithin time.Duration
MachineKeyOverlap time.Duration
}
Rotation tunes credential lifetimes. Zero values take the defaults.
type SMTP ¶
type SMTP struct {
// Host is host:port.
Host string
User string
Password string `json:"-"`
SenderAddress string
SenderName string
ReplyTo string
TLS bool
}
SMTP is the instance mail relay.
type SeedUser ¶
type SeedUser struct {
Email string
Password string `json:"-"`
GivenName string
FamilyName string
Roles []string
}
SeedUser is a throwaway account. Its password is set when the account is created and never again, so a user who changed it is not reset.
type SignInCode ¶
type SignInCode struct {
Email string
Code string
// ExpiresAt is Options.Now at minting plus SignInCodeLifetime.
ExpiresAt time.Time
}
SignInCode is a one-time code that lets a credential-less member set a first password. It is single-use and short-lived; treat it as a secret.
type Spec ¶
type Spec struct {
Project ProjectSpec
Roles []Role
Applications []Application
MachineUsers []MachineUser
Policies Policies
// SMTP is the instance mail relay. Nil leaves the instance's mail
// configuration alone.
SMTP *SMTP
// SeedUsers are throwaway accounts for local and ephemeral environments.
// Reconcile refuses them unless Options.AllowSeedUsers is set.
SeedUsers []SeedUser
}
Spec is the DECLARED content of one Zitadel instance. Reconcile makes the instance match it. Objects are matched by natural key (project name, role key, application name, username, email), never by internal id.