oidc: accept both the app's public client and the server's confidential one
server-test / test (push) Successful in 37s

Explaining public vs confidential clients surfaced a gap in my own design: I had
assumed a single client id, but there are two clients here with genuinely
different properties.

  the Android app     public + PKCE, because an APK cannot keep a secret
  the admin UI        confidential, because the server can keep one in
                      /etc/echolot-server.env and weakening it to public buys
                      nothing

So the audience check now accepts either registered client id - and only those
two. "Any client of this issuer" would let every other application registered
with the same IdP authenticate here, which is the entire reason the check
exists. Either id alone is enough to enable sign-in, since an operator may
register only the app or only the admin UI.

The profile advertises the *app's* client id, since that is what a phone should
authorize as.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 17:31:33 +02:00
co-authored by Claude Fable 5
parent 89a5ff9139
commit 80d2092f1b
5 changed files with 88 additions and 17 deletions
+6 -4
View File
@@ -69,9 +69,10 @@ type Config struct {
// Identity provider. Empty issuer disables sign-in entirely; the server is a relying
// party and never stores passwords.
OIDCIssuer string // ECHOLOT_OIDC_ISSUER / --oidc-issuer
OIDCClientID string // ECHOLOT_OIDC_CLIENT_ID / --oidc-client-id
OIDCAdminGroup string // ECHOLOT_OIDC_ADMIN_GROUP / --oidc-admin-group
OIDCIssuer string // ECHOLOT_OIDC_ISSUER / --oidc-issuer
OIDCClientID string // ECHOLOT_OIDC_CLIENT_ID / --oidc-client-id (confidential, admin UI)
OIDCAppClientID string // ECHOLOT_OIDC_APP_CLIENT_ID / --oidc-app-client-id (public, the phone app)
OIDCAdminGroup string // ECHOLOT_OIDC_ADMIN_GROUP / --oidc-admin-group
// Break-glass admin username; the password lives hashed in the state store.
AdminUser string // ECHOLOT_ADMIN_USER / --admin-user
@@ -125,7 +126,8 @@ func Load(args []string) (*Config, *Actions, error) {
fs.IntVar(&c.UploadMaxRuns, "upload-max-runs", envInt("UPLOAD_MAX_RUNS", 200), "keep at most this many runs per device; 0 disables")
fs.StringVar(&c.UploadMinAnon, "upload-min-anonymization", envOr("UPLOAD_MIN_ANONYMIZATION", "full"), "least anonymization accepted: full|balanced|strict")
fs.StringVar(&c.OIDCIssuer, "oidc-issuer", envOr("OIDC_ISSUER", ""), "OpenID Connect issuer URL; empty disables sign-in")
fs.StringVar(&c.OIDCClientID, "oidc-client-id", envOr("OIDC_CLIENT_ID", ""), "OpenID Connect client id for this server")
fs.StringVar(&c.OIDCClientID, "oidc-client-id", envOr("OIDC_CLIENT_ID", ""), "confidential OIDC client id for the admin UI")
fs.StringVar(&c.OIDCAppClientID, "oidc-app-client-id", envOr("OIDC_APP_CLIENT_ID", ""), "public OIDC client id used by the Android app (PKCE)")
fs.StringVar(&c.OIDCAdminGroup, "oidc-admin-group", envOr("OIDC_ADMIN_GROUP", ""), "group claim required for admin access; empty means nobody is an admin via OIDC")
fs.StringVar(&c.PublicControlURL, "public-url", envOr("PUBLIC_URL", ""), "public control-plane URL for enrollment links, e.g. https://probe.example.net:8443")
fs.StringVar(&c.MinAppVersion, "min-app-version", envOr("MIN_APP_VERSION", "0.2.0"), "oldest app version this server will serve (SemVer, inclusive)")