Fix single sign-on from the web, and make passwords changeable

Why: two questions about operating this turned up two gaps. "Where do I get
the admin password" had no answer once the line scrolled out of the log,
and "how do I set up OIDC" ran straight into a sign-in link that could not
work.

What:
- /oidc/login with no parameters now signs a browser in and ends in a
  session cookie. It previously required the user code of a waiting client,
  so the link on the web sign-in page always failed. With a code it still
  approves a device, and the two paths no longer overlap: an empty user
  code matches no pending device.
- POST /api/v1/auth/password, `keeporg passwd`, and a form on /web/vaults.
  Changing a password requires the current one, and the one-time code when
  a second factor is enrolled, and revokes that account's other sessions.
  Until now nothing could change a password, which made the advice printed
  at first start impossible to follow.
- keeporgd -reset-admin, for when that first password was not kept: a new
  one printed once, any second factor cleared, sessions revoked. It writes
  the state document directly and says to stop the server first.
- The client read stdin through a new buffered reader per prompt, so the
  first prompt swallowed the rest and anything asking twice could not be
  scripted. One shared reader now. A prompt for a password being set no
  longer reads $KEEPORG_PASSWORD either; that variable holds the password
  you sign in with, and taking it here would have set the new password to
  the old one.
- HANDOVER gains a first-sign-in section and an OIDC setup section covering
  the redirect URI, the config block, both entry points and how an identity
  becomes an account.

Proved: OIDC now has tests, against a stub provider that serves discovery,
authorization, token and userinfo. They cover the web sign-in that was
broken, device approval, linking an existing account by username, the
refusal when auto_create is off, and a forged callback state. Eight more
cover the password change. Checked by hand with the real binaries: the
first-start password logs once and works, -reset-admin replaces it while
the old one stops working, and `keeporg passwd` changes it from a pipe.
Server 0.5.0, desktop clients 0.3.0.
This commit is contained in:
2026-10-30 10:47:51 +01:00
parent 0848c0186c
commit 7eed722ff5
18 changed files with 828 additions and 28 deletions
+90 -6
View File
@@ -282,9 +282,64 @@ Three ways in, one kind of credential out: an opaque bearer token stored in
OIDC is implemented against the discovery document with the standard library
only. The server is a confidential client: it exchanges the authorization
code over TLS with the token endpoint and reads the identity from the
userinfo endpoint, so there is no local JWT verification to get wrong. An
identity links to an existing account by `preferred_username` or `email`, or
provisions one when `oidc.auto_create` is set.
userinfo endpoint, so there is no local JWT verification to get wrong.
### Setting up OIDC
At the identity provider, register a confidential client with one redirect
URI, which is `external_url` plus `/oidc/callback`:
```
https://keeporg.example.com/oidc/callback
```
Then put the issuer and the client credentials in `/etc/keeporg/server.json`
and restart:
```json
"oidc": {
"issuer": "https://id.example.com/realms/main",
"client_id": "keeporg",
"client_secret": "...",
"scopes": "openid profile email",
"auto_create": false,
"default_group_id": ""
}
```
`issuer` is the base URL that answers at
`<issuer>/.well-known/openid-configuration`; everything else is read from
there. All four fields above the blank ones are required — with any of
`issuer`, `client_id` or `client_secret` missing, single sign-on is simply
not offered.
Two entry points use it, and both end at the same callback:
- **A browser**, from the link on `/web`. `/oidc/login` with no parameters
starts a sign-in that ends in a session cookie.
- **A client**, from the device flow. `keeporg login -browser=sso` prints a
short code; the device page at `/device?code=…` offers the same link, this
time as `/oidc/login?code=<user code>`, and approving it releases the
token the waiting client is polling for.
How an identity becomes an account:
1. If some account is already linked to this issuer and `sub`, that one.
2. Otherwise an unlinked local account whose username equals
`preferred_username`, or whose email matches `email`, is linked to it.
This is how someone invited by link later moves to single sign-on.
3. Otherwise, with `auto_create` set, a new account is provisioned and put
in `default_group_id` when one is given. Without it, sign-in is refused
with "no account is linked to this identity; redeem an invite link
first", which is the setting to keep if membership should stay by
invitation.
An account created this way has no password, so it cannot sign in with one
and cannot set one through the password form.
The whole flow is covered by tests against a stub provider in
`internal/server/oidc_test.go`, which is the quickest way to see the
expected request sequence.
## The web interface
@@ -354,9 +409,38 @@ sudo systemctl enable --now keeporgd
sudo journalctl -u keeporgd # the first administrator's password is logged once
```
On first start with an empty state the server creates the `admin` account and
an `everyone` group, and logs the generated password once. Set
`KEEPORG_ADMIN_PASSWORD` to choose it instead.
### The first sign-in
On first start with an empty state the server creates the `admin` account
and an `everyone` group, and **logs the generated password once**:
```
keeporgd: created the first administrator: username "admin" password "13e4…"
(shown once, change it or enrol TOTP now)
```
Read it with `journalctl -u keeporgd`, or `podman logs <container>`. It is
never written anywhere else: the state file holds only a PBKDF2 verifier.
Set `KEEPORG_ADMIN_PASSWORD` before the first start to choose it instead, in
which case nothing is printed. The variable is read only when the server has
no accounts at all, so leaving it set afterwards does nothing.
Change it once you are in, with `keeporg passwd`, or the form at the bottom
of `/web/vaults`. A change requires the current password, and the one-time
code when a second factor is enrolled, and signs out that account's other
sessions.
**If the password was lost**, stop the server and run:
```sh
sudo -u keeporg keeporgd -data /var/lib/keeporg -reset-admin
```
It prints a new one, clears any second factor on that account, and revokes
its sessions. The server must be stopped: a running one holds the state in
memory and would write over the change. Nothing else is touched, and no
vault is affected — account passwords and vault passwords are different
things.
`/etc/keeporg/server.json`: