{"_id":"@edyd/verdaccio-auth-oidc","_rev":"5-4c14c71f8a5985a3dd7d69d47c749d88","name":"@edyd/verdaccio-auth-oidc","dist-tags":{"latest":"1.3.0"},"versions":{"1.0.0":{"name":"@edyd/verdaccio-auth-oidc","version":"1.0.0","license":"MIT","_id":"@edyd/verdaccio-auth-oidc@1.0.0","maintainers":[{"name":"edydeleon","email":"edy@familydeleon.com"}],"homepage":"https://gitlab.com/edydeleon/verdaccio-plugins/tree/main/plugins/verdaccio-auth-oidc","bugs":{"url":"https://gitlab.com/edydeleon/verdaccio-plugins/-/issues"},"bin":{"verdaccio-revoke-tokens":"build/cli/revoke-tokens.mjs"},"dist":{"shasum":"61ce25fa574524decc412ee0d831baf63fd2d4d0","tarball":"https://registry.npmjs.org/@edyd/verdaccio-auth-oidc/-/verdaccio-auth-oidc-1.0.0.tgz","fileCount":50,"integrity":"sha512-GQBv0f9lo6Z1Wnfp97JABSHtymoMeY+3+KUfTyhdmPlR/r1rGFFKLUuRZi0xOfcHlNsQphA5/71BoNPm4uHnOQ==","signatures":[{"sig":"MEUCIQDJ3/ZEYXcqeBi2U7gYnWDF79MGVJbqXXPtfU+s+x2MWAIgUfCv7ksfrlNawpJcT3iOYtsowoBsrmOtGcDmtzIgnQc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":626447},"main":"build/index.js","types":"build/index.d.ts","module":"build/index.mjs","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./build/index.d.ts","default":"./build/index.mjs"},"require":{"types":"./build/index.d.ts","default":"./build/index.js"}}},"gitHead":"7cd7bee0a3602567fb30a31fba33a4d247c2483a","scripts":{"test":"vitest run","build":"vite build","clean":"rimraf ./build","watch":"vite build --watch","test:e2e":"vitest run e2e/e2e.spec.ts","get-token":"node tools/get-token.mjs","start:local":"node e2e/start-local.mjs","start:google":"node e2e/start-local.mjs --provider google","test:pentest":"vitest run test/pentest.spec.ts","test:security":"vitest run test/security.spec.ts","prepublishOnly":"pnpm build"},"_npmUser":{"name":"edydeleon","email":"edy@familydeleon.com"},"repository":{"url":"git+https://gitlab.com/edydeleon/verdaccio-plugins.git","type":"git","directory":"plugins/verdaccio-auth-oidc"},"_npmVersion":"10.8.2","description":"Verdaccio auth plugin for generic OIDC/OAuth2 JWT verification","directories":{},"_nodeVersion":"20.20.2","dependencies":{"jose":"6.0.0","debug":"4.4.0","@verdaccio/core":"8.1.0","proper-lockfile":"^4.1.2"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.0.0","rimraf":"6.1.0","vitest":"4.1.0","express":"5.0.0","supertest":"7.1.0","typescript":"5.9.3","@types/node":"22.0.0","@types/debug":"4.1.0","@types/express":"5.0.0","vite-plugin-dts":"4.5.0","@types/supertest":"^7.2.0","@verdaccio/types":"13.0.0","@verdaccio/config":"8.1.0","@types/jsonwebtoken":"^9.0.10","@types/proper-lockfile":"^4.1.4"},"_npmOperationalInternal":{"tmp":"tmp/verdaccio-auth-oidc_1.0.0_1779252617804_0.7463724826874811","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@edyd/verdaccio-auth-oidc","version":"1.1.0","keywords":["verdaccio","verdaccio-plugin","oidc","oauth2","jwt","auth","npm-registry"],"license":"MIT","_id":"@edyd/verdaccio-auth-oidc@1.1.0","maintainers":[{"name":"edydeleon","email":"edy@familydeleon.com"}],"homepage":"https://gitlab.com/edydeleon/verdaccio-plugins/tree/main/plugins/verdaccio-auth-oidc","bugs":{"url":"https://gitlab.com/edydeleon/verdaccio-plugins/-/issues"},"bin":{"verdaccio-revoke-tokens":"build/cli/revoke-tokens.mjs"},"dist":{"shasum":"b7dc9d34e95ef78cc45a6a3975503001ec6458d1","tarball":"https://registry.npmjs.org/@edyd/verdaccio-auth-oidc/-/verdaccio-auth-oidc-1.1.0.tgz","fileCount":49,"integrity":"sha512-LkeTQKHvWDUkKVMsYAbdmXBYmNSiBaGVzUCXvsDhQAmBLdrS+gUCwXxGtgXRj8ULXYMEmG1YKDKmhR+vJ5T0aQ==","signatures":[{"sig":"MEUCIQC2wv2hJFqY2A2enRsEV3plaTdT87rLJo2BsiwdaYhJpwIgYmR1Mf8Csflhf5dRtN+0krEKRwDKF23+G9AA9Y3eB7A=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":621734},"main":"build/index.js","types":"build/index.d.ts","module":"build/index.mjs","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./build/index.d.ts","default":"./build/index.mjs"},"require":{"types":"./build/index.d.ts","default":"./build/index.js"}},"./cli/revoke-tokens":{"import":"./build/cli/revoke-tokens.mjs"}},"gitHead":"9f174892b9490e17300c00051e290f7e19af6ed4","scripts":{"test":"vitest run","build":"vite build","clean":"rimraf ./build","watch":"vite build --watch","test:e2e":"vitest run e2e/e2e.spec.ts","get-token":"node tools/get-token.mjs","typecheck":"tsc --noEmit","start:local":"node e2e/start-local.mjs","start:google":"node e2e/start-local.mjs --provider google","test:pentest":"vitest run test/pentest.spec.ts","test:security":"vitest run test/security.spec.ts","prepublishOnly":"pnpm build"},"_npmUser":{"name":"edydeleon","email":"edy@familydeleon.com"},"repository":{"url":"git+https://gitlab.com/edydeleon/verdaccio-plugins.git","type":"git","directory":"plugins/verdaccio-auth-oidc"},"_npmVersion":"11.11.0","description":"Verdaccio auth plugin for generic OIDC/OAuth2 JWT verification","directories":{},"_nodeVersion":"24.14.1","dependencies":{"jose":"^6.0.0","debug":"^4.4.0","@verdaccio/core":"^8.1.0","proper-lockfile":"^4.1.2"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.0.0","rimraf":"6.1.0","vitest":"4.1.0","express":"5.0.0","supertest":"7.1.0","typescript":"5.9.3","@types/node":"22.0.0","@types/debug":"4.1.0","@types/express":"5.0.0","vite-plugin-dts":"4.5.0","@types/supertest":"^7.2.0","@verdaccio/types":"13.0.0","@verdaccio/config":"8.1.0","@types/jsonwebtoken":"^9.0.10","@types/proper-lockfile":"^4.1.4"},"_npmOperationalInternal":{"tmp":"tmp/verdaccio-auth-oidc_1.1.0_1779254601635_0.12570651894434337","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@edyd/verdaccio-auth-oidc","version":"1.1.1","keywords":["verdaccio","verdaccio-plugin","oidc","oauth2","jwt","auth","npm-registry"],"license":"MIT","_id":"@edyd/verdaccio-auth-oidc@1.1.1","maintainers":[{"name":"edydeleon","email":"edy@familydeleon.com"}],"homepage":"https://gitlab.com/edydeleon/verdaccio-plugins/tree/main/plugins/verdaccio-auth-oidc","bugs":{"url":"https://gitlab.com/edydeleon/verdaccio-plugins/-/issues"},"bin":{"verdaccio-revoke-tokens":"build/cli/revoke-tokens.mjs"},"dist":{"shasum":"491c9deba2c011bd489383694159d568bf9ce8e4","tarball":"https://registry.npmjs.org/@edyd/verdaccio-auth-oidc/-/verdaccio-auth-oidc-1.1.1.tgz","fileCount":50,"integrity":"sha512-9dQyi/bQExxSCCvlcLhsmY5IDt5Wx8vBjE+1NBloHipY+qddXaG/KzAC1aFfUbjzMC+H58yrTl9Cj19x8L5yrw==","signatures":[{"sig":"MEUCIDFfxR6mj0YIyQF4rGEGW2A4pXQK8M5BYz+QN7KrfvseAiEA+VcWiRY8oO9eM5J6QhSE/ZzGnteFQqKUVE3dI8Qml4c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":631265},"main":"build/index.js","types":"build/index.d.ts","module":"build/index.mjs","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./build/index.d.ts","default":"./build/index.mjs"},"require":{"types":"./build/index.d.ts","default":"./build/index.js"}},"./cli/revoke-tokens":{"import":"./build/cli/revoke-tokens.mjs"}},"gitHead":"88671c25a6f52424fc1d837ef99cd39900ba98bd","scripts":{"test":"vitest run","build":"vite build","clean":"rimraf ./build","watch":"vite build --watch","test:e2e":"vitest run e2e/e2e.spec.ts","get-token":"node tools/get-token.mjs","typecheck":"tsc --noEmit","start:local":"node e2e/start-local.mjs","start:google":"node e2e/start-local.mjs --provider google","test:pentest":"vitest run test/pentest.spec.ts","test:security":"vitest run test/security.spec.ts","prepublishOnly":"pnpm build"},"_npmUser":{"name":"GitLab CI/CD","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"gitlab","oidcConfigId":"oidc:87002f6f-8d33-4891-a350-06e234aecc37"}},"repository":{"url":"git+https://gitlab.com/edydeleon/verdaccio-plugins.git","type":"git","directory":"plugins/verdaccio-auth-oidc"},"_npmVersion":"11.12.1","description":"Verdaccio auth plugin for generic OIDC/OAuth2 JWT verification","directories":{},"_nodeVersion":"24.15.0","dependencies":{"jose":"^6.0.0","debug":"^4.4.0","@verdaccio/core":"^8.1.0","proper-lockfile":"^4.1.2"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.0.0","rimraf":"6.1.0","vitest":"4.1.0","express":"5.0.0","supertest":"7.1.0","typescript":"5.9.3","@types/node":"22.0.0","@types/debug":"4.1.0","@types/express":"5.0.0","vite-plugin-dts":"4.5.0","@types/supertest":"^7.2.0","@verdaccio/types":"13.0.0","@verdaccio/config":"8.1.0","@types/jsonwebtoken":"^9.0.10","@types/proper-lockfile":"^4.1.4"},"_npmOperationalInternal":{"tmp":"tmp/verdaccio-auth-oidc_1.1.1_1779255377730_0.7408096074867514","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@edyd/verdaccio-auth-oidc","version":"1.2.0","keywords":["verdaccio","verdaccio-plugin","oidc","oauth2","jwt","auth","npm-registry"],"license":"MIT","_id":"@edyd/verdaccio-auth-oidc@1.2.0","maintainers":[{"name":"edydeleon","email":"edy@familydeleon.com"}],"homepage":"https://gitlab.com/edydeleon/verdaccio-plugins/tree/main/plugins/verdaccio-auth-oidc","bugs":{"url":"https://gitlab.com/edydeleon/verdaccio-plugins/-/issues"},"bin":{"verdaccio-revoke-tokens":"build/cli/revoke-tokens.mjs"},"dist":{"shasum":"963d7bdf7c1530e37d9556be220bc2b94790f615","tarball":"https://registry.npmjs.org/@edyd/verdaccio-auth-oidc/-/verdaccio-auth-oidc-1.2.0.tgz","fileCount":55,"integrity":"sha512-xe88YPgU+wa7ZBXlsVb3/5x/XpXQPM97AoriO1ZeAt2K/+6pbjEZ1ygZ7WhgA01kEgL4JMMdvYMI5fIMJAbAMA==","signatures":[{"sig":"MEUCIQDTFbUu4WMEIWDUAUAWWZ8iICGUduuVk6s5qeX8X7thAQIgU/l8eK2OqcA5BMpN3JOXGmV5Z2bFSmgI6zTAtfqjhPw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@edyd%2fverdaccio-auth-oidc@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v0.2"}},"unpackedSize":742191},"main":"build/index.js","types":"build/index.d.ts","module":"build/index.mjs","engines":{"node":">=20"},"exports":{".":{"import":{"types":"./build/index.d.ts","default":"./build/index.mjs"},"require":{"types":"./build/index.d.ts","default":"./build/index.js"}},"./cli/revoke-tokens":{"import":"./build/cli/revoke-tokens.mjs"}},"gitHead":"442e6abdd9b445f0d753c039da95accd75d3a8cd","scripts":{"test":"vitest run","build":"vite build","clean":"rimraf ./build","watch":"vite build --watch","test:e2e":"vitest run e2e/e2e.spec.ts","get-token":"node tools/get-token.mjs","typecheck":"tsc --noEmit","start:local":"node e2e/start-local.mjs","start:google":"node e2e/start-local.mjs --provider google","test:pentest":"vitest run test/pentest.spec.ts","test:security":"vitest run test/security.spec.ts","prepublishOnly":"pnpm build"},"_npmUser":{"name":"GitLab CI/CD","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"gitlab","oidcConfigId":"oidc:87002f6f-8d33-4891-a350-06e234aecc37"}},"repository":{"url":"git+https://gitlab.com/edydeleon/verdaccio-plugins.git","type":"git","directory":"plugins/verdaccio-auth-oidc"},"_npmVersion":"11.13.0","description":"Verdaccio auth plugin for generic OIDC/OAuth2 JWT verification","directories":{},"_nodeVersion":"24.16.0","dependencies":{"jose":"^6.0.0","debug":"^4.4.0","@verdaccio/core":"^8.1.0","proper-lockfile":"^4.1.2"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.0.0","rimraf":"6.1.0","vitest":"4.1.0","express":"5.0.0","supertest":"7.1.0","typescript":"5.9.3","@types/node":"22.0.0","@types/debug":"4.1.0","@types/express":"5.0.0","vite-plugin-dts":"4.5.0","@types/supertest":"^7.2.0","@verdaccio/types":"13.0.0","@verdaccio/config":"8.1.0","@types/jsonwebtoken":"^9.0.10","@types/proper-lockfile":"^4.1.4"},"_npmOperationalInternal":{"tmp":"tmp/verdaccio-auth-oidc_1.2.0_1780423693890_0.1213039009619119","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@edyd/verdaccio-auth-oidc","version":"1.3.0","description":"Verdaccio auth plugin for generic OIDC/OAuth2 JWT verification","license":"MIT","repository":{"type":"git","url":"git+https://gitlab.com/edydeleon/verdaccio-plugins.git","directory":"plugins/verdaccio-auth-oidc"},"homepage":"https://gitlab.com/edydeleon/verdaccio-plugins/tree/main/plugins/verdaccio-auth-oidc","bugs":{"url":"https://gitlab.com/edydeleon/verdaccio-plugins/-/issues"},"main":"build/index.js","module":"build/index.mjs","types":"build/index.d.ts","exports":{".":{"import":{"types":"./build/index.d.ts","default":"./build/index.mjs"},"require":{"types":"./build/index.d.ts","default":"./build/index.js"}},"./cli/revoke-tokens":{"import":"./build/cli/revoke-tokens.mjs"}},"bin":{"verdaccio-revoke-tokens":"build/cli/revoke-tokens.mjs"},"engines":{"node":">=20"},"keywords":["verdaccio","verdaccio-plugin","oidc","oauth2","jwt","auth","npm-registry"],"dependencies":{"@verdaccio/core":"^8.1.0","debug":"^4.4.0","jose":"^6.0.0","proper-lockfile":"^4.1.2"},"devDependencies":{"@types/debug":"4.1.0","@types/express":"5.0.0","@types/jsonwebtoken":"^9.0.10","@types/node":"22.0.0","@types/proper-lockfile":"^4.1.4","@types/supertest":"^7.2.0","@verdaccio/config":"8.1.0","@verdaccio/types":"13.0.0","express":"5.0.0","rimraf":"6.1.0","supertest":"7.1.0","typescript":"5.9.3","vite":"8.0.0","vite-plugin-dts":"4.5.0","vitest":"4.1.0"},"scripts":{"clean":"rimraf ./build","prepublishOnly":"pnpm build","build":"vite build","watch":"vite build --watch","typecheck":"tsc --noEmit","test":"vitest run","test:security":"vitest run test/security.spec.ts","test:pentest":"vitest run test/pentest.spec.ts","test:e2e":"vitest run e2e/e2e.spec.ts","get-token":"node tools/get-token.mjs","start:local":"node e2e/start-local.mjs","start:google":"node e2e/start-local.mjs --provider google"},"gitHead":"09e38d918b2d35c03847b71bff897c7e1f2aa1b9","_id":"@edyd/verdaccio-auth-oidc@1.3.0","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-W84xkzuxNKvj1l1P+1Ju/hMJ99V2WlcGeK0Kc+HOvB+sfYwguxIeGmKxxuZ8wxIZhdXgVExmLxHLw6T6RkXXxw==","shasum":"aa4d1c78c3769fa7c7db8cb8b6c324c7ae10b307","tarball":"https://registry.npmjs.org/@edyd/verdaccio-auth-oidc/-/verdaccio-auth-oidc-1.3.0.tgz","fileCount":95,"unpackedSize":1115510,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@edyd%2fverdaccio-auth-oidc@1.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v0.2"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGpU++/bT9s+9XOjLsRvq62d9bc/TapChGc0NGgvJUKGAiBkaSEBTFym7mCVb033x8v2iVDu7+EpXyC5vcEa4fyB9A=="}]},"_npmUser":{"name":"GitLab CI/CD","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"gitlab","oidcConfigId":"oidc:87002f6f-8d33-4891-a350-06e234aecc37"}},"directories":{},"maintainers":[{"name":"edydeleon","email":"edy@familydeleon.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/verdaccio-auth-oidc_1.3.0_1780578620185_0.3614544348548394"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-20T04:50:17.644Z","modified":"2026-06-04T13:10:20.697Z","1.0.0":"2026-05-20T04:50:17.967Z","1.1.0":"2026-05-20T05:23:21.804Z","1.1.1":"2026-05-20T05:36:17.895Z","1.2.0":"2026-06-02T18:08:14.040Z","1.3.0":"2026-06-04T13:10:20.389Z"},"bugs":{"url":"https://gitlab.com/edydeleon/verdaccio-plugins/-/issues"},"license":"MIT","homepage":"https://gitlab.com/edydeleon/verdaccio-plugins/tree/main/plugins/verdaccio-auth-oidc","keywords":["verdaccio","verdaccio-plugin","oidc","oauth2","jwt","auth","npm-registry"],"repository":{"type":"git","url":"git+https://gitlab.com/edydeleon/verdaccio-plugins.git","directory":"plugins/verdaccio-auth-oidc"},"description":"Verdaccio auth plugin for generic OIDC/OAuth2 JWT verification","maintainers":[{"name":"edydeleon","email":"edy@familydeleon.com"}],"readme":"# verdaccio-auth-oidc\n\nA Verdaccio auth plugin that verifies JWTs from any OpenID Connect provider\n(Google, Azure AD/Entra, Okta, Keycloak, Auth0). Unauthenticated users are\nalways rejected — there is no fallback or \"allow if token is missing\" behavior.\n\n## Features\n\n### Authentication\n\n- OIDC JWT verification via JWKS (auto-discovered from issuer)\n- Browser-based PKCE login flow with SPA support\n- Configurable username claim (`email`, `sub`, `preferred_username`)\n- Email domain restriction (`allowed_domains`) with `email_verified` enforcement\n- Group-based access control via IdP JWT claims (`group_claim` + `allowed_groups`)\n- Step-up authentication for sensitive operations (configurable `step_up_max_age`)\n- JWKS key caching with configurable TTL\n\n### API tokens\n\n- Long-lived `vrd_`-prefixed tokens for CI/CD and automation\n- Stored as SHA-256 hashes (raw token never persisted)\n- Configurable TTL (per-token and global max), max tokens per user\n- Token creation allowlist (by user or group; admins always bypass)\n- Optional least-privilege scope per token: read-only or publish limited to\n  package patterns (enforced server-side; reads never restricted)\n- Deny-list kill switch blocks an identity from all auth and revokes its tokens\n- Groups snapshotted at creation for consistent authorization\n- Token epoch counter detects backup-restore and rejects stale tokens\n- CLI break-glass tool (`verdaccio-revoke-tokens`) for offline revocation\n\n### Package access control\n\n- Dynamic per-package/per-scope permissions (access, publish, unpublish)\n- Additive-only model — dynamic rules extend YAML config, never restrict it\n- Pattern matching: exact name, `@scope/*`, trailing wildcard, `**` catch-all\n- HMAC-SHA256 tamper detection with separate key file\n- Monotonic epoch for rollback protection\n- Fail-closed on corrupt or unreadable store\n- Reserved principals (`$all`, `$anonymous`, `$authenticated`) blocked from dynamic add\n- Configurable limits (`max_patterns`, `max_entries_per_action`)\n\n### Admin\n\n- Admin role via config (`admin_users`, `admin_groups`) and dynamic ACL\n- List/revoke any user's tokens; cascade revocation on ACL removal\n- Manage package access rules (add/remove users, groups, patterns)\n- Uplink health dashboard\n- Audit log (token, ACL, and package access events; ring buffer, 200 entries per store)\n- Optional external audit sink (append-only file / syslog / webhook) for durable, compliance-grade records\n- Optional Prometheus metrics endpoint (aggregate counters; opt-in, auth-gated)\n\n### Security\n\n- HTTPS enforced on all mutation endpoints (bypassed only in `dev_mode`)\n- Timing-safe token comparison (`timingSafeEqual`)\n- Token store files written with mode `0600`; startup warning if world-readable\n- Admin endpoints return 404 (not 403) to prevent enumeration\n- Generic error messages on auth failure (no config leakage)\n- Corrupt store quarantined, not auto-reset\n- Rate limiting / brute-force guard: per-IP throttle on sensitive endpoints and\n  per-username lockout after repeated failed logins (default on)\n\n### Operational\n\n- Single-node file-based storage with `proper-lockfile`\n- Onboarding page at `/-/oidc/setup` (works without theme plugin)\n- User-facing permissions endpoint (`/-/oidc/me/permissions`, `/-/oidc/me/pkg-access`)\n- Optional download statistics (per-package totals + daily history; opt-in, durable, bounded)\n\n## How it works\n\nThe npm CLI sends credentials as `username:password` via HTTP Basic Auth.\nThis plugin treats the **password field as an OIDC JWT** and:\n\n1. Fetches the provider's public keys from `/.well-known/jwks.json`\n2. Verifies the token signature, expiry, issuer, and optionally audience\n3. Extracts the username from a configurable claim (`email`, `sub`, or\n   `preferred_username`)\n4. Optionally enforces email domain restrictions and group membership\n\n## Installation\n\n```bash\n# Copy to Verdaccio's plugin directory\nmkdir -p /path/to/verdaccio/plugins/verdaccio-auth-oidc\ncp -r build/* /path/to/verdaccio/plugins/verdaccio-auth-oidc/\ncp package.json /path/to/verdaccio/plugins/verdaccio-auth-oidc/\n\n# Install runtime dependencies\ncd /path/to/verdaccio/plugins/verdaccio-auth-oidc\nnpm install --omit=dev --ignore-scripts\n\n# Restart Verdaccio\n```\n\n> **Version compatibility:** install the same version of\n> `@edyd/verdaccio-auth-oidc` and `@edyd/verdaccio-theme-oidc`. They are released\n> in lockstep; the theme's admin UI calls auth-plugin endpoints, so mismatched\n> versions can break the admin views.\n\n## Configuration\n\nAdd to your Verdaccio `config.yaml`:\n\n```yaml\nplugins: /path/to/verdaccio/plugins\n\nauth:\n  auth-oidc:\n    issuer: 'https://accounts.google.com'\n    audience: 'your-client-id' # optional\n    allowed_domains: # optional — restrict by email domain\n      - mycompany.com # exact match\n      - '*.mycompany.com' # wildcard — any subdomain (not the bare apex)\n    group_claim: groups # optional — JWT claim containing group array (dot-path ok, e.g. realm_access.roles)\n    allowed_groups: # optional — require membership in at least one\n      - npm-publishers\n    username_claim: email # email | sub | preferred_username (default: email)\n    jwks_cache_ttl: 3600 # JWKS cache lifetime in seconds (default: 3600)\n    metrics: # optional — Prometheus metrics endpoint (disabled by default)\n      enabled: true\n      token: 'a-long-random-scrape-secret' # optional static bearer for scrapers\n```\n\n| Option            | Required | Default | Description                                                                                      |\n| ----------------- | -------- | ------- | ------------------------------------------------------------------------------------------------ |\n| `issuer`          | Yes      | —       | OIDC issuer URL (must serve `/.well-known/jwks.json`)                                            |\n| `audience`        | No       | —       | Expected `aud` claim value                                                                       |\n| `allowed_domains` | No       | —       | Allowed email domains. Exact (`co.com`) or wildcard (`*.co.com` = any subdomain, excludes apex)  |\n| `group_claim`     | No       | —       | JWT claim with the user's groups (array). Dot-paths for nested claims, e.g. `realm_access.roles` |\n| `allowed_groups`  | No       | —       | Require membership in at least one of these groups (requires `group_claim`)                      |\n| `username_claim`  | No       | `email` | Which JWT claim to use as the Verdaccio username                                                 |\n| `jwks_cache_ttl`  | No       | `3600`  | How long to cache JWKS keys (seconds)                                                            |\n| `metrics`         | No       | —       | Prometheus metrics endpoint (`enabled`, optional `token`); see Metrics below                     |\n\n### Provider examples\n\n#### Google\n\n```yaml\nauth:\n  auth-oidc:\n    issuer: 'https://accounts.google.com'\n    audience: '123456789.apps.googleusercontent.com'\n    allowed_domains: ['mycompany.com']\n    username_claim: email\n```\n\n#### Azure AD / Entra\n\n```yaml\nauth:\n  auth-oidc:\n    issuer: 'https://login.microsoftonline.com/{tenant-id}/v2.0'\n    audience: 'api://my-verdaccio-app'\n    group_claim: groups\n    allowed_groups: ['npm-publishers-group-id']\n    username_claim: preferred_username\n```\n\n#### Okta\n\n```yaml\nauth:\n  auth-oidc:\n    issuer: 'https://your-org.okta.com/oauth2/default'\n    audience: 'my-verdaccio-client-id'\n    allowed_domains: ['yourcompany.com']\n    group_claim: groups\n    allowed_groups: ['npm-developers']\n    username_claim: email\n```\n\n#### Keycloak\n\nKeycloak nests realm roles under `realm_access.roles`, so use a dot-path for\n`group_claim`:\n\n```yaml\nauth:\n  auth-oidc:\n    issuer: 'https://keycloak.example.com/realms/myrealm'\n    audience: 'verdaccio'\n    group_claim: realm_access.roles # nested claim\n    allowed_groups: ['npm-publishers']\n    username_claim: email\n```\n\n## Onboarding middleware (optional)\n\nThe plugin can also serve a web-based setup page at `/-/oidc/setup` that guides\nusers through authentication. Add a `middlewares` section to enable it:\n\n```yaml\nmiddlewares:\n  auth-oidc:\n    enabled: true\n    client_id: 'your-oidc-client-id' # optional — enables browser-based PKCE login\n    client_secret: 'your-secret' # optional — required by some providers for token exchange\n    scopes: 'openid email profile' # optional — OAuth scopes to request\n    external_url: 'https://your-verdaccio-host' # optional — public URL for PKCE redirects\n```\n\n| Option          | Required | Default                | Description                                              |\n| --------------- | -------- | ---------------------- | -------------------------------------------------------- |\n| `enabled`       | No       | `true`                 | Set `false` to disable; omitting enables the middleware  |\n| `client_id`     | No       | —                      | OIDC client ID for browser-based PKCE login              |\n| `client_secret` | No       | —                      | OIDC client secret (required by some providers)          |\n| `scopes`        | No       | `openid email profile` | OAuth scopes to request during PKCE flow                 |\n| `external_url`  | No       | —                      | Public registry URL for PKCE redirect URIs (recommended) |\n\n> **Security:** When `client_id` is set without `external_url`, redirect URLs\n> are derived from request headers, which can be spoofed behind misconfigured\n> proxies. Always set `external_url` in production.\n\n### Routes\n\n| Route              | Method | Description                                                       |\n| ------------------ | ------ | ----------------------------------------------------------------- |\n| `/-/oidc/config`   | GET    | Returns provider name, issuer, and PKCE availability as JSON      |\n| `/-/oidc/setup`    | GET    | Onboarding page with token paste, PKCE login, CLI help            |\n| `/-/oidc/validate` | POST   | Accepts `{ \"token\": \"...\" }`, returns username + groups           |\n| `/-/oidc/login`    | GET    | Initiates PKCE flow (`client_id` required); `?mode=spa` for theme |\n| `/-/oidc/callback` | GET    | Handles OIDC redirect; SPA mode returns fragment redirect         |\n\n### PKCE prerequisites\n\nIf you enable browser login (`client_id`), register\n`https://your-verdaccio-host/-/oidc/callback` as a redirect URI in your OIDC\nprovider's application settings.\n\n## API tokens (optional)\n\nThe plugin supports long-lived API tokens that can be used in CI/CD pipelines\nor environments where interactive OIDC login is impractical. API tokens use\nthe `vrd_` prefix and are stored as SHA-256 hashes.\n\nEnable API tokens by adding `api_tokens` to your auth config:\n\n```yaml\nauth:\n  auth-oidc:\n    issuer: 'https://accounts.google.com'\n    username_claim: email\n    api_tokens:\n      enabled: true\n      max_ttl_days: 90 # maximum token lifetime\n      default_ttl_days: 30 # default when ttl_days not specified\n      max_per_user: 10 # active tokens per user\n      step_up_max_age: 300 # max age (seconds) of OIDC auth for sensitive ops\n      dev_mode: false # true allows HTTP issuers on loopback (dev only)\n      admin_users: # users with admin privileges\n        - 'admin@mycompany.com'\n      admin_groups: # groups with admin privileges\n        - 'registry-admins'\n      allowed_users: # restrict token creation to these users\n        - 'ci-bot@mycompany.com'\n      allowed_groups: # restrict token creation to these groups\n        - 'npm-publishers'\n      denied_users: # kill switch: block these users from all auth\n        - 'former-employee@mycompany.com'\n```\n\n| Option             | Required | Default | Description                                             |\n| ------------------ | -------- | ------- | ------------------------------------------------------- |\n| `enabled`          | Yes      | —       | Enable API token management                             |\n| `max_ttl_days`     | No       | `90`    | Maximum token lifetime in days                          |\n| `default_ttl_days` | No       | `30`    | Default lifetime when `ttl_days` is not specified       |\n| `max_per_user`     | No       | `10`    | Maximum number of active tokens per user                |\n| `step_up_max_age`  | No       | `300`   | Max age (seconds) of OIDC auth for sensitive operations |\n| `dev_mode`         | No       | `false` | Allow HTTP issuers on loopback addresses (dev only)     |\n| `admin_users`      | No       | —       | Email addresses with admin privileges                   |\n| `admin_groups`     | No       | —       | Group names with admin privileges                       |\n| `allowed_users`    | No       | —       | Restrict token creation to these email addresses        |\n| `allowed_groups`   | No       | —       | Restrict token creation to these group names            |\n| `denied_users`     | No       | —       | Block these email addresses from all authentication     |\n\n> **Token creation mode:** When any `allowed_users` or `allowed_groups` are\n> configured, token creation switches to allowlist mode. Only listed users/groups\n> (plus admins) can create tokens. Without an allowlist, any authenticated user\n> can create tokens.\n\n### Scoped (least-privilege) tokens\n\nTokens can be created with an optional `scope` that narrows what they may do.\nScope only ever **reduces** privileges below the creating user's; it never\ngrants anything extra. There are two scope shapes (mutually exclusive):\n\n- **Read-only** (`{ \"readonly\": true }`): the token can install/read any\n  package the user could, but is rejected for all publish/unpublish operations.\n- **Package-scoped** (`{ \"packages\": [\"@acme/*\", \"tool-*\"] }`): publish and\n  unpublish are limited to packages matching one of the patterns. **Reads are\n  not restricted** — this is deliberate, because restricting reads would break\n  `npm install` (which fetches many transitive dependencies). Patterns use the\n  same syntax as dynamic ACLs: exact names, `@scope/*`, and trailing `prefix-*`\n  (bare `*` is rejected; use `**` for catch-all). Up to 20 patterns per token.\n\nExample request body for `POST /-/oidc/tokens`:\n\n```json\n{ \"name\": \"acme deploy\", \"ttl_days\": 30, \"scope\": { \"packages\": [\"@acme/*\"] } }\n```\n\nOmitting `scope` (or sending `{ \"readonly\": false }` with no packages) creates a\nfull-access token, preserving existing behavior. Scope is enforced server-side\nin `allow_publish`/`allow_unpublish`, independent of the UI, by threading the\ntoken's scope through the authenticated identity — it cannot be bypassed by a\ncrafted client. The theme UI exposes this as a **Permissions** selector\n(Full access / Read-only / Publish only to specific packages) on the token\ncreation form, and shows a `read-only` or `scoped` badge on scoped tokens.\n\n> **Deny-list (kill switch):** `denied_users` blocks an identity from _all_\n> authentication — browser/JWT login and API tokens — regardless of allow or\n> admin status. It is the fastest way to cut off a compromised or offboarded\n> account. Admins can also manage the deny-list at runtime via the API or theme\n> UI (`POST/DELETE /-/oidc/admin/acl/denied-users`); adding a user there also\n> immediately revokes their active API tokens. Entries are matched by canonical\n> email, so use `username_claim: email` for reliable matching. You cannot deny\n> your own account, and a config-defined admin listed in `denied_users` will be\n> locked out (the plugin warns about this at startup).\n>\n> **Session scope:** the deny check runs on every API-token and OIDC bearer\n> request, so CLI/CI access is cut off immediately. Existing browser sessions\n> backed by Verdaccio's own short-lived JWT clear when that token expires.\n\n### Token API endpoints\n\nAll mutation endpoints require a valid OIDC Bearer token and HTTPS (unless\n`dev_mode`). Read-only endpoints (`GET /me/permissions`, `GET /tokens`) enforce\nBearer auth but not HTTPS.\n\n| Route                                     | Method | Auth  | Description                       |\n| ----------------------------------------- | ------ | ----- | --------------------------------- |\n| `/-/oidc/tokens`                          | POST   | User  | Create a new API token            |\n| `/-/oidc/tokens`                          | GET    | User  | List own tokens                   |\n| `/-/oidc/tokens/:id`                      | DELETE | User  | Revoke own token                  |\n| `/-/oidc/tokens/admin/all`                | GET    | Admin | List all tokens (all users)       |\n| `/-/oidc/tokens/user/:username`           | DELETE | Admin | Revoke all tokens for a user      |\n| `/-/oidc/admin/tokens/:username/:tokenId` | DELETE | Admin | Revoke a specific user's token    |\n| `/-/oidc/me/permissions`                  | GET    | User  | Check own permissions and status  |\n| `/-/oidc/me/pkg-access`                   | GET    | User  | View own effective package access |\n\n### ACL management endpoints\n\nWhen API tokens are enabled, admins can manage access control lists via the API.\nACL entries added via the API are stored in `.verdaccio-acl.json` alongside\nthe Verdaccio storage directory and merged with YAML config entries.\n\n| Route                            | Method | Auth  | Description                                                |\n| -------------------------------- | ------ | ----- | ---------------------------------------------------------- |\n| `/-/oidc/admin/acl`              | GET    | Admin | Get merged ACL with origin flags                           |\n| `/-/oidc/admin/acl/:list`        | POST   | Admin | Add entry (`admin-users`, `allowed-users`, `denied-users`) |\n| `/-/oidc/admin/acl/:list/:value` | DELETE | Admin | Remove entry; cascades token revocation                    |\n| `/-/oidc/admin/audit`            | GET    | Admin | Unified audit log with filters + paging                    |\n| `/-/oidc/admin/uplinks`          | GET    | Admin | Proxy health dashboard (probes uplink URLs)                |\n\n> **Audit log:** `GET /-/oidc/admin/audit` merges ACL and package-access events\n> into one feed. Query params: `action` (exact), `actor` (case-insensitive\n> substring), `from`/`to`\n> (ms epoch, inclusive), `offset`, `limit` (default 50, max 200). Response:\n> `{ entries, total, offset, limit, actions }` — newest first, where `actions`\n> lists every action name available for filtering. Backed by a per-store\n> 200-entry ring buffer (durable external sink is a separate roadmap item).\n\n### Metrics\n\nSet `metrics.enabled: true` (opt-in) to expose an in-process metrics snapshot.\n\n| Route                   | Method | Auth          | Description                          |\n| ----------------------- | ------ | ------------- | ------------------------------------ |\n| `/-/oidc/metrics`       | GET    | Token / Admin | Prometheus text exposition           |\n| `/-/oidc/admin/metrics` | GET    | Admin         | JSON snapshot (used by the admin UI) |\n\nExposed series (aggregate counters — **no usernames or token data**):\n`oidc_auth_success_total{method}`, `oidc_auth_failure_total{reason}`,\n`oidc_tokens_created_total`, `oidc_tokens_revoked_total`,\n`oidc_jwks_cache_hits_total`, `oidc_jwks_cache_misses_total`,\n`oidc_active_tokens` (gauge), `oidc_uptime_seconds` (gauge).\n\n> **Security:** the endpoint is **off by default**. `/-/oidc/metrics` requires\n> either a static bearer token (`metrics.token`, for scrapers like Prometheus —\n> compared in constant time) or an admin OIDC bearer (used by the admin UI). HTTPS\n> is enforced (except in `dev_mode`), so the scrape token is never sent in the\n> clear. Counters are aggregate-only and reset on restart (use `rate()`/`increase()`).\n>\n> **`metrics.token` is optional.** Omit it for UI-only use — the admin dashboard\n> authenticates with your admin OIDC bearer. Set it only when an external scraper\n> (which can't perform an OIDC login) needs access. **Do not commit a real token.**\n> Generate one with `openssl rand -hex 32` and inject it at deploy time (secret\n> manager, Kubernetes Secret, or a templated config rendered at container start);\n> Verdaccio does not reliably substitute `${ENV}` placeholders in config values.\n\nExample Prometheus scrape config:\n\n```yaml\nscrape_configs:\n  - job_name: verdaccio-oidc\n    scheme: https\n    metrics_path: /-/oidc/metrics\n    authorization:\n      credentials: 'a-long-random-scrape-secret' # matches metrics.token\n    static_configs:\n      - targets: ['registry.example.com']\n```\n\n### CLI: revoke-tokens\n\nA CLI tool for emergency token revocation (break-glass scenarios). Works\ndirectly on the token store files without requiring a running Verdaccio instance.\n\n```bash\n# List all active tokens\nverdaccio-revoke-tokens --storage /path/to/verdaccio/storage --list\n\n# Revoke all tokens for a user\nverdaccio-revoke-tokens --storage /path/to/verdaccio/storage --user user@example.com\n\n# Revoke a specific token by ID\nverdaccio-revoke-tokens --storage /path/to/verdaccio/storage --token-id <uuid>\n\n# Compact the store, removing expired/revoked tokens past the grace window\nverdaccio-revoke-tokens --storage /path/to/verdaccio/storage --purge-expired\n```\n\n| Flag               | Short | Description                                                |\n| ------------------ | ----- | ---------------------------------------------------------- |\n| `--storage <path>` | `-s`  | Path to Verdaccio storage directory (required)             |\n| `--user <email>`   | `-u`  | Revoke all tokens for this user                            |\n| `--token-id <id>`  | —     | Revoke a specific token by ID                              |\n| `--list`           | —     | List all active tokens                                     |\n| `--purge-expired`  | —     | Remove expired/revoked tokens past the 30-day grace window |\n| `--help`           | `-h`  | Show help                                                  |\n\n> The runtime plugin auto-purges every 6 hours; `--purge-expired` is for manual\n> offline compaction (e.g. break-glass cleanup without a running Verdaccio).\n\n## Custom OIDC theme (optional)\n\nFor a fully integrated experience, install the companion theme plugin\n`verdaccio-theme-oidc`. This replaces the default Verdaccio UI login with a\nnative OIDC flow — users click \"Sign in with {Provider}\" and are redirected to\nyour OIDC provider.\n\n```yaml\ntheme:\n  oidc: {}\n\nmiddlewares:\n  auth-oidc:\n    enabled: true\n    client_id: 'your-oidc-client-id' # Required for browser login\n```\n\nThe theme provides:\n\n- OIDC login page with provider auto-detection (Google, Azure AD, Okta, etc.)\n- Settings page with `.npmrc` snippets, copy buttons, and token expiry display\n- API token management UI (create, list, revoke)\n- Admin dashboard with ACL management and uplink health monitoring\n- Silent session restoration on page refresh\n- Token expiry indicators in the header\n\nSee the [verdaccio-theme-oidc](../verdaccio-theme-oidc/) package for details.\n\n### Fallback: adding a link to the default Verdaccio UI\n\nIf you don't use the custom theme, you can inject a link into the default UI:\n\n```yaml\nweb:\n  scriptsBodyAfter:\n    - '<script>document.addEventListener(\"DOMContentLoaded\",()=>{const b=document.createElement(\"div\");b.innerHTML=\"<a href=\\\"/-/oidc/setup\\\" style=\\\"position:fixed;bottom:16px;right:16px;padding:8px 16px;background:#4b5e40;color:#fff;border-radius:4px;text-decoration:none;font-size:14px;z-index:9999\\\">OIDC Setup</a>\";document.body.appendChild(b)})</script>'\n```\n\n## Usage\n\nOnce configured, users authenticate by passing their OIDC token as the npm\npassword:\n\n```bash\nnpm --registry https://your-verdaccio-host login\n# Username: your.email@company.com\n# Password: <paste your OIDC/JWT token>\n```\n\nAlternatively, visit `https://your-verdaccio-host/-/oidc/setup` for an\ninteractive setup guide.\n\nHow to obtain a token depends on your provider:\n\n| Provider    | Command                                                                         |\n| ----------- | ------------------------------------------------------------------------------- |\n| Google      | `gcloud auth print-identity-token`                                              |\n| Azure       | `az account get-access-token --query accessToken -o tsv`                        |\n| Okta (PKCE) | Use the PKCE helper: `node tools/get-token.mjs --issuer <url> --client-id <id>` |\n\n## Package access management (optional)\n\nManage Verdaccio's per-package `access`/`publish`/`unpublish` permissions from\nthe Admin UI. Config entries remain immutable; the UI can only add users or\ngroups dynamically (additive-only model).\n\nEnable package access management by adding `pkg_access` to your auth config:\n\n```yaml\nauth:\n  auth-oidc:\n    issuer: 'https://accounts.google.com'\n    username_claim: email\n    pkg_access:\n      enabled: true\n      allow_dynamic_patterns: false # allow creating patterns not in YAML\n      allow_catchall_dynamic: false # allow dynamic entries on '**'\n      max_patterns: 100 # max dynamic patterns\n      max_entries_per_action: 50 # max entries per access/publish/unpublish\n```\n\n| Option                   | Default | Description                                         |\n| ------------------------ | ------- | --------------------------------------------------- |\n| `enabled`                | `false` | Enable dynamic package access management            |\n| `allow_dynamic_patterns` | `false` | Allow creating patterns not present in YAML config  |\n| `allow_catchall_dynamic` | `false` | Allow dynamic entries on the `**` catch-all pattern |\n| `max_patterns`           | `100`   | Maximum number of dynamic-only patterns             |\n| `max_entries_per_action` | `50`    | Maximum dynamic users/groups per action per pattern |\n\n### Recommended config for internal registries\n\nFor a private registry hosting confidential packages, use restrictive base\nrules in config and expand from the UI:\n\n```yaml\npackages:\n  '@myco/secret-*':\n    access: admin@myco.com security-team\n    publish: admin@myco.com\n  '@myco/*':\n    access: engineering-team\n    publish: engineering-team\n  '**':\n    access: $authenticated\n    publish: engineering-team\n    proxy: npmjs\n```\n\nAdmins can then add individual users to `@myco/secret-*` or `@myco/*` from\nthe Package Access section in the Admin UI without restarting Verdaccio.\n\n### Security model\n\n- **Config is the trust anchor.** YAML entries cannot be removed via API/UI.\n- **Additive-only.** Dynamic rules extend config permissions; they cannot\n  restrict what config already allows.\n- **Reserved principals blocked.** `$all`, `$anonymous`, and `$authenticated`\n  cannot be added dynamically.\n- **Fail-closed.** If the dynamic store is corrupt or unreadable, only config\n  rules apply. No fail-open.\n- **Tamper detection.** The store is signed with HMAC-SHA256 using a separate\n  key file (`.verdaccio-pkg-access.key`, auto-generated, `0600` permissions).\n  A monotonic epoch counter detects rollback attempts at runtime.\n- **Step-up auth required** for all mutations.\n- **Single-node assumption.** File-based storage with file locking is safe for\n  a single Verdaccio instance. Multi-instance deployments with shared storage\n  would need a different backend.\n\n### Admin API endpoints\n\n| Route                               | Method | Description                                  |\n| ----------------------------------- | ------ | -------------------------------------------- |\n| `/-/oidc/admin/pkg-access`          | GET    | Merged rules (config + dynamic)              |\n| `/-/oidc/admin/pkg-access/entries`  | POST   | Add user/group: `{ pattern, action, value }` |\n| `/-/oidc/admin/pkg-access/entries`  | DELETE | Remove dynamic entry                         |\n| `/-/oidc/admin/pkg-access/patterns` | POST   | Create new pattern (requires opt-in)         |\n| `/-/oidc/admin/pkg-access/patterns` | DELETE | Delete dynamic-only pattern                  |\n\n## Security considerations\n\n- **HTTPS required:** All API token endpoints enforce HTTPS unless `dev_mode`\n  is enabled. Verdaccio must sit behind exactly one trusted reverse proxy\n  (ALB, nginx, etc.) that sets `X-Forwarded-Proto`.\n- **Audience validation:** Omitting `audience` means tokens from any client\n  sharing the issuer will be accepted. Always set `audience` in production.\n- **Step-up auth:** Creating or revoking tokens requires a fresh OIDC\n  authentication (within `step_up_max_age` seconds).\n- **Token epoch:** Revoking tokens bumps a monotonic epoch counter. If the\n  epoch file is ahead of the store (e.g., after a backup restore), all tokens\n  are rejected until resolved via the CLI.\n- **File permissions:** The token store is written with mode `0600`. The plugin\n  warns at startup if the file is world/group-readable.\n\n## Rate limiting / brute-force guard\n\nThe plugin includes an in-process rate limiter (no external store/Redis) with\ntwo layers, **enabled by default**:\n\n- **Per-IP endpoint throttle** on `POST /validate`, `GET /login`, the\n  `GET /metrics` scrape, and **every authenticated endpoint** (the throttle runs\n  inside the auth guard, _before_ `verifyToken`, so an unauthenticated flood\n  cannot drive JWKS fetches / signature checks). Over the limit returns `429`\n  with a `Retry-After` header. Transient only — no extended lockout. Relies on\n  `trust proxy` (set automatically whenever rate limiting, API tokens, or\n  metrics are enabled) so `req.ip` is the real client behind your reverse proxy.\n  Without a single trusted proxy in front, `X-Forwarded-For` can be spoofed and\n  per-IP limiting bypassed — see the deployment requirement below.\n- **Per-username failure lockout** on the CLI/basic-auth path (`authenticate`).\n  After `auth_max_failures` failed attempts within the window, that username is\n  locked for `lockout_ms`. A successful login clears the counter. The auth\n  callback has no request IP, so this layer is keyed by username.\n\nConfigure under `auth.auth-oidc.rate_limit`:\n\n```yaml\nauth:\n  auth-oidc:\n    rate_limit:\n      enabled: true # default true; set false to disable both layers\n      window_ms: 60000 # sliding window length (default 60s)\n      max_requests: 120 # per-IP requests/window for sensitive endpoints\n      auth_max_failures: 10 # per-username failed logins before lockout\n      lockout_ms: 900000 # lockout duration once threshold hit (default 15m)\n```\n\n| Option              | Default  | Description                                         |\n| ------------------- | -------- | --------------------------------------------------- |\n| `enabled`           | `true`   | Master switch for both layers                       |\n| `window_ms`         | `60000`  | Sliding-window length (ms)                          |\n| `max_requests`      | `120`    | Per-IP requests per window on sensitive endpoints   |\n| `auth_max_failures` | `10`     | Per-username failed logins within the window        |\n| `lockout_ms`        | `900000` | Lockout duration after the failure threshold is hit |\n\n> **Targeted-lockout trade-off:** because the `authenticate` path exposes no\n> client IP, the failure lockout is keyed by username. An attacker who knows a\n> username could deliberately trip the lockout to deny that user (for the\n> `lockout_ms` window). Defaults are conservative (10 failures, 15-minute\n> lockout, auto-cleared on success). Tune `auth_max_failures`/`lockout_ms`, or\n> set `enabled: false`, if this trade-off is unacceptable for your threat model.\n> State is per-process and resets on restart; key cardinality is bounded to cap\n> memory under spoofed-key abuse. Rejections are counted in the\n> `oidc_rate_limited_total` metric.\n\n## External audit sink\n\nAudit events (token create/revoke, ACL changes, package-access changes) live in\na 200-entry in-store ring buffer — fine for the admin UI, but lost on restart\nand unsuitable for compliance. An **optional** audit sink forwards every audited\nevent to a durable destination. It hooks the same `appendAuditLog` choke point\nthe ring buffer uses, so coverage is automatic.\n\nEach event is a JSON object: `{ action, actor, target, ts, source }` where\n`source` is `acl` or `pkg-access` (`revoked_count` is included when relevant).\n\nConfigure under `auth.auth-oidc.audit_sink` (omit the block to disable):\n\n```yaml\nauth:\n  auth-oidc:\n    # ── File: append-only JSONL (one event per line) ──\n    audit_sink:\n      type: file\n      path: /var/log/verdaccio/audit.jsonl # absolute path; file written 0600\n\n\n    # ── Syslog: UDP RFC 5424 datagrams ──\n    # audit_sink:\n    #   type: syslog\n    #   host: 127.0.0.1\n    #   port: 514\n    #   facility: 13          # default 13 (log audit)\n    #   app_name: verdaccio-oidc\n\n    # ── Webhook: HTTP POST, buffered + retried ──\n    # audit_sink:\n    #   type: webhook\n    #   url: https://siem.example.com/ingest   # HTTPS required (http:// loopback only with dev_mode)\n    #   secret: \"<hmac-key>\"  # optional; signs body as X-Audit-Signature: sha256=<hex>\n    #   max_queue: 1000       # events buffered before drop-oldest\n    #   max_retries: 5        # delivery attempts per event (exponential backoff)\n    #   timeout_ms: 5000      # per-request timeout\n```\n\n| Sink      | Durability                  | Notes                                                  |\n| --------- | --------------------------- | ------------------------------------------------------ |\n| `file`    | Disk (survives restart)     | JSONL append, `0600`. You manage rotation (logrotate). |\n| `syslog`  | Depends on collector        | UDP, fire-and-forget; pair with a reliable collector.  |\n| `webhook` | At-least-once (best effort) | In-memory queue; HMAC-signed; lost on crash.           |\n\nDesign guarantees:\n\n- **Never blocks or breaks a request.** Delivery is async and failure-isolated; a\n  slow/down receiver never stalls a publish, login, or ACL change.\n- **Bounded memory.** The webhook queue caps at `max_queue` and drops the oldest\n  event on overflow (counted as `dropped`).\n- **No process keep-alive.** Sockets and retry timers are `unref`'d.\n- **Multi-instance:** each node forwards independently. For a single aggregated\n  stream, point all nodes at one syslog/webhook collector (file sinks are\n  per-node).\n- **Secrets:** token values are never logged (already stored hashed); events\n  carry only IDs/usernames/actions.\n\nThe webhook payload is the raw audit JSON\n(`{ action, actor, target, ts, source, app_name }`). This works directly with\nlog collectors / SIEMs that accept arbitrary JSON (Splunk HEC, Datadog, Elastic,\nor your own endpoint).\n\n**Chat integrations (Slack, Teams, etc.):** these expect a service-specific body\n(e.g. Slack requires `{ \"text\": \"...\" }`) and will reject the raw audit JSON with\n`400`. Point the webhook at a small adapter that reshapes the event, rather than\nat the chat provider directly:\n\n```text\nverdaccio  ──POST audit JSON──▶  adapter (Lambda / Cloud Function / tiny service)\n                                   │ maps → { \"text\": \"...\" } (+ verifies HMAC)\n                                   ▼\n                                 Slack / Teams incoming webhook\n```\n\nThe adapter can also verify the `X-Audit-Signature` HMAC (chat providers ignore\nit) and apply provider rate-limiting so a burst of events doesn't get dropped.\n\nDelivery outcomes are exported as the `oidc_audit_sink_total{outcome=...}`\nmetric (`delivered` / `failed` / `dropped`).\n\n## Download statistics\n\nOpt-in per-package download counts with daily history. When enabled, the plugin\ntallies successful tarball fetches (`GET /<pkg>/-/<file>.tgz`, status `200` served\nor `304` client-cache hit) and the theme's package page shows an all-time total\nplus a 30-day bar chart.\n\nConfigure under `auth.auth-oidc.download_stats` (omit the block to disable):\n\n```yaml\nauth:\n  auth-oidc:\n    download_stats:\n      enabled: true\n      retention_days: 90 # daily buckets older than this are pruned (default 90)\n      max_packages: 5000 # lowest-total packages evicted past this (default 5000)\n```\n\nCounting is a pure in-memory increment on the serve path (no added latency).\nCounts accumulate as deltas and are merged into `.verdaccio-download-stats.json`\non a debounced timer; the read-modify-write runs under a lock and applies deltas\nadditively, so multiple processes sharing one storage dir don't clobber each\nother. Memory and disk are bounded by `retention_days` and `max_packages`.\n\nEndpoints:\n\n- `GET /-/oidc/downloads/:pkg` — any authenticated user; `:pkg` is URL-encoded.\n  Optional `?days=N` (1–365, default 90). Returns `{ package, total, series }`.\n- `GET /-/oidc/admin/downloads` — admin only; `?limit=N` (1–500, default 50).\n  Returns the top-N packages by all-time total. Hidden (`404`) from non-admins.\n\nA process-lifetime `oidc_downloads_total` counter is also exported via the\nmetrics endpoint. Only package names and counts are stored — no per-user data.\n\n## Event webhooks\n\nOpt-in outbound HTTP notifications on security-relevant events (token, ACL, and\npackage-access changes). Unlike `audit_sink` (one durable destination for _every_\nevent), event webhooks fan out to **many** subscriptions, each filtered to the\nevents it cares about — and admins can manage them at runtime from the theme's\n**Settings → Admin → Event Webhooks** page (no restart).\n\nConfigure under `auth.auth-oidc.event_webhooks` (omit the block to disable):\n\n```yaml\nauth:\n  auth-oidc:\n    event_webhooks:\n      enabled: true\n      max_subscriptions: 50 # cap on dynamic (API-created) subs (default 50)\n      max_queue: 1000 # per-sub buffered events before drop-oldest (default 1000)\n      max_retries: 5 # delivery attempts per event (default 5)\n      timeout_ms: 5000 # per-request timeout (default 5000)\n      ssrf_protection: true # block dynamic targets on private/internal IPs (default true)\n      # allowed_hosts: ['hooks.slack.com'] # if set, dynamic subs may ONLY use these hosts\n      # Optional config-seeded subscriptions (immutable via API/UI):\n      subscriptions:\n        - url: https://siem.example.com/verdaccio\n          events: ['*'] # or ['acl:*', 'pkg-access:*', 'token_created', ...]\n          secret: ${WEBHOOK_SECRET} # optional HMAC-SHA256 signing key\n        # Slack incoming webhook via headers + body template:\n        - url: https://hooks.slack.com/services/T000/B000/XXXX\n          events: ['acl:*', 'token_revoked']\n          headers:\n            Content-Type: application/json\n          template: '{\"text\":\"verdaccio: {{actor}} did {{action}} on {{target}}\"}'\n```\n\n**Event selectors.** A subscription targets events with `*` (all), `<source>:*`\n(`acl:*` or `pkg-access:*`), or an exact audit action (`token_created`,\n`token_revoked`, `admin_revoke_all`, `admin_revoke_token`, `add_admin_users`,\n`remove_admin_users`, `add_allowed_users`, `remove_allowed_users`,\n`add_denied_users`, `remove_denied_users`, `pkg_access_add`, `pkg_access_remove`,\n`pkg_pattern_create`, `pkg_pattern_delete`). Auth/login events are **not** sent —\nonly durable change events that flow through the audit stream.\n\n**Payload + signing.** By default the body is the raw audit JSON\n(`{ action, actor, target, ts, source }`, plus `revoked_count` on\ndeny/revoke events). When a subscription has a secret, the\nbody is signed as `X-Webhook-Signature: sha256=<hmac>` (verify it the same way as\n`audit_sink`'s `X-Audit-Signature`). The signature is computed over the final\n(templated) body.\n\n**Custom headers.** A subscription may attach static request headers (e.g. a\nprovider auth token). Header **values are write-only** — the API/UI return only\nthe header _names_. Header names must be tokens (`[A-Za-z0-9-]`); values may not\ncontain CR/LF or control chars (header-injection guard); `Host`,\n`Content-Length`, and `X-Webhook-Signature` are reserved and rejected.\n\n**Body templates (Slack/Teams).** Set `template` to reshape the payload without a\nseparate adapter. Placeholders `{{action}}`, `{{actor}}`, `{{target}}`,\n`{{source}}`, `{{ts}}`, `{{revoked_count}}` are substituted; every value is\n**JSON-escaped**, so a\nhostile package name or actor cannot break out of a JSON string literal and\ninject structure. Put placeholders inside JSON string literals\n(`{\"text\":\"{{actor}}\"}`). Leave `template` unset to send the raw audit JSON.\n\n**SSRF protection.** Because admins create dynamic subscriptions at runtime, the\nHTTPS scheme check alone wouldn't stop a target pointed at an internal address\n(cloud metadata `169.254.169.254`, RFC-1918 hosts, etc.). With\n`ssrf_protection: true` (the **default**), each dynamic target is resolved and\nrejected if any address falls in a private, loopback, link-local, CGNAT, or\notherwise non-public range — both at creation/update and again immediately before\nevery delivery (to blunt DNS-rebinding). Loopback is permitted only when\n`api_tokens.dev_mode` is on. Config-seeded subscriptions are operator-trusted and\nexempt. To allow specific internal receivers, list them in `allowed_hosts`: when\nset, dynamic subs may target **only** those hostnames and the IP-range check is\nskipped for them. Residual risk: there is a small TOCTOU window between the\npre-flight resolve and the HTTP client's own resolution; for hard guarantees pair\nthis with `allowed_hosts` or egress firewalling. Set `ssrf_protection: false` to\ndisable entirely (not recommended in production).\n\n**Guarantees.** Delivery is non-blocking and failure-isolated: a slow or down\nreceiver never delays a token/ACL change. Each subscription has its own buffered,\nretried worker (exponential backoff, drop-oldest past `max_queue`); sockets and\ntimers are `unref`'d. Secrets are stored on disk (`0600`) but **never** returned\nby the API — the UI only shows whether one is set.\n\nEndpoints (all admin-only; mutations require fresh OIDC auth, like ACL changes):\n\n- `GET /-/oidc/admin/webhooks` — list subscriptions + available selectors.\n- `POST /-/oidc/admin/webhooks` — create a dynamic subscription.\n- `PATCH /-/oidc/admin/webhooks/:id` — update a dynamic subscription. Omitted\n  fields are unchanged; `null`/`\"\"` clears. `secret`/`headers` are write-only\n  (re-send to change, blank keeps the stored value); `template` is sent in full.\n- `POST /-/oidc/admin/webhooks/:id/enabled` — enable/disable.\n- `DELETE /-/oidc/admin/webhooks/:id` — remove (dynamic only).\n- `POST /-/oidc/admin/webhooks/:id/test` — send a signed test ping (uses the\n  subscription's headers + template).\n\nDelivery outcomes are exported as `oidc_webhook_total{outcome=...}`\n(`delivered` / `failed` / `dropped`).\n\n## License\n\nMIT\n","readmeFilename":"README.md"}