Skip to content

feat: add OAuth 2.0 PKCE authentication support#218

Open
mstrlc wants to merge 2 commits into
freema:mainfrom
mstrlc:feat/oauth2-support
Open

feat: add OAuth 2.0 PKCE authentication support#218
mstrlc wants to merge 2 commits into
freema:mainfrom
mstrlc:feat/oauth2-support

Conversation

@mstrlc

@mstrlc mstrlc commented May 18, 2026

Copy link
Copy Markdown

Summary

  • Adds JIRA_AUTH_TYPE=oauth mode alongside the existing basic auth (API token) — no breaking changes for existing users
  • Implements full OAuth 2.0 PKCE flow: browser-based authorization, automatic token refresh, and persistent token storage at ~/.mcp-jira-stdio/tokens.json (mode 600)
  • OAuth token and Atlassian Cloud ID are injected per-request via an async Axios interceptor, so cloud ID discovery happens transparently after the first authorization

Motivation

Some Atlassian organizations disable personal API token access for security policy reasons. This PR makes the server usable in those environments without requiring any changes to existing basic-auth setups.

Changes

New file

  • src/utils/oauth.ts — PKCE authorization flow, token exchange, refresh, file-based + in-memory cache

Modified files

  • src/types/common.tsJiraAuthConfig.email/apiToken made optional; authType field added
  • src/utils/jira-auth.tsvalidateAuth(), getAuthenticatedClient(), and getMultipartClient() branch on JIRA_AUTH_TYPE; OAuth path uses async interceptor to inject Bearer token + correct base URL per request
  • src/index.ts — Startup log handles OAuth path
  • .env.example — Documents the three new env vars

Docs

  • README.md — OAuth 2.0 Setup section, updated Quick Install / Configuration / MCP client config / env vars table / troubleshooting
  • DOCKER.md — OAuth docker run examples with token persistence and callback port

Environment variables

Variable Required Notes
JIRA_AUTH_TYPE No basic (default) or oauth
JIRA_OAUTH_CLIENT_ID Yes (OAuth) From Atlassian Developer Console
JIRA_OAUTH_CLIENT_SECRET Yes (OAuth) From Atlassian Developer Console

Callback URL to register in the Atlassian app: http://localhost:7789/callback

Required OAuth scopes: read:jira-user read:jira-work write:jira-work offline_access

Test plan

  • Basic auth still works with existing JIRA_EMAIL + JIRA_API_TOKEN config (no env change needed)
  • JIRA_AUTH_TYPE=oauth with valid client credentials opens browser, completes authorization, and stores tokens in ~/.mcp-jira-stdio/tokens.json
  • Second run uses stored tokens without browser interaction
  • Expired access token is refreshed automatically using the stored refresh token
  • Missing OAuth env vars produce a clear error message at startup

mstrlc and others added 2 commits May 18, 2026 12:21
Adds JIRA_AUTH_TYPE=oauth mode alongside existing basic auth.
Implements full PKCE flow with browser-based authorization,
automatic token refresh, and persistent token storage at
~/.mcp-jira-stdio/tokens.json.

Co-authored-by: Claude <claude@anthropic.com>
Add setup instructions, environment variable table, troubleshooting
entries, and Docker usage examples for the new JIRA_AUTH_TYPE=oauth mode.

Co-authored-by: Claude <claude@anthropic.com>
@mstrlc mstrlc force-pushed the feat/oauth2-support branch from 1225ea8 to 09cd7ba Compare May 18, 2026 10:23
@freema

freema commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Thanks a lot for this PR, @mstrlc — OAuth 2.0 PKCE support is a genuinely useful addition for orgs that disable personal API tokens, and the overall shape (opt-in via JIRA_AUTH_TYPE=oauth, no breaking changes for existing basic-auth users, per-request token + cloud-id injection) is solid. 🙏

I did a thorough review. Before merging I'd like a few must-fix items addressed, all in src/utils/oauth.ts / src/utils/jira-auth.ts. None of them are structural — the design is good.

Must-fix (blocking)

  1. Token file permissions aren't enforced on rewrite.
    saveStore uses writeFileSync(TOKEN_FILE, ..., { mode: 0o600 }), but Node only applies mode when creating a file — on an overwrite of an existing file it's a no-op. Since the store is rewritten on every auth/refresh, any tokens.json that ever existed with looser perms (different umask, restore from backup, Docker bind-mount, manual edit) keeps them, so the access/refresh tokens can stay world-readable. The README explicitly promises mode 600.
    Fix: after writing, unconditionally fs.chmodSync(TOKEN_FILE, 0o600) — or write atomically (temp file created with mode: 0o600 + rename).

  2. Token directory is created world-traversable.
    mkdirSync(TOKEN_DIR, { recursive: true }) with a typical umask 022 yields 0o755. The token file itself is 0o600 so the secret isn't directly exposed, but on a shared host the directory is still traversable (defense-in-depth).
    Fix: mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 }) + an explicit chmodSync(TOKEN_DIR, 0o700).

  3. Concurrent first requests race the authorization flow.
    The OAuth token is fetched lazily inside the async request interceptor, which runs per request, and getOAuthToken has no in-flight dedup (the in-memory cache is only set after await authorize()). The MCP server dispatches tools/call requests concurrently, so two simultaneous first requests start two authorize() flows: the second server.listen(7789) fails with EADDRINUSE and two browser windows open with mismatched state/verifier pairs.
    Fix: memoize a module-scoped in-flight Promise (single-flight) for both the initial authorize and the refresh path, cleared once it settles.

  4. OAuth errors are masked as NETWORK_ERROR and then retried 3×.
    When getOAuthToken fails (cancelled auth, EADDRINUSE, cloud-id not found, token-exchange failure), the rejected request has no error.response, so the response-error interceptor maps it to ERROR_MESSAGES.NETWORK_ERROR and discards the real message (e.g. "Jira site not found in accessible resources"). The retry loop in makeJiraRequest only short-circuits on error.response?.status 4xx, so a failed OAuth flow retries up to 3×, re-spawning the callback server/browser each time.
    Fix: detect non-HTTP/auth errors (no .response) and break out of the retry loop; don't rewrite the original OAuth message to NETWORK_ERROR.

CI

The CI workflow hadn't run on this PR (fork PR — it needs maintainer approval). I'm approving the run now so we can see lint / typecheck / test:coverage / build across Node 20/22/24 against the new code (the diff also includes some formatting changes that need to pass format:check).

Not blocking

  • I originally worried the lack of tests would fail an 80% coverage gate — that turned out not to be the case (the gate in vitest.config.ts uses the key threshold instead of thresholds, so it's silently inactive; the real gate is Codecov at 50/40% with fail_ci_if_error: false). So no hard blocker on coverage, but unit tests for oauth.ts (PKCE generation, cache hit, stored-token reuse, refresh success/failure-fallback, cloud-id match/no-match) and the new validateAuth/client-factory branches would be very welcome as hygiene.
  • Optional polish: bind the callback server to 127.0.0.1 rather than all interfaces; run validateAuth() at the start of the OAuth branches too; make the AUTH_REQUIRED message authType-aware; consider an env override for the hardcoded port 7789.

Happy to re-review as soon as the four items above are in and CI is green. Thanks again for the contribution! 🚀

@freema

freema commented Jun 16, 2026

Copy link
Copy Markdown
Owner

CI ran now — it fails only at the lint step (the other matrix jobs were cancelled by fail-fast, not real failures). npm run lint reports 35 problems:

  • 34 are prettier/prettier formatting (trailing commas, line wrapping) in src/utils/jira-auth.ts and src/utils/oauth.ts — all auto-fixable. Just run npm run format (or eslint --fix) and commit.
  • 1 real lint error: src/utils/jira-auth.ts:43'addInterceptors' is defined but never used (@typescript-eslint/no-unused-vars). Either wire it up or remove it (or prefix with _ if intentionally unused).

Once lint is green the job will continue to typecheck → test:coverage → build, so we'll get full signal. 👍

@freema

freema commented Jul 13, 2026

Copy link
Copy Markdown
Owner

@mstrlc thanks for the PR and for adding OAuth 2.0 PKCE support! 🙏

The test suite is currently failing on all Node versions (test (20.x), test (22.x), test (24.x)) — the build job passes, so it looks isolated to the tests. Could you take a look and get CI green? I'll review and merge once the tests pass. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants