Skip to content

fix: add Microsoft 365 OAuth login#44

Merged
namastex888 merged 2 commits into
mainfrom
fix/m365-oauth-login
May 31, 2026
Merged

fix: add Microsoft 365 OAuth login#44
namastex888 merged 2 commits into
mainfrom
fix/m365-oauth-login

Conversation

@namastex888

@namastex888 namastex888 commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #43 by adding a Microsoft 365 OAuth provider path for the read-only pilot. This keeps login simple for Hapvida employees: browser OAuth, not technical device-code/manual setup.

What changed

  • Adds Microsoft OAuth v2.0 browser login support with PKCE.
  • Supports wk auth add <email> --services m365 --readonly.
  • Supports wk auth manage --services m365 --print-url for a clean OAuth handoff URL.
  • Stores M365 refresh tokens under the m365 client namespace, separate from Google tokens.
  • Requests only read-only Graph pilot scopes plus offline_access for refresh token continuity:
    • User.Read
    • Mail.Read
    • Calendars.Read
  • Rejects M365 auth without explicit --readonly fail-closed.
  • Fails closed when WK_M365_CLIENT_ID / build-time DefaultM365ClientID is missing.
  • Adds build-time injection support for internal/non-technical builds:
    • WK_M365_CLIENT_ID
    • WK_M365_TENANT_ID

Safety

  • No Microsoft write scopes.
  • No Graph write commands.
  • No token values printed.
  • Google OAuth path remains separate.
  • M365 token namespace is m365, not default.

Tests

  • RED first: auth add --services m365 was undefined / unknown before this change.
  • GREEN:
    • go test ./internal/msauth ./internal/cmd -run 'TestManualAuthURL|TestOAuthScopes|TestAuth(Add|Manage)M365' -count=1 -v
    • make lint
    • make deadcode
    • go test ./...
    • make fmt-check after commit

Dogfood without Bernardo's real account

Built local binary and verified:

  • Missing M365 client id fails closed:
    • m365 oauth client id missing
  • OAuth handoff URL generation works with a fake client id:
    • host: login.microsoftonline.com
    • endpoint: /organizations/oauth2/v2.0/authorize
    • PKCE: code_challenge_method=S256
    • scopes include: offline_access User.Read Mail.Read Calendars.Read
    • scopes exclude: Mail.Send Calendars.ReadWrite
  • auth add ... --services m365 --readonly --remote --step 1 emits Microsoft OAuth URL.
  • auth add ... --services m365 without --readonly fails closed.

Operational note

To be ready for Bernardo/Hapvida login, the deployed/internal Workit build or runtime environment needs an Entra app client id configured as WK_M365_CLIENT_ID (and optionally WK_M365_TENANT_ID; defaults to organizations). Redirect URI should allow the loopback browser callback used by the CLI:

http://localhost:8085/oauth2/callback

Summary by CodeRabbit

  • New Features

    • Microsoft 365 (Microsoft Graph) authentication via browser OAuth (with PKCE)
    • Read-only M365 account access mode and manual OAuth URL option for browser-based login
    • Support for configuring M365 client ID and tenant ID for builds
  • Tests

    • Added extensive CLI and OAuth tests covering M365 flows, URL generation, scopes, and edge cases

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@namastex888, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 29 minutes and 32 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c7d1417-f1e1-4dd6-9963-1df77a7b5c44

📥 Commits

Reviewing files that changed from the base of the PR and between e83486d and 668e6ac.

📒 Files selected for processing (14)
  • Makefile
  • internal/cmd/auth.go
  • internal/cmd/auth_m365.go
  • internal/cmd/m365_auth_dryrun_test.go
  • internal/cmd/m365_auth_edges_test.go
  • internal/cmd/m365_auth_more_test.go
  • internal/cmd/m365_auth_test.go
  • internal/cmd/testutil_test.go
  • internal/config/defaults.go
  • internal/msauth/oauth.go
  • internal/msauth/oauth_more_test.go
  • internal/msauth/oauth_test.go
  • internal/msauth/open_browser.go
  • internal/msauth/open_browser_test.go
📝 Walkthrough

Walkthrough

Adds Microsoft 365 read-only OAuth: build-time credential injection, a PKCE-based msauth package (browser flow, manual URL, token exchange, Graph email fetch), command-layer M365 routing/handlers, token storage, and extensive tests.

Changes

Microsoft 365 OAuth Authentication

Layer / File(s) Summary
Build-time M365 credential injection
Makefile, internal/config/defaults.go
M365 client ID and tenant ID are added as optional build-time variables with WK_*/GOG_* fallback and injected into the binary via -X linker flags for build-internal and build-automagik.
OAuth2 with PKCE protocol implementation
internal/msauth/oauth.go, internal/msauth/open_browser.go, internal/msauth/oauth_test.go, internal/msauth/oauth_more_test.go
Authorization-code + PKCE flow with local callback server, ManualAuthURL, token exchange requiring refresh token, Graph /me email fetching (mail then userPrincipalName), readonly-only scope selection, and platform-specific openBrowser.
Auth command routing and M365 handlers
internal/cmd/auth.go, internal/cmd/auth_m365.go
AuthAddCmd/AuthManageCmd detect m365 services and delegate to runM365, which enforces --readonly, rejects unsupported modes, supports remote/step manual URL handoffs, runs authorization, validates email, and stores refresh tokens under microsoft_graph/m365 metadata.
Command-layer and msauth tests
internal/cmd/*_test.go, internal/msauth/*_test.go, internal/cmd/testutil_test.go
Tests validate dry-run outputs, readonly enforcement, manual URL printing/JSON, token storage, error/edge cases (state/code/missing client), and update stdout/stderr capture helpers for concurrent streaming.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CLI as Workit CLI
  participant Browser
  participant CallbackServer as Local Callback
  participant MSGraph as Microsoft Graph
  participant TokenStore as Secrets Store

  User->>CLI: auth add --services m365 --readonly
  CLI->>CLI: Generate PKCE state/verifier/challenge
  CLI->>Browser: openBrowser(authURL)
  Browser->>MSGraph: User authenticates and consents
  MSGraph->>CallbackServer: POST /oauth2/callback with code
  CallbackServer->>CLI: deliver code (validate state)
  CLI->>MSGraph: exchange code (with PKCE verifier)
  MSGraph-->>CLI: access_token + refresh_token
  CLI->>MSGraph: FetchEmail (/me)
  MSGraph-->>CLI: mail or userPrincipalName
  CLI->>TokenStore: storeM365Token (microsoft_graph provider)
  TokenStore-->>CLI: stored result (provider,email,services)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • automagik-dev/workit#38 — Related pilot-scope allowlist and scope-guard logic used by the msauth implementation.

Poem

🐰 A rabbit taps the PKCE drum,

opens browsers, waits for code to come,
Graph returns an email song,
Tokens stored where they belong,
Read-only hops — secure and calm.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: add Microsoft 365 OAuth login' clearly and concisely describes the main change: adding M365 OAuth support.
Linked Issues check ✅ Passed The PR fully implements the required acceptance criteria from issue #43: M365 OAuth provider with PKCE, read-only pilot scopes, explicit config requirements, separate token namespace, comprehensive tests, and fail-closed behavior.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing M365 OAuth login: Makefile build config injection, auth command handlers, OAuth implementation, token storage, and comprehensive test coverage for the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/m365-oauth-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6060622f7f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/auth_m365.go Outdated
u.Out().Printf("provider\tmicrosoft_graph")
u.Out().Printf("auth_url\t%s", result.URL)
u.Out().Printf("state\t%s", result.State)
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep M365 remote auth from exiting before callback

When wk auth add <email> --services m365 --readonly --remote --step 1 takes this branch, it prints an authorization URL and exits immediately. That URL still uses redirect_uri=http://localhost:8085/oauth2/callback, but no local callback server is left running, and the step-2 path above rejects exchanging the returned redirect/code, so after the user completes Microsoft consent the redirect cannot be consumed and no token can be stored. Either keep a listener/exchange path alive for this mode or reject the remote option until it is implemented.

Useful? React with 👍 / 👎.

@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 introduces Microsoft 365 (M365) OAuth authentication support to the CLI, adding commands, configuration defaults, and an OAuth flow handler with a local callback server. Feedback on these changes highlights three key areas for improvement: first, AuthManageCmd.runM365 should fail closed when not requested with --print-url or JSON output, as it currently exits immediately without opening a browser or starting a callback server; second, the slice returned by msauth.PilotAllowedScopes() should be copied before sorting to prevent modifying shared package-level state; and third, the Windows browser-opening command should use cmd /c start instead of rundll32.exe to avoid enterprise security blocks and URL truncation issues with ampersands.

Comment thread internal/cmd/auth_m365.go
Comment thread internal/cmd/auth_m365.go Outdated
Comment on lines +118 to +119
scopes := msauth.PilotAllowedScopes()
sort.Strings(scopes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Sorting the slice returned by msauth.PilotAllowedScopes() in-place can lead to unexpected side effects or race conditions if the underlying slice is a shared package-level variable in the msauth package. To prevent modifying shared state, copy the slice before sorting it.

Suggested change
scopes := msauth.PilotAllowedScopes()
sort.Strings(scopes)
scopes := append([]string(nil), msauth.PilotAllowedScopes()...)
sort.Strings(scopes)

Comment thread internal/msauth/open_browser.go Outdated
Comment on lines +18 to +19
case "windows":
cmd = exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", url)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using rundll32.exe to open URLs on Windows has two major drawbacks:

  1. It is frequently blocked in enterprise environments (such as Hapvida) by security policies (AppLocker, WDAC) to prevent security bypasses.
  2. rundll32 url.dll,FileProtocolHandler has known issues parsing URLs that contain ampersands (&), which can truncate the OAuth URL and break the flow.

Using cmd /c start is much more robust and less likely to be blocked by enterprise security policies.

Suggested change
case "windows":
cmd = exec.CommandContext(ctx, "rundll32", "url.dll,FileProtocolHandler", url)
case "windows":
cmd = exec.CommandContext(ctx, "cmd", "/c", "start", "", url)

@namastex888 namastex888 force-pushed the fix/m365-oauth-login branch from 6060622 to e83486d Compare May 31, 2026 19:18
@blacksmith-sh

This comment has been minimized.

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
internal/msauth/open_browser.go (1)

11-11: 💤 Low value

Consider accepting a context parameter instead of using context.Background().

Using context.Background() means the browser launch can't be cancelled if the parent operation times out. While the current design (fire-and-forget with error discarded) makes this low-impact, accepting ctx context.Context as a parameter would be more idiomatic and future-proof.

♻️ Proposed refactor
-func openBrowser(url string) error {
-	ctx := context.Background()
+func openBrowser(ctx context.Context, url string) error {

Then update the caller in oauth.go:

-	_ = openBrowserFn(authURL)
+	_ = openBrowserFn(ctx, authURL)

And the function variable declaration:

-	openBrowserFn          = openBrowser
+	openBrowserFn          func(context.Context, string) error = openBrowser
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/msauth/open_browser.go` at line 11, The open_browser.go function
should accept a context parameter instead of calling context.Background();
change the function signature (the openBrowser function) to take ctx
context.Context, replace the local ctx := context.Background() with the passed
ctx, and update any callers (e.g., in oauth.go where openBrowser is invoked and
the openBrowser function variable declaration) to pass through the parent
context (e.g., the ctx available in OAuth flow) so the browser launch can be
cancelled by the caller.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cmd/m365_auth_dryrun_test.go`:
- Line 11: The test uses a real-looking email literal in the call to Execute
(the test function in internal/cmd/m365_auth_dryrun_test.go) — replace
"bernardo@hapvida.com.br" with an obviously synthetic address (for example
"pilot@example.com" or "user@example.test") in the Execute invocation and in any
other M365 test fixtures mentioned (same occurrence at lines referenced) so
tests no longer contain real user identifiers; update the string passed to
Execute([]string{...}) and any duplicate occurrences in related test files.
- Around line 24-34: In TestAuthAddM365RealFlowFailsClosedWithoutClientID, clear
both sources that resolveOAuthSettings checks by setting
config.DefaultM365ClientID = "" and clearing the environment variable
WK_M365_CLIENT_ID (e.g., os.Unsetenv("WK_M365_CLIENT_ID") or
os.Setenv("WK_M365_CLIENT_ID", "")) before calling Execute; then run the Execute
call that triggers the error check; optionally restore previous values after the
test to avoid side effects.

In `@internal/cmd/m365_auth_edges_test.go`:
- Around line 8-23: The test TestAuthAddM365RejectsUnsupportedModes is too loose
because it only checks for non-nil errors; change it to assert specific usage
error messages produced by runM365 for each unsupported mode. Update the
table-driven cases to include the expected error string for each args slice,
call Execute(args) as before, and instead of only checking err != nil, assert
that err is non-nil and that err.Error() (or the captured stderr) contains the
precise message indicating headless callback-server mode, raw --auth-code
rejection, and remote step 2 rejection; reference the Execute(...) call and
runM365/usage(...) behavior to craft the expected strings. Ensure the test fails
if the exact rejection message is not present.

In `@internal/msauth/oauth_more_test.go`:
- Around line 220-237: The test is racing with a hard-coded port and sleep;
change the code to use an ephemeral port from the actual listener instead of
DefaultLocalAuthPort and signal readiness instead of sleeping: in oauth.go (the
code that does net.Listen("tcp", fmt.Sprintf("localhost:%d",
DefaultLocalAuthPort)) and builds the redirectURI) accept/return the listener or
the resolved port (or make the port configurable) and construct the redirectURI
from listener.Addr().(*net.TCPAddr).Port; then update the test's openBrowserFn
in oauth_more_test.go to use the listener-derived redirect URL (not
"http://localhost:8085/...") and replace the fixed time.Sleep with a readiness
channel/notification that the server has started (e.g., notify after listener is
created and before serving) so the test triggers the callback only after the
server is listening.

---

Nitpick comments:
In `@internal/msauth/open_browser.go`:
- Line 11: The open_browser.go function should accept a context parameter
instead of calling context.Background(); change the function signature (the
openBrowser function) to take ctx context.Context, replace the local ctx :=
context.Background() with the passed ctx, and update any callers (e.g., in
oauth.go where openBrowser is invoked and the openBrowser function variable
declaration) to pass through the parent context (e.g., the ctx available in
OAuth flow) so the browser launch can be cancelled by the caller.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9610bfc6-8bae-47d6-a774-25f234c7263e

📥 Commits

Reviewing files that changed from the base of the PR and between 6060622 and e83486d.

📒 Files selected for processing (13)
  • Makefile
  • internal/cmd/auth.go
  • internal/cmd/auth_m365.go
  • internal/cmd/m365_auth_dryrun_test.go
  • internal/cmd/m365_auth_edges_test.go
  • internal/cmd/m365_auth_more_test.go
  • internal/cmd/m365_auth_test.go
  • internal/cmd/testutil_test.go
  • internal/config/defaults.go
  • internal/msauth/oauth.go
  • internal/msauth/oauth_more_test.go
  • internal/msauth/oauth_test.go
  • internal/msauth/open_browser.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • internal/config/defaults.go
  • internal/cmd/auth.go
  • internal/msauth/oauth_test.go
  • internal/cmd/auth_m365.go
  • internal/cmd/m365_auth_test.go
  • internal/msauth/oauth.go

Comment thread internal/cmd/m365_auth_dryrun_test.go Outdated
Comment thread internal/cmd/m365_auth_dryrun_test.go
Comment thread internal/cmd/m365_auth_edges_test.go
Comment thread internal/msauth/oauth_more_test.go Outdated
@namastex888 namastex888 force-pushed the fix/m365-oauth-login branch from e83486d to 26d67c2 Compare May 31, 2026 19:26
@namastex888 namastex888 merged commit 30e16d9 into main May 31, 2026
12 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.

Add Microsoft 365 OAuth login flow for read-only pilot

2 participants