Skip to content

feat(auth): add OIDC device authorization flow for CLI authentication - #29

Merged
chadcrum merged 14 commits into
dcm-project:mainfrom
chadcrum:flpath-4477-dcm-cli-oidc-auth
Aug 13, 2026
Merged

feat(auth): add OIDC device authorization flow for CLI authentication#29
chadcrum merged 14 commits into
dcm-project:mainfrom
chadcrum:flpath-4477-dcm-cli-oidc-auth

Conversation

@chadcrum

@chadcrum chadcrum commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

OIDC device authorization for the DCM CLI - dcm login / dcm logout, token storage (keyring with file fallback), authenticated HTTP transport with refresh, and a DCM_TOKEN / --token bypass for CI.

Split into three commits for easier review:

  1. feat(auth): core code - auth / commands / config
  2. test(auth): all test files
  3. chore(auth): go.mod / go.sum + CLAUDE.md / README.md

Related PRs

Closes https://issues.redhat.com/browse/FLPATH-4477

@chadcrum
chadcrum force-pushed the flpath-4477-dcm-cli-oidc-auth branch 3 times, most recently from 0cf3cf1 to df8df45 Compare August 5, 2026 16:51
chadcrum and others added 3 commits August 5, 2026 12:58
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for the DCM
CLI. This adds `dcm login` and `dcm logout` commands, token storage
with OS keyring primary and file fallback, and an authenticated HTTP
transport with lazy token loading and auto-refresh.

Key capabilities:
- `dcm login` performs OIDC device flow via Keycloak dcm-cli client
- `dcm logout` revokes refresh token and clears stored credentials
- AuthTransport injects Bearer tokens with automatic refresh on expiry
- DCM_TOKEN / --token bypasses OIDC flow for CI/scripting
- Config persistence: login writes issuer-url to the active config file
- Token file permissions: 0600 file, 0700 directory
- HTTP scheme warning when sending tokens over unencrypted connections
- Login/logout and token refresh use TLS from the issuer URL/base transport

Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Expand unit and command tests with an OIDC mock server, covering device
flow login/logout, AuthTransport refresh behavior, token store fallbacks,
and FileStore inaccessible-path error handling.

Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Update go.mod/go.sum for OIDC and keyring dependencies, and document
login/logout, issuer URL, token bypass, and auth architecture in
README.md and CLAUDE.md.

Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@chadcrum
chadcrum force-pushed the flpath-4477-dcm-cli-oidc-auth branch from df8df45 to c761e33 Compare August 5, 2026 16:59
Comment thread internal/auth/transport_test.go Outdated
Rename unused Save params and gofumpt-align the AuthTransport literal so CI lint passes.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread internal/commands/login.go Outdated
Comment thread internal/auth/token.go Outdated
Comment thread CLAUDE.md Outdated
Comment thread README.md Outdated
Comment thread README.md
Comment thread internal/auth/transport.go Outdated

@gabriel-farache gabriel-farache left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review done with an agent. It supported the other comments and added one

Comment thread internal/auth/transport.go Outdated
@gabriel-farache

Copy link
Copy Markdown
Contributor

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Windows cmd URL injection ✓ Resolved 🐞 Bug ⛨ Security
Description
On Windows, openBrowser executes cmd.exe /c start <url> with a verification URL taken directly
from the OIDC device authorization response, allowing cmd.exe metacharacter parsing and potential
command execution. A malicious/compromised issuer (or tampered issuer response) can trigger code
execution when dcm login opens the browser.
Code

internal/auth/auth.go[R154-157]

+		cmd = exec.Command("open", url)
+	case "windows":
+		cmd = exec.Command("cmd", "/c", "start", url)
+	default:
Evidence
DeviceLogin selects openURL from provider-supplied verification URI fields and calls
openBrowser(openURL). On Windows, openBrowser shells out via cmd.exe /c start, introducing a
shell interpretation boundary for attacker-controlled URL content.

internal/auth/auth.go[49-52]
internal/auth/auth.go[67-68]
internal/auth/auth.go[150-160]

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

## Issue description
`openBrowser` uses `exec.Command("cmd", "/c", "start", url)` on Windows. Because `cmd.exe` parses metacharacters and `start` is a shell builtin, a crafted URL can be interpreted as additional commands.

## Issue Context
The URL passed to `openBrowser` is derived from `devAuth.VerificationURIComplete` / `devAuth.VerificationURI` returned by the OIDC provider during device authorization.

## Fix Focus Areas
- internal/auth/auth.go[150-160]

## Suggested fix
- Avoid invoking `cmd.exe` entirely on Windows. Prefer a Windows-native URL opener that does not involve shell parsing (e.g., `rundll32 url.dll,FileProtocolHandler <url>`), or use a dedicated library.
- Additionally, parse/validate the URL and only allow expected schemes (typically `http`/`https`) before attempting to open it.
- Add a unit test (Windows build-tagged if needed) ensuring special characters in the URL do not result in extra argv segments/shell interpretation.

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



Remediation recommended

2. Stale token endpoint saved ✓ Resolved 🐞 Bug ☼ Reliability
Description
AuthTransport.refreshToken uses current.TokenEndpoint (which may come from a reloaded TokenData)
for the refresh request, but then persists TokenEndpoint from the pre-lock tokenData argument.
This can overwrite newer stored endpoint metadata and cause later refreshes to target the wrong
token endpoint.
Code

internal/auth/transport.go[R115-118]

+		RefreshToken:  newToken.RefreshToken,
+		IDToken:       idToken,
+		Expiry:        newToken.Expiry,
+		TokenEndpoint: tokenData.TokenEndpoint,
Evidence
The function explicitly switches current to reloaded and uses current.TokenEndpoint for the
refresh request, but constructs the persisted refreshed token with `TokenEndpoint:
tokenData.TokenEndpoint, which can be stale relative to current`.

internal/auth/transport.go[74-86]
internal/auth/transport.go[91-96]
internal/auth/transport.go[112-119]

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

## Issue description
During refresh, `refreshToken` may switch to `current = reloaded`, but the persisted refreshed TokenData writes `TokenEndpoint` from the older `tokenData` argument instead of from `current`.

## Issue Context
This code is in a `RoundTripper` and can be hit under concurrent request usage of the same `http.Client`.

## Fix Focus Areas
- internal/auth/transport.go[82-119]

## Suggested fix
- Build the refreshed TokenData using `current` consistently:
 - `TokenEndpoint: current.TokenEndpoint`
- Consider adding a regression test that:
 - Loads an expired token with TokenEndpoint=A
 - Before refresh persists, make the store return a reloaded expired token with TokenEndpoint=B
 - Assert the final stored TokenEndpoint is B (the one actually used to refresh).

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


3. HomeDir error ignored ✓ Resolved 🐞 Bug ⛨ Security
Description
The file token store ignores os.UserHomeDir errors and falls back to using an empty home value,
which results in writing credentials under a relative .dcm/ directory. This can persist tokens in
an unexpected working directory and increase the risk of accidental credential
exposure/misplacement.
Code

internal/auth/token.go[R137-140]

+func newFileStore() *fileStore {
+	home, _ := os.UserHomeDir()
+	return &fileStore{dir: filepath.Join(home, ".dcm")}
+}
Evidence
newFileStore drops the UserHomeDir error and uses the possibly-empty home to construct the token
directory, and writeAll then creates/writes under that directory, making token persistence location
dependent on the current working directory.

internal/auth/token.go[137-140]
internal/auth/token.go[164-177]

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

## Issue description
`newFileStore()` discards the error from `os.UserHomeDir()`. When it fails, the resulting path becomes relative (e.g., `.dcm/tokens.json`), so credentials may be stored in the process working directory.

## Issue Context
This path is used when keyring access is unavailable and the code falls back to the file-backed store.

## Fix Focus Areas
- internal/auth/token.go[137-140]
- internal/auth/token.go[164-178]

## Suggested fix
- Do not silently fall back to a relative directory.
- Prefer changing store construction to surface an error (e.g., `NewTokenStore() (TokenStore, error)`), and handle it in callers (`login`, `logout`, and `buildHTTPClient`).
- If changing the API is too invasive, at minimum:
 - Detect `UserHomeDir` error and make file-store operations fail fast with a clear error rather than writing to a relative path (e.g., set an invalid dir and return descriptive errors from Save/Load/Delete).

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


Grey Divider

Context
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/auth/auth.go Outdated
Comment thread internal/auth/transport.go Outdated
Comment thread internal/auth/token.go Outdated
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@chadcrum
chadcrum requested a review from vkolodny August 7, 2026 14:54
chadcrum and others added 4 commits August 7, 2026 13:57
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
chadcrum and others added 4 commits August 10, 2026 10:32
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@chadcrum

Copy link
Copy Markdown
Contributor Author

Review done with an agent. It supported the other comments and added one

Thanks @gabriel-farache - the TLS refresh one was valid. Fixed in 0152e50 (issuer-derived RefreshTransport).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@gciavarrini gciavarrini left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

lgtm

vkolodny added a commit to dcm-project/utilities that referenced this pull request Aug 11, 2026
## Summary
- Trim auth test plan E2E gaps to **TC-36 – TC-42** (CLI and
JWT-negative cases removed from this plan)
- Block SP / instance happy path on
[FLPATH-4622](https://redhat.atlassian.net/browse/FLPATH-4622); UI path
on [FLPATH-4645](https://redhat.atlassian.net/browse/FLPATH-4645)
- Fix `POST /catalog-item-instances` (HTTP 201), provider
`health_status`, port convention (`:8080` local / `:9080` Ecosystem
Jenkins)
- Add subsystem + E2E checklist tables with ❗ markers; document Jenkins
dead `--auth-enabled` on `run-e2e.sh`
- Clarify TC-08 as control-plane API only

## Out of scope here
- CLI auth →
[utilities#34](#34) /
[cli#29](dcm-project/cli#29)
- Wrong audience / `alg:none` → ❗ should cover on TC-14/TC-15
(subsystem)

## Test plan
- [x] Review feedback addressed (Gloria / Chad)
- [ ] Confirm sanitization notice still holds
- [ ] Optional: pipeline fix for dead `--auth-enabled`; subsystem
wrong-aud / alg:none follow-ups

Signed-off-by: Vladislav Kolodny <vkolodny@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
chadcrum added a commit to dcm-project/utilities that referenced this pull request Aug 11, 2026
)

## Summary
- Add the FLPATH-4477 DCM CLI OIDC authentication e2e test plan under
`test-plans/`
- Relocate it from dcm-project/cli because the cases need a live
Keycloak/control-plane stack

## Related PRs
- Spec: dcm-project/cli#30
- Implementation: dcm-project/cli#29

---------

Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

@jordigilh jordigilh 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.

Our thread (unused issuerURL + gofumpt in transport_test.go) is fixed. No further concerns.

@chadcrum
chadcrum merged commit 1759958 into dcm-project:main Aug 13, 2026
4 checks passed
chadcrum added a commit that referenced this pull request Aug 13, 2026
## Summary
- Add FLPATH-4477 OIDC auth `.ai` spec and design decisions
- Point `dcm-cli.spec.md` at the new OIDC auth spec and remove auth from
out-of-scope
- Split docs from the CLI implementation so review can land
independently

## Related PRs
- Implementation: #29
- E2E test plan: dcm-project/utilities#34

---------

Signed-off-by: Chad Crum <ccrum@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

4 participants