Identity Provider
What it does
The identity provider connects your organization's sign-in services, Keycloak, Okta or any other OIDC service, to a deployment. It tells workstations which sign-in services they may offer, and publishes each service's signing keys to the aggregator, where every other provider reads them to verify who is signed in. See the Overview for how it fits with the other components.
In this manual, the identity provider is this program, and a sign-in service is the OIDC system that signs people's tokens.
Quick start
Describe the provider in the deployment description, deployment.kdl:
the host it runs on and each sign-in service your people use.
identity-provider {
host "providers-1"
issuer "keycloak" {
issuer-url "https://keycloak.example.com/realms/acme"
client-id "datumhue-client"
scopes "profile" "email" "groups"
}
}
Make its files with the deployment tool, generate or add-provider.
At each sign-in service, register the aggregator's URL followed by
/oidc-callback.html as a redirect URI, and allow http://127.0.0.1:*
for the workstation app; at a service that lists every redirect URI
exactly, give the issuer a client-secret and register
/oidc-native-callback.html instead.
On the host, install the program, then follow the README.md in the
host's folder of the bundle, which installs the rest and starts it:
sudo install -D -m 755 identity-provider-linux-x86_64 \
/opt/datumhue/identity-provider
Its log names its public key, which matches the line for it in the
bundle's pubkeys.txt, and then each announcement.
It logs identity_provider ready once every service's keys are
published.
How it works
Sign-in services. Each issuer entry names one sign-in service
under a short name, which people see when they sign in. The provider
announces each one to the aggregator under a capability for
identity/<name>, and workstations find the list there.
Signing keys. The provider fetches each service's signing keys over
HTTPS from its OIDC discovery address, whose document must name the
service's issuer-url exactly. It publishes them to the aggregator
under a second capability, for the service's issuer URL, and logs that
capability's namespace at start. Every provider that checks sign-ins
reads the keys there, by the exact issuer URL of a person's token. The
keys are published again every half of the shortest jwks-ttl; while
a fetch fails, the keys published before stay valid until their
jwks-ttl ends.
Token requests. A sign-in service with a client-secret issues
tokens only to requests that carry it. Workstations and browsers send
their sign-in and token-refresh requests for such a service to the
provider, which adds the secret and passes them on over HTTPS. It
passes on no other request, and never logs their contents.
Sessions. For a service with a client-secret, the provider keeps
each sign-in's refresh token, and the workstation holds a handle in its
place. A sign-in unused for idle ends, and so does a person's least
recently used one when they sign in a 21st time. Signing out on a
workstation ends its sign-in here, and datumhue logout --everywhere
every sign-in of the person that this provider holds;
upstream-revocation says what that does at the service. When the
provider a workstation signed in through is gone for good, its people
sign in again.
Without a client-secret, a workstation that signs out revokes its
refresh token at the service, when the service's discovery document
names a revocation address. Keycloak before 26.1 answers a revocation
by ending every sign-in the person has with the client; 26.1 and later
end only the revoked one.
Ending a person's sign-ins. When someone asks you to erase what the
provider keeps about them, identity-provider sessions erase ends
every sign-in of theirs that the provider holds, as
datumhue logout --everywhere does, upstream-revocation included.
Run it on the provider's host as the account the provider runs as,
naming the admin socket that serve --admin-socket gives; without that
setting no sessions command reaches the provider. Name the sign-in
service with --issuer, and give the person's sub on standard input:
the identifier datumhue login shows after "Signed in as". --dry-run
lists the sign-ins it would end. When the command says that no sign-in
held a token the service could revoke, or that the service confirmed
fewer revocations than it was asked for, ask the person to remove the
deployment's access in their account settings at the service.
sudo -u datumhue /opt/datumhue/identity-provider sessions erase \
--issuer google --dry-run \
--admin-socket /var/lib/datumhue/identity-provider/admin.sock
Readiness. Once the provider has published every service's keys,
it announces its sign-in services and logs identity_provider ready.
Until then people see none of its services, not even one whose keys
were published, and the log says why each attempt failed. A supervisor
can wait for that line.
Command line
identity-provider <COMMAND>
| Command | Description |
|---|---|
identity-provider serve | Start the provider service. |
identity-provider sessions | End an account's sign-in sessions through a running provider's admin socket. |
identity-provider serve
identity-provider serve [OPTIONS]
| Flag | Env | Default | Description |
|---|---|---|---|
--config <CONFIG>, -c | ./identity_provider.kdl | Path to the configuration file | |
--aggregator-url <AGGREGATOR_URL>, -a | none | Override the aggregator-url from the configuration file | |
--admin-socket <ADMIN_SOCKET> | DATUMHUE_ADMIN_SOCKET | none | Local socket identity-provider sessions reaches this provider through; it answers only the account the provider runs as. Its folder is made, mode 0700, when missing; a folder another account owns, or others may write in, stops the start. Without it, no sessions command reaches this provider |
identity-provider sessions erase
identity-provider sessions erase [OPTIONS] --issuer <ISSUER>
| Flag | Env | Default | Description |
|---|---|---|---|
--issuer <ISSUER> | required | The sign-in service the account signs in with, by its issuer name in the settings | |
--dry-run | off | List the sessions that would end, and end none | |
--admin-socket <ADMIN_SOCKET> | DATUMHUE_ADMIN_SOCKET | none | The running provider's admin socket, the path its serve --admin-socket names |
Configuration
The settings file is KDL: a provider block, then a trusted-issuers
block with an issuer entry for each sign-in service.
provider {
secret-key-file "/var/lib/datumhue/identity-provider/provider.key"
capability-file "/var/lib/datumhue/identity-provider/provider.caps"
aggregator-url "https://aggregator.example.com"
relay-url "https://aggregator.example.com"
}
trusted-issuers {
issuer "keycloak" {
issuer-url "https://keycloak.example.com/realms/acme"
client-id "datumhue-client"
scopes "profile" "email" "groups"
jwks-ttl "5m"
}
issuer "okta" {
issuer-url "https://acme.okta.com/oauth2/default"
client-id "datumhue"
scopes "profile" "email"
}
issuer "entra" {
issuer-url "https://login.microsoftonline.com/<tenant-id>/v2.0"
client-id "<application-id>"
scopes "profile" "email"
person-claim "oid"
}
}
Common provider settings
Every provider's settings block takes these keys beside its own. A relative path anywhere in the settings file resolves against the folder that holds the file, not the folder the provider starts in.
| Key | Required | Default | Description |
|---|---|---|---|
aggregator-url | yes | none | The aggregator's public HTTPS URL. --aggregator-url overrides it |
relay-url | yes | none | The aggregator's relay, at the same public HTTPS URL |
discovery-url | no | the aggregator's | Where the provider publishes its address and looks up others |
announce-ttl | no | 300 | How long each announcement stays valid: seconds, or a duration such as "5m", at least 2 seconds. The provider announces again after half of it |
webrtc-bind-addr | no | none | A fixed UDP address for direct browser connections, such as "0.0.0.0:7844". Set it with webrtc-public-addr when the provider runs in a container or behind port forwarding |
webrtc-public-addr | no | none | The host:port browsers reach webrtc-bind-addr at. Every address it resolves to must route to that socket |
The key and the capability file. The secret key is the provider's identity: the provider creates it at its first start when the file is missing, and logs its public key at every start. The capability file holds the capabilities that let the provider announce what it serves. The provider reads it again before every announcement and rewrites it as capabilities renew, so it refuses to start when it cannot write to the file's folder. See Provider authorization.
| Variable | Description |
|---|---|
DATUMHUE_KEYSTORE_PASSPHRASE | Encrypts the secret key the first start creates. While the key is encrypted the variable must be set, and while it is plaintext it must be unset |
DATUMHUE_CERT_PATH | A PEM file of CA certificates to trust beside the public ones, for an aggregator whose certificate a private CA signed. Set, it must name a file of CA certificates only |
RUST_LOG | Log levels, info unless set |
Identity provider settings
| Key | Required | Default | Description |
|---|---|---|---|
secret-key-file | no | ./identity_provider.key | The provider's secret key |
capability-file | no | ./identity_provider.capabilities | The capabilities to announce each sign-in service and publish its keys |
Sign-in services
| Key | Required | Default | Description |
|---|---|---|---|
issuer "<name>" | yes | none | One sign-in service, by the short name people see and its capabilities use |
issuer-url | yes | none | The service's issuer URL, exactly as its tokens' iss claim gives it; a trailing / counts. An https URL of at most 2048 bytes with no query, fragment or user name, one entry per URL |
client-id | yes | none | The client registered at the service for this deployment |
client-secret | no | none | The client's secret, for a service that asks for one when it issues tokens. The provider then makes those requests for workstations and browsers, which never see the secret |
auth-param "<key>" "<value>" | no | none | One more parameter for the service's sign-in page, such as auth-param "prompt" "consent"; one entry per parameter |
audience | no | the client-id | The value tokens' aud claim must hold. Set it only when the service puts something else in its ID tokens' aud, and never to the id of an API, whose access tokens could then pass for sign-ins |
scopes | no | none | Scopes to ask for besides openid, which is always asked for. Ask for the scopes that carry the claims your rules use, such as groups |
jwks-ttl | no | "5m" | How long published keys stay valid, from a second to a day: seconds, or a duration such as "30s", "1h" or "24h" |
person-claim | no | "sub" | The claim that names the person in the service's ID tokens and access tokens alike, for a service whose sub differs between them. An egress mount that forwards access tokens takes one as the signed-in person's only when it carries the ID token's value of this claim. A claim many people share, or one a person or the service can change, such as tid or email, stops the provider at start |
upstream-revocation | no | "none" | With a client-secret only: what ending a sign-in does at the service. "token" revokes that sign-in's refresh token, for a service whose revocation ends one sign-in (Okta, Keycloak 26.1 and later); "grant" revokes once every sign-in of the person has ended, for a service whose revocation ends all of them (Google, Keycloak before 26.1); "none" asks nothing of the service |
A shorter jwks-ttl spreads a key change at the service sooner, at the
cost of fetching and publishing more often. Five minutes suits a
service whose keys rarely change.
Microsoft Entra ID gives each application its own sub for one person,
so its access tokens for an API registered as another application name
the person by another sub than its ID tokens do. Its oid is the same
in both: set person-claim "oid", and ask for the profile scope,
without which Entra leaves oid out of ID tokens.
Sessions
A sessions block sets how the provider keeps the sign-ins of the
services with a client-secret.
provider {
secret-key-file "/var/lib/datumhue/identity-provider/provider.key"
capability-file "/var/lib/datumhue/identity-provider/provider.caps"
aggregator-url "https://aggregator.example.com"
relay-url "https://aggregator.example.com"
}
trusted-issuers {
issuer "google" {
issuer-url "https://accounts.google.com"
client-id "datumhue.apps.googleusercontent.com"
client-secret "the-client-secret"
upstream-revocation "grant"
}
}
sessions {
store "/var/lib/datumhue/identity-provider/sessions.redb"
idle "30d"
reuse-grace "60s"
}
| Key | Required | Default | Description |
|---|---|---|---|
store | no | identity_provider.sessions.redb beside secret-key-file | The file that holds the sign-ins. A relative path counts from the settings file |
idle | no | "30d" | How long an unused sign-in lasts, at least a second |
reuse-grace | no | "60s" | How long after a refresh the handle it replaced still refreshes once, for a workstation that lost the answer; up to 5 minutes |
Registering the client
At each sign-in service, register the aggregator's public URL followed
by /oidc-callback.html as a redirect URI of the client, for browsers.
The workstation app's redirect URI depends on the service's entry:
- Without
client-secret, the workstation app signs in through a loopback redirect URI on a port it picks: allow loopback redirect URIs on any port,http://127.0.0.1:*. - With
client-secret, the workstation app signs in through the aggregator: register the aggregator's public URL followed by/oidc-native-callback.htmlas well. Use this for a service that lists every redirect URI exactly, such as Google.
Access rules
The identity provider decides no access itself; the other providers
decide it with their access rules, from the claims of a person's ID
token. Each rule that reads a sign-in sits in a block naming its
service's issuer URL: issuer "<issuer-url>" { … }, with the URL
exactly as this provider's issuer-url spells it. A claim works in a
rule when:
- the service that signed the token is an
issuerhere, itsissuer-urlexactly the token'siss, and this provider holds the capability to publish that service's keys; - the rule sits in the block of that same URL: a token from any other issuer gets nothing the block grants;
- the service puts the claim in the ID token: ask for the scope that
carries it in
scopes, and set the service up to add the claim; - for
email, the same token saysemail_verifiedistrue, and forphone_number,phone_number_verifiedistrue. Theemailandphonescopes carry them. Microsoft Entra ID does not sendemail_verified: matchoid,suborgroupsthere.
Network, state and dependencies
Network surface
| Direction | What |
|---|---|
| from workstations | The list of sign-in services |
| from workstations | Their token requests and sign-outs, for a service with a client-secret |
| to the aggregator | Each service's keys, every half of the shortest jwks-ttl |
| to the aggregator | Announcing each service, every half announce-ttl |
| to each sign-in service | Its discovery address and its keys, over HTTPS |
| to each sign-in service | Workstations' token requests, with the client-secret |
| to each sign-in service | Revocations, as its upstream-revocation says |
The provider listens on no TCP port; workstations reach it through the
aggregator. It takes 256 connections at once; past that a connection is
turned away.
The admin socket, when serve --admin-socket names one, is a local
socket on the provider's host.
Persistent state
| Path | Holds |
|---|---|
secret-key-file | The provider's identity. A lost key needs new capabilities |
capability-file | Its capabilities, which it renews itself |
sessions store | The sign-ins of the services with a client-secret. Only with such a service |
The session store and the key file together expose the current and recently ended sign-ins: keep both, and their backups, as private as the key file. Restoring an older store ends each sign-in that refreshed since it was saved, and losing the store or the key ends every sign-in: its people sign in again.
External dependencies
- The aggregator, for announcing and publishing keys.
- Each sign-in service, reachable over HTTPS at its
issuer-url. Its certificate must come from a public CA, or from one inDATUMHUE_CERT_PATH.
Running as a service
systemd
The deployment tool writes a unit like this one for every provider it
sets up. To run the provider under systemd by hand,
install the program as /opt/datumhue/identity-provider, its settings as
/, and give the datumhue user the state
folder /.
/:
[Unit]
Description=DatumHue Identity Provider
After=network.target
[Service]
Type=simple
User=datumhue
Group=datumhue
WorkingDirectory=/var/lib/datumhue/identity-provider
ExecStart=/opt/datumhue/identity-provider serve \
--config /etc/datumhue/identity-provider.kdl
Environment=DATUMHUE_CERT_PATH=/etc/datumhue/ca-cert.pem
EnvironmentFile=/etc/datumhue/keystore.env
Restart=always
RestartSec=10
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/datumhue/identity-provider
[Install]
WantedBy=multi-user.target
- The unit keeps
/etc/datumhueread-only, and a relative path in the settings file resolves there. Pointsecret-key-file,capability-fileand every other file the provider writes into/.var/ lib/ datumhue/ identity-provider/ keystore.envholdsDATUMHUE_KEYSTORE_PASSPHRASE=...; keep it mode 600. Drop theEnvironmentFile=line for a plaintext key.- Drop the
DATUMHUE_CERT_PATHline when the aggregator's certificate comes from a public CA. - On the aggregator's host, add
datumhue-aggregator.servicetoAfter=.
sudo systemctl daemon-reload
sudo systemctl enable --now datumhue-identity-provider
journalctl -u datumhue-identity-provider -f
systemctl stop stops the provider the way Ctrl+C does: it stops
announcing and gives its provider seat back, so the next start takes a
seat at once.
Set DATUMHUE_CERT_PATH when the aggregator or a sign-in service uses a
certificate from a private CA; one file may hold several CA
certificates.
Docker
To run the provider in a container, build an image from this
Dockerfile, in the folder that holds the program:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY identity-provider-linux-x86_64 /opt/datumhue/identity-provider
ENTRYPOINT ["/opt/datumhue/identity-provider"]
CMD ["serve", "--config", "/etc/datumhue/identity-provider.kdl"]
Run it as the datumhue user that owns the state folder, with the
settings folder read-only:
docker build -t datumhue/identity-provider .
state=/var/lib/datumhue/identity-provider
docker run -d --name datumhue-identity-provider --restart always \
--user "$(id -u datumhue):$(id -g datumhue)" \
--env-file /etc/datumhue/keystore.env \
-e DATUMHUE_CERT_PATH=/etc/datumhue/ca-cert.pem \
-v /etc/datumhue:/etc/datumhue:ro -v "$state:$state" \
datumhue/identity-provider
The docker-compose.yml the deployment tool writes runs images by these
names, with the same folders.
Security notes
- The write capability is the right to publish keys. Only a provider that holds it for the namespace of the service's issuer URL publishes that service's keys, whatever name its settings give the service. Several providers may publish for one service; readers take the keys of all of them. A revocation of the provider's key in that namespace stops its publishing, and other providers stop accepting the keys it published within a minute.
- A key change at the service reaches every provider within half
the shortest
jwks-ttlplus a minute. Shortenjwks-ttlwhen that matters. - The audience and the kind of token are always checked. A token
whose
auddoes not hold theaudience, or theclient-idwhen noaudienceis set, is refused. So is a token that marks itself as an access token by itstyp,token_use,scopeorscp. - A copied sign-in ends. With a
client-secret, a workstation's handle changes at every refresh, and one used again ends its sign-in. A service without aclient-secretleaves its refresh tokens on the workstations: turn on refresh-token rotation with reuse detection there (Keycloak: "Revoke Refresh Token" on, "Max reuse" 0), and a copied one ends the same way. - The admin socket ends sign-ins. Whoever can run commands as the provider's account can end anyone's sign-ins through it; it answers that account alone.
- A
client-secretis a password. Keep a settings file that holds one at mode 600, owned by thedatumhueuser. - Protect the key. The deployment tool encrypts it with your
passphrase; a key the provider makes at its first start is plaintext
unless
DATUMHUE_KEYSTORE_PASSPHRASEis set. The provider refuses a key file that others can read.
Troubleshooting
Messages. The provider writes its log to standard output; under
systemd, read it with journalctl -u and the unit's name.
The aggregator is refused as untrusted. It serves the certificate it
made itself: point DATUMHUE_CERT_PATH at its tls/ca-cert.pem.
provider lease acquisition failed. With QuotaExhausted, every
provider seat of the license is taken: stop a provider, or extend the
license.
The seat or announcements are refused. capability-file holds no
current capability for this key: make one with
datumhue-deploy reissue-capabilities.
The provider stops at start, naming relay-url. Set relay-url to
the aggregator's public HTTPS URL.
The provider refuses its key file. A key file that others can read
is refused: chmod 600 it. DATUMHUE_KEYSTORE_PASSPHRASE must be set
for an encrypted key and unset for a plaintext one; the message says
which the key is.
The provider cannot write its capability file. Give the provider's user write access to the folder that holds the file; it rewrites the file as capabilities renew.
The provider never logs identity_provider ready. One service
whose keys fail to publish holds back every service of the provider.
Read the failed to publish JWKS for lines:
no capability to publish the keys of <url>, orholds no current capability to publish these keys: the capability file lacks a current capability for that issuer URL's namespace, which the line names. A capability foridentity/<name>announces the service but publishes no keys; the deployment operator issues the missing one withdatumhue-deploy reissue-capabilities.a revocation withdraws this provider's right: the deployment operator revoked this provider's key for that service withdatumhue-deploy revoke;datumhue-deploy liftrestores it.the discovery document names issuer: the service calls itself by another issuer URL, which its tokens carry too. Setissuer-urlto the one the line quotes, a trailing/included; the changed URL needs a capability of its own, as above.holds no signing key: the service's key address lists no RSA or EC key for signatures.URL scheme is not allowed: the service's discovery document gives anhttpaddress, or one of its addresses redirects to one. The provider reaches a sign-in service over HTTPS only.the aggregator refused the issuer's settings: the line names the setting it refused.the key set is not, orthe aggregator refused the sign-in service's key set: the service's key address answered with something other than a key set.make a publish of: the service publishes more keys than an aggregator takes in one publish, 64 KiB.- A discovery or fetch error: the service cannot be reached, or
issuer-urlis wrong.
People see no sign-in services. The log shows announcer running
and announced issuer once the provider announces. Without
announcer running, the provider is not ready yet: see the entry
above on identity_provider ready. With it but without
announced issuer, the capability file lacks a current capability to
announce identity/<name>.
The sign-in page reports an invalid redirect URI. Register the
redirect URIs that
Registering the client
lists for the service's entry: a service that lists every redirect URI
exactly needs a client-secret, since the workstation app uses
/oidc-native-callback.html only then.
Other providers refuse signed-in people. Check that the provider
published the keys: from its host,
curl -I <issuer-url>/. must succeed.
sessions erase gets no answer. "connecting to the admin socket"
means the path is not the one serve --admin-socket names, or the
provider runs without it. "closed the connection unanswered" means the
command ran as another account than the provider's: run it as that
account. When the provider stops at start over the admin socket, its
folder belongs to another account or others may write in it: make it
mode 700.
Tokens fail on the audience. The token's aud lacks the
client-id. Add the client-id as an audience at the service, or set
audience to what the service puts in aud.
A claim is missing from tokens. Ask for the scope that carries it
in scopes, and set the service up to add the claim to the ID token.
DATUMHUE_CERT_PATH is refused at start. It names a server
certificate. Point it at the CA certificate, such as ca-cert.pem.