Part of https://github.com/pnpm/tasks/issues/191. Wrong claims: - cli: drop the nonexistent `add-user --password` flag and document the terminal/stdin password read; `oci-gc --min-age` and `cache-gc --max-age` are the flags (the `-secs` forms are aliases); `--from` defaults to the `storage` key, not an internal field; `info` and `http` log the same access records; only global and server options have PNPR_* variables. - configuration: `packumentTtl` defaults to 5m; the minimal config now sets `auth.htpasswd.file`, and the auth section says accounts live in memory without it or a backend; grouped hosted registries default their org to `<ecosystem>~<name>`; the handshake is served for any surface; the pipeline surface also satisfies "something must be served"; add `package.update_maintainers` to the audit list. - tls-and-reverse-proxy: remove the invalid `publicUrl:` YAML key and the nonexistent `rate_limiting` setting. - backup-and-restore: `pnpr verify` hashes by default (`--check-hashes` does not exist, `--skip-hashes` skips only the hash comparison); `migrate-storage --verify` compares bytes. - webhooks: audit events do not cover publishes or token creation; the example uses a real action and the `actor` field. - cli: the `RUST_LOG` example keeps `pnpr::audit=info`. - install-acceleration: the `--public-url` host is exempt from the private-address refusal. - shared-side-effects-cache: `pnpm pipeline` publishes workspace-task artifacts. Missing facts: `routes.allowedPrivateNetworks`, the upstream `search` key and canonical `maxAge`, built-in `npm.jsr.io` route, relative path resolution, `log` defaults and `type`, `${VAR:-default}` substitution and the full secret-field list, `oci` defaults, `artifacts.quota` bounds, `migrate-storage` destination rules, `--osv false`, `--packument-ttl` default, S3 `forcePathStyle`/`allowHttp` defaults, grouped org layout, OIDC key table, container image section, `backend:` ignoring the htpasswd and token files. Duplicates and links: health probe bodies, reload procedure, and the resolver allowlist now link to their one home; broken `#secret` anchor fixed; links into the pnpm docs use absolute pnpm.io URLs. Stale history: drop "pnpr v1" and the per-paragraph "Since alpha.N" / "Since pnpm 12.x" prefixes, and the contributor-only extensibility text. Also corrects the `as_filter_directive` doc comment, which claimed `http` quiets the rest of the crate.
10 KiB
id, title
| id | title |
|---|---|
| oidc | OpenID Connect |
Added in: v0.1.0-alpha.11
pnpr supports OIDC browser sign-in and keyless npm publishing from workloads such as GitHub Actions. Both use discovery and signed ID tokens from explicitly configured issuers. Local password authentication keeps working alongside them.
Browser sign-in
Register a web application with your identity provider. Set its callback to
https://registry.example/-/oidc/company/callback, replacing the hostname and
company with your pnpr provider name, and enable the authorization code flow.
Start pnpr with --public-url https://registry.example, its HTTPS origin
without a path prefix.
auth:
oidc:
- name: company
issuer: https://accounts.google.com
audience: your-client-id
login:
clientSecret: ${OIDC_CLIENT_SECRET}
users:
- subject: 'the-users-stable-sub-claim'
username: alice
claims:
hd: example.com
audience is the application's client ID. clientSecret can be omitted for
providers that support public clients with PKCE. subject is the exact sub
claim issued to this application, not an email address. Each binding maps that
subject to a pnpr username, and the registry's existing access rules and teams
then apply. Extra claims are exact string matches and all of them must match.
There is no automatic account creation, and no linking by email.
Teams from groups
A signed-in session can also join teams through the groups your identity provider puts in the ID token:
auth:
oidc:
- name: company
issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
audience: your-client-id
login:
users:
- subject: 'the-users-stable-sub-claim'
username: alice
groups:
claim: groups
teams:
- group: 7f3c2a10-0000-0000-0000-000000000000
registry: private
team: platform
claimnames the ID-token claim that lists the groups, either as a list of strings or as one string. It defaults togroups.- A session whose claim lists
groupis a member ofteamon the hosted registryregistry, as if the team listed its username.registryis the registry's key inregistries:. - The membership lasts as long as the session. Removing someone from a group
takes effect at their next sign-in, within an hour. It never applies to the
user's password logins or
npm tokens. - On a registry whose teams are the config's, the team must be declared in its
teams:. WithteamsManagedBy: api, it may name a team the admin API creates later.
Configure the provider to issue the claim, and use the values it emits as
group. Entra ID emits group object IDs under groups once group claims are
enabled for the application, as in the example above. Okta emits group names
under groups when a groups claim is added to the authorization server.
Use the issuer your provider publishes:
| Provider | Issuer |
|---|---|
| Google Workspace | https://accounts.google.com |
| Microsoft Entra ID | https://login.microsoftonline.com/<tenant-id>/v2.0 |
| Okta org authorization server | https://<your-org>.okta.com |
| Okta custom authorization server | https://<your-org>.okta.com/oauth2/<authorization-server-id> |
Use a tenant-specific Entra issuer. For Google Workspace, require the hd claim
for your organization's domain, as shown above. See the provider documentation
for Google,
Entra,
and Okta.
Open https://registry.example/-/oidc/company/login in a browser. After
sign-in, pnpr displays a token to put in the registry's _authToken setting,
for example in your private user .npmrc:
//registry.example/:_authToken=THE_DISPLAYED_TOKEN
The browser session expires at the earlier of one hour or the ID token's expiry,
and npm logout revokes it. Sessions live in memory and disappear on restart,
so a multi-replica deployment needs affinity for both the login flow and the
authenticated requests that follow. They do not appear in npm token list. The
web UI lists these providers on its sign-in screen and returns to
the UI after sign-in.
pnpm login and npm login use this flow automatically via /-/v1/login. The
CLI opens the browser to sign in and polls until authentication completes. If no
OIDC provider declares login:, /-/v1/login returns 404 and the CLI falls back
to username and password authentication (/-/user/org.couchdb.user:{name}).
An OIDC session cannot yet be exchanged for an OCI scoped bearer token.
GitHub Actions keyless publishing
Configure a dedicated workload username, a named hosted npm registry, and exact
package names. The package's normal publish rule must grant that username
access too.
registries:
private:
type: hosted
packages:
'@example/widget':
publish: [release-ci]
auth:
oidc:
- name: github
issuer: https://token.actions.githubusercontent.com
audience: https://registry.example
workloads:
- identity:
subject: repo:example/widgets:ref:refs/heads/main
username: release-ci
claims:
repository_id: '123456789'
repository_owner_id: '987654321'
workflow_ref: example/widgets/.github/workflows/release.yml@refs/heads/main
registry: private
packages: ['@example/widget']
Use immutable repository and owner IDs so trust does not transfer when names are
reused. For reusable workflows, also require job_workflow_ref to pin the
workflow that performs the publication. A job using a GitHub environment has an
environment-based subject, such as
repo:example/widgets:environment:production. Configure the exact subject and
the additional claims you intend to trust. See
GitHub's OIDC reference.
The publishing job needs id-token: write. Request an ID token with pnpr's
configured audience and prefix it with pnpr_workload_ as the registry token.
No pnpr password, persistent API key, or token exchange is involved:
permissions:
contents: read
id-token: write
steps:
# Check out, install dependencies, and build before requesting the token.
- name: Publish to pnpr
shell: bash
run: |
response=$(curl --fail --silent --show-error \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=https%3A%2F%2Fregistry.example")
token=$(jq -er '.value' <<< "$response")
echo "::add-mask::$token"
export NODE_AUTH_TOKEN="pnpr_workload_${token}"
npm publish --registry=https://registry.example/npm/~private/
The project's .npmrc holds only the environment reference:
//registry.example/npm/~private/:_authToken=${NODE_AUTH_TOKEN}
The credential permits ordinary PUT publications to the configured packages
through that named registry and nothing else. It cannot read packages, unpublish,
change dist-tags separately, manage accounts, publish batches, or reach other
pnpr services. Request the token after building so it stays valid through the
publish request; its issuer's expiry applies to every request.
Keys
Every block rejects unknown keys.
| Key | Required | Description |
|---|---|---|
name |
yes | Provider name, used in the /-/oidc/<name>/ URLs. |
issuer |
yes | The provider's issuer URL. |
audience |
yes | The expected aud claim. For browser sign-in, the client ID. |
login.clientSecret |
no | Client secret for browser sign-in. Omit for public clients with PKCE. |
login.users |
yes, with login |
Bindings of subject, username, and optional claims. |
login.groups.claim |
no | The ID-token claim that lists groups. Defaults to groups. |
login.groups.teams |
yes, with groups |
Grants of group, registry, and team. See Teams from groups. |
workloads[].identity |
yes | A binding of subject, username, and optional claims. |
workloads[].registry |
yes | The hosted npm registry the workload publishes to. |
workloads[].packages |
yes | The exact package names the workload may publish. |
Validation and operations
pnpr accepts RS256 and ES256 signatures and verifies the issuer, audience, authorized party where present, expiry, issue time, and not-before claims. Browser login also verifies a browser-bound state, a nonce, and S256 PKCE. Unmapped and ambiguous identities are rejected, and user subjects must be unique within a provider, its workload bindings included.
The pnpr_oidc_ and pnpr_workload_ credential prefixes are reserved for OIDC,
and workload credentials bypass the persistent token backend.
Discovery and JWKS use HTTPS with redirects disabled and bounded response sizes and deadlines. Literal and DNS-resolved destinations must be public IP addresses, and OIDC requests ignore environment-configured HTTP proxies. Metadata is cached for five minutes, and a verification failure can trigger a refresh at most once every 30 seconds per provider, so an unavailable provider can never turn an invalid credential into anonymous access.
Login state lives in a signed HttpOnly cookie for five minutes. Anonymous login starts reserve no server-side entries. Successful login replays and browser sessions are each capped at 1,024 per process, and a separate 1,024-entry callback-attempt history rejects recent failed and concurrent replays before token exchange, evicting the oldest attempt when full. At most 16 browser callbacks perform network operations concurrently.
pnpr omits OIDC callback query strings from its request logs. Configure reverse proxies to omit them too.