fix(authserver): map deliberate provisioning refusal to access_denied - #6441
Conversation
CreateUser can return storage.ErrUserNotProvisioned to signal a deployment that provisions users out-of-band (e.g. SCIM) is deliberately refusing to auto-create an unbound identity, rather than hitting an internal failure. The callback handler now maps that specific error to fosite.ErrAccessDenied instead of folding it into the generic server_error response every other resolution failure still gets.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6441 +/- ##
==========================================
- Coverage 78.00% 77.95% -0.06%
==========================================
Files 766 766
Lines 74069 74072 +3
==========================================
- Hits 57780 57744 -36
- Misses 16284 16323 +39
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jhrozek
left a comment
There was a problem hiding this comment.
Reviewed via automated multi-agent pass (security + Go code quality). Small, well-scoped change: storage.ErrUserNotProvisioned is correctly propagated through ResolveUser → createUserWithIdentity (wrapped with %w) and mapped to access_denied only on that specific path, leaving every other resolution failure as server_error. No information-disclosure risk (the branch only fires after a successful upstream token exchange, so the subject is attacker-proven, not attacker-supplied) and no auth/authz bypass. Both the new path and the server_error regression are covered by tests. LGTM.
Summary
Under a deployment where user accounts are provisioned out-of-band (e.g. SCIM) rather than auto-created on first login, an unprovisioned upstream identity's OAuth login is correctly denied — but the denial currently surfaces to the client as
error=server_error, noterror=access_denied.server_erroris semantically wrong (nothing failed internally) and OAuth clients treat it as a retryable/transient failure rather than a definitive denial.Root cause:
CallbackHandler'sResolveUsererror branch inpkg/authserver/server/handlers/callback.gomaps every resolution failure — including a storage layer's deliberate refusal to provision — to the same genericfosite.ErrServerError.storage.ErrUserNotProvisionedthat aUserStorage.CreateUserimplementation can return to mean "deliberately not provisioned, deny the login" instead of raising an internal error.CallbackHandlerto checkerrors.Is(err, storage.ErrUserNotProvisioned)and respond withfosite.ErrAccessDeniedin that case; every other resolution error keeps mapping tofosite.ErrServerError, unchanged.UserStorage.CreateUserdoc comment to document the new contract.Type of change
Test plan
task test)task test-e2e)task lint-fix)Added two new tests in
callback_test.go:TestCallbackHandler_UserResolutionFailure_UserNotProvisioned_DeniesAccess— assertserror=access_deniedwhenCreateUserreturnsstorage.ErrUserNotProvisioned.TestCallbackHandler_UserResolutionFailure_OtherError_ServerError— asserts the existingerror=server_errorbehavior is unchanged for unrelatedCreateUserfailures.Ran
go build ./pkg/authserver/...,go test ./pkg/authserver/...,go vet ./pkg/authserver/...,gofmt -l, andgolangci-lint runagainst the changed packages — all pass.Does this introduce a user-facing change?
Yes: an unprovisioned identity's login attempt (in deployments that provision users out-of-band and opt into returning
storage.ErrUserNotProvisioned) now redirects witherror=access_deniedinstead oferror=server_error. No change for deployments whoseUserStorage.CreateUsernever returns this sentinel.Special notes for reviewers
This is additive/opt-in: a
UserStorageimplementation must explicitly return the new sentinel to get the new behavior. Existing implementations that just auto-provision onCreateUserare unaffected.