Trusted publishing, token-less npm releases via GitHub OIDC
Replace NPM_TOKEN secrets with OIDC. publish.yml that triggers on tag push. npm provenance badges. Cleaner trust signal for security-conscious users.
Trusted publishing, token-less npm releases via GitHub OIDC
Storing NPM_TOKEN as a GitHub secret has worked forever, but it's a long-lived credential, leak it and an attacker can publish malicious versions of your package. Trusted publishing swaps the static token for OIDC: GitHub Actions exchanges a short-lived signed token at publish time, no static secret on either side. This recipe is the 5-minute setup plus the npm provenance badge that tells users "this came from a verified GitHub Actions build".
Schritt 1: Why this matters
Three failure modes the static token pattern allows:
- Repo compromise,
NPM_TOKENenv var leaks through a malicious dep, attacker publishes[email protected]with a backdoor. - Maintainer compromise, npm account stolen, attacker publishes from elsewhere.
- Misuse drift, the token gets reused in random scripts over time, eventually exposed.
OIDC fixes #1 and partially #3. The token is created at publish time, valid for ~10 minutes, scoped to the exact repo + workflow + ref. There's no static secret to leak.
npm provenance (the bonus) adds a verifiable claim to the published version: "this package was built by GitHub Actions in you/your-mcp-server from commit SHA abc123 at timestamp Y." Users can verify the chain.
Schritt 2: Configure trusted publisher on npm
- Go to https://www.npmjs.com/package/your-mcp-server/access (you must be the maintainer).
- Trusted Publisher Management → Add trusted publisher.
- Fill:
- Repository owner: your GitHub username/org
- Repository name:
your-mcp-server - Workflow name:
publish.yml(must match the file in.github/workflows/) - Environment (optional):
production(recommended, adds another layer)
- Save.
After this, npm will accept publishes only from this exact workflow / repo / branch combo.
Schritt 3: The publish workflow
# .github/workflows/publish.yml
name: Publish to npm
on:
push:
tags: ['v*'] # triggers on v1.0.0, v1.0.1, etc.
jobs:
publish:
runs-on: ubuntu-latest
environment: production # if you set the optional environment in Step 2
permissions:
contents: read
id-token: write # ← required for OIDC token exchange
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
- run: npm publish --provenance --access public
# No NODE_AUTH_TOKEN! OIDC handles auth via the id-token permission above.
Key bits:
permissions.id-token: write, grants the workflow the right to request an OIDC token from GitHub. Without this, npm rejects.- No
NODE_AUTH_TOKEN, that's the whole point. Compare to the old pattern where this would be${{ secrets.NPM_TOKEN }}. --provenance, generates the signed provenance attestation. Requiresid-token: write.--access public, required for scoped packages on first publish.npm testbefore publish, bad version doesn't ship.
Schritt 4: Trigger a publish
npm version patch # 1.0.0 → 1.0.1, creates git tag v1.0.1, commits
git push origin main --tags
The tag push triggers the workflow. Watch it under Actions:
Run npm publish --provenance --access public
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm notice ✓ Provenance statement uploaded to https://search.sigstore.dev/
+ [email protected]
Sigstore link is the verifiable claim. Anyone can check it later.
Schritt 5: Provenance badge in README
After your first provenance-enabled publish, add to README:
[](https://www.npmjs.com/package/your-mcp-server?activeTab=provenance)
The npm package page now shows a "Provenance" tab listing every release built via OIDC, with the source commit and workflow. Reviewers can verify the published bytes match the GitHub source.
Schritt 6: Confirm setup
# 1. Check that OIDC publish worked (no token used)
gh run list --workflow=publish.yml --limit=1
# Look for "success" status
# 2. Verify provenance on npm
npm view your-mcp-server --json | jq '.dist'
# Should include `signatures` array
# Plus an `attestations` block referencing GitHub Actions
# 3. Public verification (anyone can run this)
npm audit signatures your-mcp-server
# verified registry signatures, audited Y packages in Xs
# Y packages have verified registry signatures
cat package.json 2>/dev/null | python3 -c "import json,sys; p=json.load(sys.stdin); print(\"name:\", p.get(\"name\")); print(\"version:\", p.get(\"version\")); print(\"private:\", p.get(\"private\"))" 2>/dev/null || echo "no package.json"
Schritt 7: Verify
Run academy_validate_step. The validator checks npm view your-mcp-server version returns a version.
For trusted-publishing specifically, the package page on npmjs.com should show:
- ✅ "Provenance" tab with attestations
- ✅ "Built and signed on GitHub Actions" badge
- ✅ Source commit visible per release
If you don't see those, the workflow either didn't request the OIDC token (id-token: write missing) or didn't pass --provenance to npm publish.
Schritt 8: Migration from NPM_TOKEN
If you currently publish with NPM_TOKEN:
- Set up trusted publisher on npm (Step 2).
- Update workflow to remove
NODE_AUTH_TOKENand addid-token: write+--provenance(Step 3). - Test with a
npm version prereleasefirst to verify. - After successful provenance publish, revoke the old
NPM_TOKENin npm Settings → Access Tokens. - Remove the
NPM_TOKENGitHub secret in Settings → Secrets → Actions.
The old static token is now useless. If anyone leaked it, the leak is no longer dangerous, npm rejects it.
Common traps
- Forgetting
id-token: write, workflow runs butnpm publishfails with auth error. Always include the permission. - Workflow filename mismatch, npm trusted publisher config requires exact filename. If you renamed
publish.yml→release.yml, update both sides. - Branch / tag mismatch, if you set the trusted publisher to "main" branch but trigger on tag push, the OIDC claim ref doesn't match. Use
tags: ['v*']trigger and configure the publisher accordingly. environment: productionset on workflow but not on npm, npm rejects with "environment claim mismatch". Set both or neither.--provenancewithoutid-token: write, error "OIDC unavailable". Fix permissions block.- Forgetting to revoke the old
NPM_TOKEN, security debt. After OIDC works, delete the static token.
What good looks like
Tag push triggers the workflow, OIDC exchanges a short-lived token, npm publish --provenance succeeds, npm package page shows the Provenance tab with the signed attestation pointing at the exact commit. No long-lived secrets anywhere. Reviewers see the provenance badge and trust your supply chain a notch more.
For OSS MCP servers, trusted publishing is becoming the table-stakes signal. Set it up once and never think about NPM_TOKEN rotation again.