Skip to content

feat(jira): Jira Cloud OAuth 2.0 (3LO) runtime — dual-mode alongside Server/DC#501

Merged
CybotTM merged 3 commits into
mainfrom
feat/jira-cloud-oauth2
Jul 2, 2026
Merged

feat(jira): Jira Cloud OAuth 2.0 (3LO) runtime — dual-mode alongside Server/DC#501
CybotTM merged 3 commits into
mainfrom
feat/jira-cloud-oauth2

Conversation

@CybotTM

@CybotTM CybotTM commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

Implements the runtime half of the Jira Cloud plan started in #416 ("PR-2..4" of the original series): TimeTracker now syncs worklogs against both Jira Server/DC (OAuth 1.0a, unchanged) and Jira Cloud (OAuth 2.0 authorization-code / 3LO), selected per ticket system via the deployment type that #416 introduced.

How it works

  • JiraCloudApiService extends the existing service and swaps only the transport/auth layer: 3LO authorize redirect, authorization-code exchange, rotating refresh tokens with recorded expiry (via the app's ClockInterface, frozen-time safe), automatic cloudId resolution from accessible-resources (matched by site host), Bearer-authenticated REST through api.atlassian.com/ex/jira/{cloudId}/rest/api/2/, and the Cloud-only search/jql endpoint. All worklog/issue/subticket operations of the base class run unchanged on top.
  • CloudOAuthStateCodec: Cloud redirect URIs must match the registered URL exactly, so the ticket-system id cannot ride as ?tsid= like in the OAuth1 flow — it travels inside the encrypted, user-bound, 10-minute-TTL state parameter instead.
  • JiraOAuthApiFactory branches on DeploymentType — every existing caller (worklog subscriber, subticket sync, exports, admin sync) gets the right transport transparently.
  • The shared /jiraoauthcallback route serves both flows (a state parameter selects the Cloud branch; error=access_denied is reported cleanly).
  • REST v2 is pinned deliberately: v3 requires Atlassian Document Format for worklog comments; v2 keeps accepting the plain-text comments the app sends.

Docs: ADR-017 updated to "implemented" with the mechanism recorded; Cloud app-registration walkthrough (scopes, exact callback URL) added to docs/configuration.md; README feature bullet updated.

Context: the Mogic fork's Cloud adaptation (mogic-le/timetracker, TIM-130) confirmed search/jql as the required Cloud search endpoint; their fork still uses deprecated OAuth 1.0a against Cloud — this PR provides the supported 3LO path instead.

Verification

  • 21 new unit tests: state codec (round-trip, tamper, cross-key, TTL expiry), authorize-URL construction with decodable state, Bearer client against the gateway URL, token refresh & rotation incl. rejected-refresh cleanup + re-authorize, cloudId host matching (case-insensitive) and no-match failure, malformed token responses, search/jql payload, OAuth1-callback rejection, bare callback URL
  • Full unit suite: 1575 tests green
  • PHPStan level 10 clean, PHPat clean, PER-CS clean

Out of scope

…Server/DC

Implements the runtime half of the Cloud plan from #416 (PR-2..4):

- JiraCloudApiService (extends the OAuth1 monolith): 3LO authorize
  redirect, authorization-code exchange, rotating refresh tokens with
  recorded expiry (App ClockInterface — frozen-time safe), automatic
  cloudId resolution via accessible-resources, Bearer REST through
  api.atlassian.com/ex/jira/{cloudId}/rest/api/2/ (v2 keeps plain-text
  worklog comments), Cloud-only search/jql endpoint
- CloudOAuthStateCodec: the ticket-system id rides encrypted inside the
  OAuth state (Cloud redirect URIs must match the registration exactly,
  so no ?tsid= like the OAuth1 flow), bound to the user, 10-min TTL
- JiraOAuthApiFactory branches on DeploymentType — every existing
  caller (worklog subscriber, subticket sync, exports) gets the right
  transport transparently; Server/DC OAuth 1.0a is untouched
- shared /jiraoauthcallback serves both flows (state => Cloud branch)
- tokens stored encrypted at rest, tokensecret unused on Cloud

Docs: ADR-017 marked implemented, Cloud app-registration walkthrough in
configuration.md, README feature bullet.

Verified: 21 new unit tests (state codec round-trip/tamper/expiry,
authorize URL + decodable state, token rotation incl. rejected-refresh
cleanup, cloudId host matching, search/jql payload); full unit suite
1575 green; PHPStan level 10, PHPat, PER-CS clean.

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
Copilot AI review requested due to automatic review settings July 2, 2026 12:41
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.25751% with 46 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.03%. Comparing base (9cbe141) to head (342ba47).

Files with missing lines Patch % Lines
src/Controller/Default/JiraOAuthCallbackAction.php 0.00% 28 Missing ⚠️
...c/Service/Integration/Jira/JiraCloudApiService.php 90.44% 17 Missing ⚠️
.../Service/Integration/Jira/CloudOAuthStateCodec.php 95.45% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #501      +/-   ##
============================================
- Coverage     84.15%   84.03%   -0.13%     
- Complexity     2814     2894      +80     
============================================
  Files           187      189       +2     
  Lines          7588     7819     +231     
============================================
+ Hits           6386     6571     +185     
- Misses         1202     1248      +46     
Flag Coverage Δ
integration 51.59% <0.85%> (-1.56%) ⬇️
unit 50.41% <80.25%> (+0.90%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Jira Cloud (OAuth 2.0 / 3LO) integration with rotating refresh tokens, adding the JiraCloudApiService and CloudOAuthStateCodec alongside corresponding unit tests and documentation. The code review identified a critical self-assignment bug in JiraOAuthCallbackAction where the token encryption service is assigned to itself instead of the method parameter, and suggested simplifying the controller's dependencies by injecting CloudOAuthStateCodec directly. Additionally, a potential runtime TypeError was pointed out in JiraCloudApiService due to DateTimeImmutable::modify() potentially returning false, which should be explicitly checked before setting the token expiration.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/Controller/Default/JiraOAuthCallbackAction.php
Comment thread src/Controller/Default/JiraOAuthCallbackAction.php Outdated
Comment thread src/Controller/Default/JiraOAuthCallbackAction.php Outdated
Comment thread src/Service/Integration/Jira/JiraCloudApiService.php

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds runtime support for Jira Cloud alongside existing Jira Server/Data Center integration by introducing an OAuth 2.0 (3LO) transport/auth layer and routing the callback/factory based on ticket-system deployment type.

Changes:

  • Introduces JiraCloudApiService (OAuth2 3LO + rotating refresh tokens, cloudId resolution, gateway REST v2, Cloud search/jql) while reusing the base Jira API operations.
  • Branches JiraOAuthApiFactory by DeploymentType and updates the shared /jiraoauthcallback controller to handle both OAuth1 (Server/DC) and OAuth2 (Cloud).
  • Adds unit tests for the new Cloud OAuth state codec, token lifecycle, cloudId matching, and factory branching; updates docs/README/ADR accordingly.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Service/Integration/Jira/JiraOAuthApiFactoryTest.php Updates factory tests to cover Server vs Cloud service selection and the new clock dependency.
tests/Service/Integration/Jira/JiraCloudApiServiceTest.php Adds unit tests covering Cloud OAuth2 authorize/exchange/refresh, cloudId resolution, and Cloud-specific REST behavior.
tests/Service/Integration/Jira/CloudOAuthStateCodecTest.php Adds tests for encrypted OAuth2 state encoding/decoding, tamper resistance, and TTL expiry.
src/Service/Integration/Jira/JiraOAuthApiService.php Makes decryptStored() overridable for Cloud subclass token handling.
src/Service/Integration/Jira/JiraOAuthApiFactory.php Adds DeploymentType branching and injects ClockInterface for Cloud token expiry logic.
src/Service/Integration/Jira/JiraCloudApiService.php New Cloud implementation: 3LO flow, rotating refresh tokens, gateway REST v2, cloudId lookup, and search/jql.
src/Service/Integration/Jira/CloudOAuthStateCodec.php New encrypted, TTL-bound OAuth2 state codec carrying user + ticket-system identity.
src/Controller/Default/JiraOAuthCallbackAction.php Extends callback route to handle both OAuth1 (Server/DC) and OAuth2 (Cloud) callbacks.
README.md Updates Jira integration feature statement to include Cloud (OAuth2/3LO).
docs/configuration.md Documents Jira Cloud app registration (scopes + exact callback URL) and user authorization flow.
docs/adr/ADR-017-jira-cloud-oauth2.md Updates ADR status/record to reflect the implemented runtime mechanism.

Comment thread src/Service/Integration/Jira/JiraCloudApiService.php
Comment thread src/Service/Integration/Jira/JiraCloudApiService.php
Comment thread src/Service/Integration/Jira/JiraCloudApiService.php Outdated
Comment thread src/Controller/Default/JiraOAuthCallbackAction.php
CybotTM added 2 commits July 2, 2026 14:49
- JiraOAuthCallbackAction gets CloudOAuthStateCodec injected directly
  instead of assembling it from TokenEncryptionService + Clock (Gemini)
- #[Override] attributes and a native never return type on the Cloud
  service's overrides (Rector gate that CI runs but my local gate list
  missed — now part of it)

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
- read the access token through the repository-loaded row instead of
  the User collection (single source of truth; survives first-auth and
  rotation in the same process)
- only a definitive 400/401/403 from the token endpoint clears the
  grant and restarts authorization; transient failures (network, 5xx)
  rethrow and keep the rotating refresh token for the next attempt
  (+ regression test)
- cache the tenant REST client in one slot rebuilt on rotation instead
  of accumulating per-token entries
- authorization-denied callback answers 400 instead of 200

Signed-off-by: Sebastian Mendel <info@sebastianmendel.de>
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

@CybotTM CybotTM merged commit d6f16d8 into main Jul 2, 2026
26 checks passed
@CybotTM CybotTM deleted the feat/jira-cloud-oauth2 branch July 2, 2026 13:01
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