Identity & SSO
Operator guide for OIDC/SSO login and role-based access control.
Model in one paragraph
Section titled “Model in one paragraph”Two authentication schemes converge on one authorization path. Machines authenticate with an
API key (X-NuGet-ApiKey); interactive humans authenticate with OIDC over a session
cookie. Both resolve to the same Principal (subject + role), and every protected action is
checked by a single Authorization.Authorize(principal, scope). There are three fixed roles:
| Role | Scopes | Can |
|---|---|---|
| Reader | — | authenticate and browse the UI |
| Publisher | package:push, package:unlist | push and unlist/relist packages |
| Admin | admin + publisher scopes | everything, incl. user & key management |
NuGetKeep’s database is the source of truth for a user’s role — the IdP authenticates; NuGetKeep
authorizes. A user’s first OIDC login provisions a Reader (or Admin if bootstrap-listed); an
admin promotes others on the Users page (/admin/users).
Configuration (environment variables)
Section titled “Configuration (environment variables)”| Variable | Meaning | Default |
|---|---|---|
NUGETKEEP_OIDC_AUTHORITY | OIDC issuer / discovery base URL | unset → OIDC disabled |
NUGETKEEP_OIDC_CLIENT_ID | OIDC client id | unset → OIDC disabled |
NUGETKEEP_OIDC_CLIENT_SECRET | OIDC client secret (confidential code flow + PKCE) | unset |
NUGETKEEP_OIDC_SCOPES | extra scopes beyond openid profile email (space/comma list) | none |
NUGETKEEP_BOOTSTRAP_ADMINS | comma-list of emails/subjects auto-granted Admin on first login | empty |
OIDC is enabled only when both AUTHORITY and CLIENT_ID are set. When disabled, the server
runs API-key-only exactly as before: no /auth/login route, and unauthenticated hits on the
Web admin pages redirect home. (The existing API-key bootstrap — NUGETKEEP_BOOTSTRAP_APIKEY or the
once-logged generated key — still applies and is always an Admin key.)
IdP setup
Section titled “IdP setup”Register a confidential client at your IdP (Entra / Okta / Google / Keycloak / any OIDC-compliant provider) and add the redirect URIs:
- Sign-in callback:
https://<your-host>/signin-oidc - Sign-out callback:
https://<your-host>/signout-callback-oidc
Then set the env vars and set at least one NUGETKEEP_BOOTSTRAP_ADMINS entry (your email) so there
is a first administrator. Example:
NUGETKEEP_OIDC_AUTHORITY=https://login.microsoftonline.com/<tenant>/v2.0NUGETKEEP_OIDC_CLIENT_ID=<client-id>NUGETKEEP_OIDC_CLIENT_SECRET=<client-secret>NUGETKEEP_BOOTSTRAP_ADMINS=you@corp.comHow it behaves
Section titled “How it behaves”- Sign in / out: navbar Sign in →
/auth/login(OIDC challenge) → IdP → back. Sign out is an antiforgery-protectedPOST /auth/logoutthat clears both the local cookie and the IdP session. - First login provisions a
UserAccount(Reader, or Admin if bootstrap-listed) and records the login. An admin manages roles at/admin/users(promote/demote, disable). - Web admin pages (
/admin/api-keys,/admin/quarantine,/admin/audit,/admin/users) require the Admin role; the MCP setup page (/mcp-server) stays public but its enable/disable toggle is admin-only. - API keys are issued as a role (
{ "name": "...", "role": "Publisher" }); a key’s permissions are that role’s scopes. Keys created on older NuGetKeep versions are migrated automatically to the nearest role (admin → Admin, push/unlist → Publisher, otherwise Reader). - Last-admin protection: you cannot demote or disable the last remaining active Admin.
Safety invariants
Section titled “Safety invariants”- Unknown role strings are rejected (never silently defaulted).
- A disabled user authenticates at the IdP but is denied all protected actions.
- With OIDC disabled, existing API-key deployments boot and operate unchanged.
MCP role-based tool filtering
Section titled “MCP role-based tool filtering”An MCP session’s identity comes from its API key: send X-NuGet-ApiKey: <key> on the /mcp
requests and the server resolves the key to a role, then filters the tool surface to that role. No
key ⇒ anonymous ⇒ read-only.
| Role (api-key) | MCP tools |
|---|---|
| (none) / Reader | the 8 read tools (search_packages, get_package, list_versions, get_readme, assess_package, find_secure_version, package_health, get_dependency_graph) |
| Publisher | the read tools + request_publish |
| Admin | the read tools + request_publish + list_quarantined |
request_publish(Publisher/Admin) imports a package from upstream through the SAME supply-chain gate as a push — a vulnerable/disallowed package is quarantined, not served — and can optionally subscribe it for auto-import. It is the first write MCP tool.list_quarantined(Admin) lists the currently quarantined versions.- Filtering removes unauthorized tools from
tools/listand rejects an unauthorizedCallTool; the authoritative scope check still runs inside each Application handler (defense in depth). The named policies (mcp:publish,mcp:admin) assert the role confers the scope viaRolePermissions, so Admin (which holdspackage:push) also seesrequest_publish. - OIDC bearer for MCP remains out of scope; machine auth stays API-key-based.
Current limitations
Section titled “Current limitations”- Custom roles, per-feed permissions, and OIDC machine-to-machine bearer tokens for the API are not yet supported.