Skip to content

feat(auth): add OAuth 2.0 login flow for Bitbucket Cloud#152

Merged
avivsinai merged 1 commit intoavivsinai:masterfrom
ekrako:136-oauth-login-flow
Apr 12, 2026
Merged

feat(auth): add OAuth 2.0 login flow for Bitbucket Cloud#152
avivsinai merged 1 commit intoavivsinai:masterfrom
ekrako:136-oauth-login-flow

Conversation

@ekrako
Copy link
Copy Markdown
Contributor

@ekrako ekrako commented Apr 11, 2026

Summary

Adds browser-based OAuth 2.0 login for Bitbucket Cloud via bkt auth login --web.
Short-lived access tokens are stored as JSON blobs in the keyring and transparently
refreshed on 401. The existing API-token flow is preserved under --web-token.

Closes #136 (Phase 2).

Changes

OAuth flow (pkg/oauth/flow.go)

  • Full authorization code flow: localhost callback server → browser → code exchange → /2.0/user verification
  • RefreshToken() for transparent 401-triggered refresh
  • Output routed through io.Writer (not stdout) for testability
  • User identity: prefers username > account_id, never stores display_name as the API username

Auth command (pkg/cmd/auth/auth.go)

  • --web on Cloud runs OAuth flow; --web on DC errors with guidance to use --web-token
  • --web-token replaces old --web behavior (opens token management page)
  • --web and --web-token are mutually exclusive
  • auth status shows OAuth token expiry (hidden when BKT_TOKEN overrides)
  • Guard: --web fails fast if OAuth client credentials are missing from the build

Token refresh wiring (pkg/cmdutil/client.go)

  • NewCloudClient wires TokenRefresher for OAuth hosts using keyring tokens
  • Bearer auth + refresher only when using keyring; BKT_TOKEN overrides default to basic
  • Missing client credentials checked at refresh time, not construction — local builds work until token expires

Token detection (pkg/cmdutil/context.go)

  • loadHostToken detects JSON blob in keyring → extracts access_token, sets auth_method=oauth
  • BKT_TOKEN path now applies BKT_USERNAME and BKT_AUTH_METHOD overrides (fixes OAuth host + env token)
  • Requires BKT_USERNAME when overriding an OAuth Cloud host (prevents silent 401 from wrong username)

Credentials injection (pkg/oauth/cloud.go, Makefile, .goreleaser.yaml, release.yml)

  • OAuth client_id/client_secret injected at build time via -ldflags -X
  • Runtime fallback to BKT_OAUTH_CLIENT_ID/BKT_OAUTH_CLIENT_SECRET env vars for go install builds
  • Release workflow validates secrets are non-empty before goreleaser publishes

Cloud client (pkg/bbcloud/client.go)

  • Added AuthMethod and TokenRefresher to Options
  • Auth method selection: explicit > refresher-implied-bearer > default-basic

Skill docs

  • Updated SKILL.md, auth.md (regenerated), headless.md with OAuth env vars and examples

Testing

  • go test ./... — 1013 tests pass
  • Pre-commit hooks pass
  • Manual end-to-end: bkt auth login --web → browser consent → token stored → auth status shows expiry
  • Manual: --web without build creds → clear error; --web on DC → error with --web-token guidance
  • New tests in pkg/oauth/flow_test.go: authorize URL, code exchange, refresh, state mismatch, full flow, error paths, randomState

Risks

  • BKT_OAUTH_CLIENT_ID/BKT_OAUTH_CLIENT_SECRET GitHub secrets must be added by repo admin before the next release (comment posted on feat(auth): OAuth 2.0 login for Cloud and Data Center #136)
  • Repo admin should rotate the OAuth consumer credentials since the original values were exposed in issue comments

@ekrako ekrako marked this pull request as ready for review April 11, 2026 20:19
@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 11, 2026

Codecov Report

❌ Patch coverage is 63.23529% with 125 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/cmd/auth/auth.go 44.73% 76 Missing and 8 partials ⚠️
pkg/oauth/flow.go 74.35% 16 Missing and 14 partials ⚠️
pkg/cmdutil/client.go 71.05% 9 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

Comment thread pkg/oauth/cloud_test.go Fixed
Comment thread pkg/oauth/cloud_test.go Outdated
orig := cloudClientSecret
defer func() { cloudClientSecret = orig }()

cloudClientSecret = "build-secret" // gitleaks:allow
@ekrako ekrako force-pushed the 136-oauth-login-flow branch 7 times, most recently from 2b7750f to e5aee7d Compare April 11, 2026 21:04
- pkg/oauth/flow.go: full authorization code flow with localhost callback,
  state validation, code exchange, and /2.0/user verification
- pkg/cmd/auth: --web runs OAuth for Cloud, --web-token opens token page;
  auth status shows OAuth expiry; mutual exclusion enforced
- pkg/cmdutil: wire TokenRefresher for OAuth hosts, detect JSON blob in
  keyring, apply BKT_USERNAME/BKT_AUTH_METHOD on env token override
- pkg/oauth/cloud.go: client creds via ldflags with runtime env var fallback
  for go install builds
- Build/CI: Makefile, goreleaser, release workflow inject and validate
  OAuth secrets

Closes avivsinai#136.
@ekrako ekrako force-pushed the 136-oauth-login-flow branch from e5aee7d to 33de012 Compare April 11, 2026 21:14
Copy link
Copy Markdown
Owner

I reviewed the auth flow more closely and compared it against a few mature CLIs.

The important bit: I am not comfortable claiming that Bitbucket Cloud can be supported as a true secretless/public-client flow today.

What I found:

  • Atlassian's Bitbucket Cloud docs still show client_id:client_secret at token exchange/refresh.
  • gh is not a good model here: its localhost web flow also uses an embedded/injected client secret and no PKCE.
  • glab is a stronger modern example: public client + PKCE.
  • The most relevant Bitbucket-specific precedent I found is Git Credential Manager, and that appears to do PKCE plus a client secret, not PKCE-only.

So as I see it, the choices are:

  1. Keep this PR as-is: loopback redirect + state + secret-backed code exchange/refresh. This is the path I believe we can support with confidence right now.
  2. Add PKCE on top of the current flow as a hardening improvement, but still keep the client secret for Bitbucket until we have live proof that secretless/public-client works end-to-end.

I would not merge a change that assumes PKCE means we can drop the secret unless we verify that against a real Bitbucket OAuth consumer first.

@ekrako what do you prefer here:

  • merge the current secret-backed flow now, or
  • update this PR to add PKCE as well before merge?

@ekrako
Copy link
Copy Markdown
Contributor Author

ekrako commented Apr 12, 2026

Let's merge the current flow as-is — it's working end-to-end and matches gh's approach. I'll open a follow-up PR to add PKCE on top.

@avivsinai
Copy link
Copy Markdown
Owner

avivsinai commented Apr 12, 2026 via email

Copy link
Copy Markdown
Owner

@avivsinai avivsinai left a comment

Choose a reason for hiding this comment

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

Reviewed. Merging the current secret-backed OAuth flow now; PKCE can follow as a hardening pass in a separate PR.

@avivsinai avivsinai merged commit c07fc0f into avivsinai:master Apr 12, 2026
7 checks passed
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.

feat(auth): OAuth 2.0 login for Cloud and Data Center

3 participants