Skip to content

OIDC: refresh the account from the provider on every sign-in - #16

Merged
mastacontrola merged 1 commit into
mainfrom
oidc-profile-refresh
Aug 18, 2026
Merged

OIDC: refresh the account from the provider on every sign-in#16
mastacontrola merged 1 commit into
mainfrom
oidc-profile-refresh

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

Problem

The display name was written once, by _provisionUser(), and never again. Rename somebody in the directory and FOG keeps showing the name they had on the day they first signed in — forever, with nothing an admin can do short of editing the row by hand.

Reported against the lab Keycloak: first/last name corrected there to Tom Elliott, FOG kept rendering telliott Lab User on every subsequent login.

That is not what a directory-backed account means. The provider is the source of truth for who this is, and a value copied once starts drifting immediately.

Not a bug in LDAP

Worth stating since it's the obvious next question: the LDAP plugin already gets this right. ldap/hooks/ldappluginhook.hook.php:238-241 sets name, display, api and authsource on every login, new row or not. So this is the OIDC plugin catching up to the behaviour beside it, not a shared defect.

Two fields deliberately NOT refreshed

Field Why not
uName The username is what oidcIdentity is keyed against for accounts that predate their link, and renaming a FOG account out from under its history, tasks and audit rows is a migration, not a login side effect. A provider-side rename keeps working anyway — the binding is the subject.
uAuthSource Stamping it would take local password login away from an account an admin created, which is the opposite of break-glass. _provisionUser() stamps the rows this plugin made; those are the only ones that get it.

One real behaviour change

uAllowAPI is re-asserted from the provider, so the provider's setting now wins over a FOG-side edit at the next sign-in. It matches LDAP, and it means "can these accounts use the API" is answered in one place instead of drifting per user. Flagging it because someone who has hand-enabled API access on an OIDC account will see it revert.

Edge cases handled

  • An empty or absent name claim leaves the stored value alone rather than blanking it — Google omits it on some scope combinations, and ?? '' yields '' for a provider that sends "name": "", which is not the same as absent.
  • The save is conditional, so an unchanged sign-in costs no write and leaves no history row.

Gate

tests/oidc-profile-refresh.test.php, 7 assertions, comments stripped first (this file's own docblocks name every token asserted on).

Two that took a second attempt to get right:

  • Both call sites are pinned. _resolveUser() reaches an existing FOG row down two different branches — subject-linked, and link-on-first-sign-in. Refreshing on one but not the other is the original bug wearing a fix, and which branch you are on is invisible from the UI.
  • The fallback VALUE is pinned, not just its guard. Asserting only that if ('' === $display) exists passes a version that keeps the branch and assigns '' inside it — exactly the bug, with the guard still visible in the diff. That mutation survived the first version of this test.

Six mutations tried, six caught. sh tests/run-all.sh4 passed, 0 failed.

The display name was written once, by _provisionUser(), and never
again. Rename somebody in the directory and FOG kept showing the name
they had on the day they first signed in -- forever, with nothing an
admin could do about it short of editing the row by hand. Reported
against the lab Keycloak: first/last name corrected there, FOG kept
rendering the old value on every subsequent login.

That is not what a directory-backed account means. The provider is the
source of truth for who this is, and a value copied once starts
drifting immediately.

The LDAP plugin already gets this right -- ldappluginhook.hook.php
sets name, display, api and authsource on every login, new row or not
-- so this is the OIDC plugin catching up to the behaviour beside it
rather than a new idea.

Two fields are deliberately NOT refreshed:

- uName. The username is what oidcIdentity is keyed against for
  accounts that predate their link, and renaming a FOG account out
  from under its history, tasks and audit rows is a migration, not a
  login side effect. A provider-side rename keeps working anyway,
  because the binding is the subject.
- uAuthSource. Stamping it would take local password login away from
  an account an admin created, which is the opposite of break-glass.
  _provisionUser() stamps the rows this plugin made; those are the
  only ones that get it.

uAllowAPI IS re-asserted, and that is a real behaviour change worth
naming: the provider's setting now wins over a FOG-side edit at the
next sign-in. It matches LDAP, and it means "can these accounts use
the API" is answered in one place instead of drifting per user.

An empty or absent name claim leaves the stored value alone rather
than blanking it -- Google omits it on some scope combinations. The
save is conditional, so an unchanged sign-in costs no write and leaves
no history row.

tests/oidc-profile-refresh.test.php pins both call sites (refreshing
on the subject-linked path but not the name-linked one is the original
bug wearing a fix), the two fields that must not be written, the
fallback VALUE rather than merely the presence of its guard, and the
conditional save. Comments stripped first. Six mutations, six caught.

Closes part of #15's neighbourhood; single logout is separate.
@mastacontrola
mastacontrola merged commit c3fdf0d into main Aug 18, 2026
2 checks passed
@mastacontrola
mastacontrola deleted the oidc-profile-refresh branch August 18, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant