Skip to content

Adding oauth security with existing basic PAT #422

Merged
ralphbean merged 2 commits intorelease-engineering:uat-instancefrom
Bala-Sakabattula:oauth-authenitcation
Feb 13, 2026
Merged

Adding oauth security with existing basic PAT #422
ralphbean merged 2 commits intorelease-engineering:uat-instancefrom
Bala-Sakabattula:oauth-authenitcation

Conversation

@Bala-Sakabattula
Copy link
Collaborator

Adding oauth logging with existing basic PAT logging for jira.

@qodo-code-review
Copy link

Review Summary by Qodo

Add OAuth 2.0 authentication support for Jira instances

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add OAuth 2.0 authentication support alongside existing PAT method
• Implement token caching to avoid redundant OAuth token requests
• Refactor Jira client initialization to use centralized auth builder
• Add comprehensive tests for both PAT and OAuth2 authentication flows
Diagram
flowchart LR
  A["Jira Instance Config"] --> B["build_jira_client_kwargs"]
  B --> C{Auth Method}
  C -->|PAT| D["Use token_auth or basic_auth"]
  C -->|OAuth2| E["_get_oauth2_token"]
  E --> F{Token Cached?}
  F -->|Yes| G["Return Cached Token"]
  F -->|No| H["_fetch_oauth2_token"]
  H --> I["Request New Token"]
  I --> J["Cache Token"]
  J --> G
  D --> K["JIRA Client"]
  G --> K
Loading

Grey Divider

File Changes

1. sync2jira/jira_auth.py ✨ Enhancement +153/-0

New Jira authentication module with OAuth2 support

• New module implementing OAuth 2.0 and PAT authentication methods
• Implements token caching mechanism to reuse tokens across syncs
• Provides build_jira_client_kwargs() function to construct JIRA client kwargs
• Supports configurable token URL and automatic token refresh with buffer

sync2jira/jira_auth.py


2. sync2jira/downstream_issue.py ✨ Enhancement +4/-1

Refactor Jira client initialization to use auth builder

• Import new build_jira_client_kwargs function from jira_auth module
• Refactor get_jira_client() to use centralized auth builder instead of direct config
• Extract jira instance config and build client kwargs before JIRA instantiation

sync2jira/downstream_issue.py


3. tests/test_downstream_issue.py 🧪 Tests +11/-4

Update tests for refactored Jira client initialization

• Update mock config to use proper PAT auth structure with token_auth and options
• Update test assertions to expect token_auth and options kwargs instead of raw config
• Align test setup with new build_jira_client_kwargs() behavior

tests/test_downstream_issue.py


View more (1)
4. tests/test_main.py 🧪 Tests +109/-1

Add comprehensive tests for PAT and OAuth2 authentication

• Add imports for jira_auth module and build_jira_client_kwargs function
• Add new TestJiraAuth test class with comprehensive authentication tests
• Test PAT authentication with basic_auth and token_auth methods
• Test OAuth2 token caching and refresh behavior with mocked time and requests
• Test error handling for missing credentials and network failures

tests/test_main.py


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 12, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. response.json() unhandled failure 📘 Rule violation ⛯ Reliability
Description
_fetch_oauth2_token() calls response.json() and casts expires_in without handling invalid JSON
or unexpected types, which can raise exceptions without actionable context. This reduces robustness
when the token endpoint returns non-JSON or malformed responses.
Code

sync2jira/jira_auth.py[R68-74]

+    response.raise_for_status()
+    data = response.json()
+    access_token = data.get("access_token")
+    if not access_token:
+        raise ValueError("OAuth 2.0 token response did not contain access_token")
+    expires_in = int(data.get("expires_in", 3600))
+    return access_token, expires_in
Evidence
PR Compliance ID 3 requires handling edge cases and failure points with meaningful context. The new
code parses JSON and coerces expires_in without guarding against JSON decoding/type errors, which
can surface as unhandled exceptions.

Rule 3: Generic: Robust Error Handling and Edge Case Management
sync2jira/jira_auth.py[68-74]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
OAuth2 token parsing is not resilient: `response.json()` and `int(...)` can raise exceptions on malformed responses, producing failures without clear, actionable context.
## Issue Context
Token endpoints sometimes return HTML/text on proxy errors, or JSON with unexpected shapes/types.
## Fix Focus Areas
- sync2jira/jira_auth.py[58-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Retry reuses cached token 🐞 Bug ⛯ Reliability
Description
On JIRAError, the code “refreshes” the Jira client by recreating it, but OAuth2 token retrieval is
cached and may return the same (revoked/invalid) token until expiry. This makes the retry path
ineffective for OAuth2 and can cause repeat failures even after a refresh attempt.
Code

sync2jira/jira_auth.py[R77-96]

+def _get_oauth2_token(
+    client_id: str,
+    client_secret: str,
+    token_url: str = DEFAULT_OAUTH2_TOKEN_URL,
+) -> str:
+    """Return a valid OAuth2 token, reusing cache if not expired (with refresh buffer)."""
+    key = (client_id, client_secret, token_url)
+    now = time.time()
+    entry = _oauth2_token_cache.get(key)
+    if entry:
+        token, expires_at = entry
+        if now < expires_at - OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS:
+            return token
+    token, expires_in = _fetch_oauth2_token(
+        client_id=client_id,
+        client_secret=client_secret,
+        token_url=token_url,
+    )
+    _oauth2_token_cache[key] = (token, now + expires_in)
+    return token
Evidence
The retry logic assumes recreating the Jira client refreshes auth after JIRAError. However,
build_jira_client_kwargs uses _get_oauth2_token(), which returns a cached token until it is near
expiry, so the refreshed client can still be built with the same failing token.

sync2jira/downstream_issue.py[1391-1410]
sync2jira/downstream_pr.py[188-207]
sync2jira/jira_auth.py[82-96]
sync2jira/jira_auth.py[118-140]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Jira retry paths rebuild the Jira client on `JIRAError` expecting refreshed authentication, but for OAuth2 the access token may be returned from `_oauth2_token_cache` until expiry. If Jira rejects a token early (revoked/invalid), the retry will likely reuse the same token and fail again.
### Issue Context
- `sync_with_jira()` for both issues and PRs calls `get_jira_client()` again when retrying after `JIRAError`.
- `get_jira_client()` now uses `build_jira_client_kwargs()`, which uses `_get_oauth2_token()` and the cache.
### Fix Focus Areas
- sync2jira/jira_auth.py[77-96]
- sync2jira/jira_auth.py[118-140]
- sync2jira/downstream_issue.py[1391-1410]
- sync2jira/downstream_pr.py[188-207]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

✅ 3. ValueError leaks exception details 📘 Rule violation ⛨ Security
Description
build_jira_client_kwargs() re-raises token request failures as ValueError embedding the original
exception text, which can expose internal network/implementation details to callers. This risks
leaking sensitive system information in user-visible error output.
Code

sync2jira/jira_auth.py[R136-139]

+        except requests.RequestException as e:
+            log.error("OAuth 2.0 token request failed: %s", e)
+            raise ValueError(f"OAuth 2.0 token request failed: {e}") from e
+        kwargs["token_auth"] = access_token
Evidence
PR Compliance ID 4 requires user-facing errors to be generic and to avoid exposing internal details;
the new ValueError message includes raw exception text ({e}), which commonly contains internal
connection/URL/status details.

Rule 4: Generic: Secure Error Handling
sync2jira/jira_auth.py[136-139]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A raised `ValueError` includes raw exception details (`{e}`), which may be displayed to end users/operators and can leak internal implementation details.
## Issue Context
Secure error handling requires generic outward messages and detailed diagnostics only in internal logs.
## Fix Focus Areas
- sync2jira/jira_auth.py[136-139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. OAuth2 error log lacks context 📘 Rule violation ✓ Correctness
Description
The new OAuth2 token failure log is unstructured and omits key audit context (e.g., Jira
server/instance identifier and outcome metadata), making it harder to reconstruct authentication
events reliably. This reduces auditability and can hinder incident investigations.
Code

sync2jira/jira_auth.py[137]

+            log.error("OAuth 2.0 token request failed: %s", e)
Evidence
PR Compliance ID 1 requires audit trails with essential context to reconstruct critical actions; ID
5 requires structured logs for auditing. The added log line is plain text and does not include
identifying fields (e.g., server/instance/auth_method), limiting audit usefulness.

Rule 1: Generic: Comprehensive Audit Trails
Rule 5: Generic: Secure Logging Practices
sync2jira/jira_auth.py[137-137]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
OAuth2 token failure logging is not structured and lacks sufficient non-sensitive context to serve as an audit trail for authentication events.
## Issue Context
Audit logs should be parseable and include identifiers needed to correlate incidents, without including secrets like `client_secret` or access tokens.
## Fix Focus Areas
- sync2jira/jira_auth.py[118-140]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

✅ 5. OAuth config undocumented 🐞 Bug ✓ Correctness
Description
The repo’s config documentation still only shows PAT/token_auth-based Jira configuration and doesn’t
describe the new auth_method/oauth2 stanza. This increases the chance of misconfiguration when users
try to enable OAuth2.
Code

sync2jira/jira_auth.py[R18-27]

+"""
+Jira authentication helpers.
+
+Supports two authentication methods, selectable per Jira instance in config:
+- PAT (Personal Access Token / API token): use ``auth_method: "pat"`` and
+  ``token_auth`` or ``basic_auth``.
+- OAuth 2.0 2-Legged (2LO) with Atlassian service account: use
+  ``auth_method: "oauth2"`` and ``oauth2`` with ``client_id`` and
+  ``client_secret``.
+"""
Evidence
The new module introduces a new configuration shape for OAuth2 (auth_method/oauth2 client
credentials), but the published config docs still show only token_auth under jira instances.

sync2jira/jira_auth.py[18-27]
docs/source/config-file.rst[49-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Documentation doesn’t explain how to configure the new OAuth2 auth mode.
### Issue Context
The code now supports OAuth2 via `auth_method: &amp;amp;quot;oauth2&amp;amp;quot;` and an `oauth2` stanza, but existing docs only describe `token_auth`.
### Fix Focus Areas
- docs/source/config-file.rst[49-60]
- sync2jira/jira_auth.py[18-27]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines 68 to 74
response.raise_for_status()
data = response.json()
access_token = data.get("access_token")
if not access_token:
raise ValueError("OAuth 2.0 token response did not contain access_token")
expires_in = int(data.get("expires_in", 3600))
return access_token, expires_in

Choose a reason for hiding this comment

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

Action required

1. response.json() unhandled failure 📘 Rule violation ⛯ Reliability

_fetch_oauth2_token() calls response.json() and casts expires_in without handling invalid JSON
or unexpected types, which can raise exceptions without actionable context. This reduces robustness
when the token endpoint returns non-JSON or malformed responses.
Agent Prompt
## Issue description
OAuth2 token parsing is not resilient: `response.json()` and `int(...)` can raise exceptions on malformed responses, producing failures without clear, actionable context.

## Issue Context
Token endpoints sometimes return HTML/text on proxy errors, or JSON with unexpected shapes/types.

## Fix Focus Areas
- sync2jira/jira_auth.py[58-75]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Collaborator

Choose a reason for hiding this comment

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

I disagree with @qodo-code-review: in order to hit a problem, the response would have to return a success status with a malformed payload; in this event, an exception from the json() call or from the int() cast would be caught at the outer level of the code and handled there. I don't think there are any actions that we would take in this code in response to a bad payload, so there is no need for exception handling here.

Choose a reason for hiding this comment

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

Question:

I disagree with @qodo-code-review: in order to hit a problem, the response would have to return a success status with a malformed payload; in this event, an exception from the json() call or from the int() cast would be caught at the outer level of the code and handled there. I don't think there are any actions that we would take in this code in response to a bad payload, so there is no need for exception handling here.

Answer:

You can reduce (or eliminate) this kind of “add local try/except for json()/int()” suggestion by adding a repo-specific instruction to the /improve tool, telling Qodo Merge to avoid recommending local exception handling when exceptions are intentionally handled at a higher layer (and when there’s no meaningful local recovery action).

Add to your .pr_agent.toml (or wiki .pr_agent.toml) something like:

[pr_code_suggestions]
extra_instructions="""\
- Avoid suggesting local try/except blocks around parsing (e.g., response.json(), int(...) casts) when exceptions are already handled by an outer caller and there is no meaningful local recovery action.
- Only suggest adding local error handling if the exception would be uncaught, or if local context/logging materially improves diagnosability beyond existing outer-level handling.
- Prefer verifying whether upstream status checks / outer error handling exists before flagging "unhandled failure".
"""

If you want this to affect the /review tool as well (since the comment looks like “review-style” reliability feedback), add the same idea here too:

[pr_reviewer]
extra_instructions="""\
Avoid flagging missing local try/except around parsing (response.json(), type casts) when the codebase intentionally relies on outer-layer exception handling and there is no local remediation step.
"""

If you share which tool produced the comment in your setup (automatic /review vs /improve vs another workflow), the instructions can be narrowed to match that tool only.

Relevant Sources:

Copy link
Collaborator

Choose a reason for hiding this comment

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

@ralphbean, perhaps this should be added to the changes in #409?

Choose a reason for hiding this comment

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

Question:

@ralphbean, perhaps this should be added to the changes in #409?

Answer:

Yes—this is a good candidate for a repo-level configuration tweak, so future /improve (and optionally /review) runs avoid this kind of “add local try/except around parsing” noise when your codebase intentionally handles exceptions at a higher layer.

Add to the repo’s .pr_agent.toml (or the repo wiki page .pr_agent.toml):

[pr_code_suggestions]
extra_instructions="""\
- Avoid suggesting local try/except blocks around parsing (e.g., response.json(), int(...) casts) when exceptions are already handled by an outer caller and there is no meaningful local recovery action.
- Only suggest adding local error handling if the exception would be uncaught, or if adding local context/logging materially improves diagnosability beyond existing outer-level handling.
- Prefer verifying whether upstream status checks / outer error handling exists before flagging "unhandled failure".
"""

If the comment you’re seeing is coming from /review (or you want the same behavior there too), add:

[pr_reviewer]
extra_instructions="""\
Avoid flagging missing local try/except around parsing (response.json(), type casts) when the codebase intentionally relies on outer-layer exception handling and there is no local remediation step.
"""

If you confirm whether the bot message was generated by /improve vs /review (or an automatic run configured under [github_app].pr_commands), this can be narrowed to only the relevant tool.

Relevant Sources:

Comment on lines 77 to 96
def _get_oauth2_token(
client_id: str,
client_secret: str,
token_url: str = DEFAULT_OAUTH2_TOKEN_URL,
) -> str:
"""Return a valid OAuth2 token, reusing cache if not expired (with refresh buffer)."""
key = (client_id, client_secret, token_url)
now = time.time()
entry = _oauth2_token_cache.get(key)
if entry:
token, expires_at = entry
if now < expires_at - OAUTH2_TOKEN_REFRESH_BUFFER_SECONDS:
return token
token, expires_in = _fetch_oauth2_token(
client_id=client_id,
client_secret=client_secret,
token_url=token_url,
)
_oauth2_token_cache[key] = (token, now + expires_in)
return token

Choose a reason for hiding this comment

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

Action required

2. Retry reuses cached token 🐞 Bug ⛯ Reliability

On JIRAError, the code “refreshes” the Jira client by recreating it, but OAuth2 token retrieval is
cached and may return the same (revoked/invalid) token until expiry. This makes the retry path
ineffective for OAuth2 and can cause repeat failures even after a refresh attempt.
Agent Prompt
### Issue description
The Jira retry paths rebuild the Jira client on `JIRAError` expecting refreshed authentication, but for OAuth2 the access token may be returned from `_oauth2_token_cache` until expiry. If Jira rejects a token early (revoked/invalid), the retry will likely reuse the same token and fail again.

### Issue Context
- `sync_with_jira()` for both issues and PRs calls `get_jira_client()` again when retrying after `JIRAError`.
- `get_jira_client()` now uses `build_jira_client_kwargs()`, which uses `_get_oauth2_token()` and the cache.

### Fix Focus Areas
- sync2jira/jira_auth.py[77-96]
- sync2jira/jira_auth.py[118-140]
- sync2jira/downstream_issue.py[1391-1410]
- sync2jira/downstream_pr.py[188-207]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good catch.

Copy link
Collaborator

@webbnh webbnh left a comment

Choose a reason for hiding this comment

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

Looks good, but it could use a polishing pass.

@webbnh
Copy link
Collaborator

webbnh commented Feb 12, 2026

@Bala-Sakabattula, the review bot brought up a good point: there should probably be a documentation update accompanying this change, and maybe an update to the sample sync2jira.py?

@Bala-Sakabattula Bala-Sakabattula changed the base branch from main to uat-instance February 13, 2026 09:53
@Bala-Sakabattula
Copy link
Collaborator Author

there should probably be a documentation update accompanying this change, and maybe an update to the sample sync2jira.py?

I will updated the documentation and sample part later when we will move the changes to prod

@ralphbean ralphbean merged commit 928310d into release-engineering:uat-instance Feb 13, 2026
6 checks passed
Comment on lines +433 to +436
json=lambda t=access_token, e=expires_in: {
"access_token": t,
"expires_in": e,
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just for future reference (i.e., I'm not actually suggesting a change, here), this approach strikes me as clever but fragile: you're counting on the fact that the json() function will be invoked with no arguments. I don't see any reason why we need to use the default argument values to inject the values -- I'm pretty sure that we can reference them directly:

Suggested change
json=lambda t=access_token, e=expires_in: {
"access_token": t,
"expires_in": e,
},
json=lambda: {
"access_token": access_token,
"expires_in": expires_in,
},

I believe that the closure will capture and return the correct values.

@webbnh
Copy link
Collaborator

webbnh commented Feb 13, 2026

there should probably be a documentation update accompanying this change, and maybe an update to the sample sync2jira.py?

I will updated the documentation and sample part later when we will move the changes to prod

Ha! Promises, promises.... 😉

Also, speaking of accruing technical debt, I don't think you acted on this comment: we need a tweak to get_jira_client() so that this call can avoid using a cached token. (And, sadly, that code is duplicated here, so we need the fix in two places.)

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.

3 participants