OIDC: honour Automatic onboarding on the API ID-token path (users must log into the UI once before bearer calls work)
Is your feature request related to a problem? Please describe.
With auth_mode: oidc_auth and Automatic onboarding enabled, an API request that carries a valid ID token from the configured provider (Authorization: Bearer <id_token>) is still answered with 401 for any user who has not yet logged into Harbor's own UI at least once.
Cause (v2.15.2, unchanged on main): src/server/middleware/security/idtoken.go → idToken.Generate verifies the token, then user.Ctl.GetBySubIss(sub, iss); on NotFound it returns nil and the request falls through to unauthorized. Onboarding exists only in the browser callback (src/core/controllers/oidc.go → Callback → userOnboard), so oidc_auto_onboard never applies to the API path. There is no other way to create an OIDC user either: POST /api/v2.0/users is refused outside db_auth.
Impact: every system that shares the IdP with Harbor and forwards the user's token (a VM/image portal in our case, using Dex cross-client audience) has to tell each user to "open Harbor once first". Earlier reports of the same wall: #13683, #14958, #16047, #16398; #17520 asks for a broader OIDC-based flow.
Describe the solution you'd like
When oidc_auto_onboard is enabled and the subject is unknown, the ID-token path onboards the user exactly like the callback does (username from oidc_user_claim with spaces replaced by _, e-mail, groups, a random CLI secret) and continues. With oidc_auto_onboard disabled nothing changes.
Describe the main design/architecture of your solution
Two details need care, both handled in the PR:
- CLI secrets.
pkg/oidc/secret.go→VerifySecretre-validates the stored OAuth2 token and refreshes it whenoauth2.Token.Valid()is false. The ID-token path has no refresh token, so the record stores the ID token as the access token with the ID token's ownexp; on later bearer requests the stored token is renewed when it holds no refresh token and is older than the presented one. Records created by the browser flow (which hold a refresh token) are never modified. - Insert conflicts. A conflict on
OnboardOIDCUseris either a concurrent first request from the same user (re-read by sub+iss and continue) or a stale row with the same username / e-mail under a different subject (for example after an IdP connector change); the latter is reported and left to an administrator, nothing half-created remains.
Cost: one extra oidc_user read per bearer request for the renew check; a write only when the stored token is older than the presented one, i.e. once per IdP login.
Describe the development plan you've considered
Done: a one-file change to idtoken.go plus idtoken_onboard_test.go (error-code matching through errors.Wrap, stored token shape, DB onboarding path incl. race and stale-username cases, runs under the package's existing Postgres-backed TestMain). Verified on a v2.15.2 deployment behind Dex (LDAP connector): a deleted user was recreated on the first API call from a peer client's ID token; an existing browser-onboarded record stayed unchanged. PR follows this issue. A short docs note for the OIDC page (website repo) can accompany it.
Additional context
Harbor v2.15.2, Dex 2.x as OIDC provider, oidc_user_claim: preferred_username, oidc_auto_onboard: true.
Source: goharbor/harbor