feat: admin service accounts via client_credentials grant#226
feat: admin service accounts via client_credentials grant#226eugenioenko wants to merge 1 commit into
Conversation
Adds is_admin_service_account flag on clients. When enabled on a confidential client with the client_credentials grant and autentico-admin in its audience, tokens issued via client_credentials satisfy the admin-API authorization check without a backing user or session. This replaces the "fake admin user + --enable-admin-password-grant" pattern for headless CI/CD automation. The flag is admin-gated at every write path, preserved across partial updates, rejected on public clients and clients missing the client_credentials grant, and logged at WARN with an audit entry whenever toggled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
||
| // Service-account elevation mints admin-API credentials. Always require an | ||
| // explicit confirmation before the form is submitted with the flag enabled. | ||
| const confirmServiceAccount = () => |
There was a problem hiding this comment.
We don't need this modal, we can just show a warning alert when toggle is enabled
| } | ||
| }, [client, open, form]); | ||
|
|
||
| const confirmServiceAccount = () => |
There was a problem hiding this comment.
We don't need this modal, just an alert with a warning. also lets reuse the alert from create form
| } | ||
| // Service-account elevation is equivalent to minting admin-API credentials. | ||
| // Log at WARN so leaks are traceable to the acting admin via audit + logs. | ||
| slog.Warn("client: admin service account created", |
There was a problem hiding this comment.
Do we need this slog warn? audit log when enabled will have the info
| if actor != nil { | ||
| actorID = actor.GetID() | ||
| } | ||
| slog.Warn("client: admin service account flag toggled", |
There was a problem hiding this comment.
Do we need this slog warn? audit log when enabled will have the info
| if newIsAdminServiceAccount { | ||
| effectiveClientType := c.ClientType | ||
| if effectiveClientType == "" { | ||
| effectiveClientType = "confidential" |
There was a problem hiding this comment.
If the configuration is invalid instead of fixing it silently we should fail
| azp := jwtutil.ExtractAzp(tokenString) | ||
| if azp != "" && azp == claims.UserID { | ||
| cli, cliErr := client.ClientByClientID(azp) | ||
| if cliErr == nil && cli != nil && cli.IsActive && cli.ClientType == "confidential" && cli.IsAdminServiceAccount { |
There was a problem hiding this comment.
This might get problematic: user table might have same id as clients table. and other issues that might happen here. More thoughts on this check are required
Summary
is_admin_service_accountflag on clients. When set on a confidential client with theclient_credentialsgrant andautentico-adminin its audience, tokens issued viaclient_credentialssatisfy the admin-API check without a backing user or session.--enable-admin-password-grant" pattern for headless CI/CD automation. Human admins still use the user-backed path; the two paths live side-by-side in the middleware.docs-web/src/content/docs/security/service-accounts.mdxfor setup, rotation, and leak response.Security properties
/oauth2/register,/admin/api/clients) — both wrapped inadminAPI(...)inpkg/cli/start.go.client_credentialsgrant, at both create and update time. A defense-in-depth check insideCreateClient/UpdateClientrejects the same combinations even if a caller bypasses the validator.WARNslog line and anaudit_logsentry (detail={"is_admin_service_account":"true"}) with the acting admin'sactor_idand request IP.subdoesn't match any user.client_secretover TLS).client_secret_jwt/private_key_jwtnot introduced in this PR.Test plan
go test ./...— all pass, including new unit + e2e coveragemake lint— 0 issuesadmin-uibuilds (TypeScript + Vite)TestAdminAuthMiddleware_ServiceAccount_{Accepted,FlagMissing,InactiveClient,WrongAudience}TestValidateClientCreateRequest_AdminServiceAccount(five scenarios including default client_type)TestUpdateClient_{PreservesAdminServiceAccountFlag,CannotFlagPublicClient,FlagRequiresClientCredentialsGrant}TestAdminServiceAccount_{FullFlow,WithoutFlag_Rejected,PublicClient_Rejected}TestMigrate_RunSucceedsrewritten to use a bare DB rather than a pre-migrated one —ALTER TABLE ADD COLUMNis not idempotent in SQLite, andCLAUDE.md's claim of idempotent migration replay was incidental, not a design guarantee)Docs
README.md— admin-API auth noteCLAUDE.md— new "Admin API Authentication" subsection in the architecture docdocs-web/.../security/service-accounts.mdx— new page (setup, validation, audit, rotation, leak response, comparison with ROPC)rfc/rfc.md— implementation-defined extension under RFC 6749 §4.4make generate-docsNot in this PR
client_secret_jwt/private_key_jwtclient authentication🤖 Generated with Claude Code