Skip to content

feat: wire M365 broker into auth server#47

Merged
namastex888 merged 3 commits into
mainfrom
feat/m365-enterprise-auth-server
Jun 3, 2026
Merged

feat: wire M365 broker into auth server#47
namastex888 merged 3 commits into
mainfrom
feat/m365-enterprise-auth-server

Conversation

@namastex888
Copy link
Copy Markdown
Contributor

@namastex888 namastex888 commented Jun 2, 2026

Summary

Wires the M365 one-click broker into Workit's auth-server so Felipe can deploy it as a tenant-owned pod inside Hapvida/OCI HV.

What changed

  • wk auth m365 login-link now uses dynamic callback-server defaults when URLs are omitted:
    • WK_CALLBACK_SERVER / configured callback server becomes broker base URL
    • M365 callback derives as <base>/m365/callback
    • explicit --base-url / --callback-url still override
  • Adds M365 broker endpoints to auth-server:
    • POST /m365/sessions
    • GET /m365/start/:state
    • GET /m365/callback
  • Adds dynamic auth-server env/flag config:
    • WK_PUBLIC_BASE_URL / --public-base-url
    • WK_M365_CLIENT_ID / --m365-client-id
    • WK_M365_TENANT_ID / --m365-tenant-id
    • WK_CALLBACK_SERVER remains backward-compatible public base fallback
  • Allows M365-only auth-server deployments without Google client secret.
  • Adds auth-server/Dockerfile for OCI/HV pod deployment.
  • Adds root Make targets:
    • make build-auth-server
    • make docker-auth-server

Safety

  • HTTPS-only public base URL for M365 session creation.
  • Microsoft OAuth URL uses PKCE S256.
  • No write scopes in generated URL (Mail.Send, Calendars.ReadWrite absent).
  • Code verifier is not exposed in session JSON.
  • State expires and is consumed on callback.
  • Callback validates Graph /me email/UPN against expected email before storing token.
  • Token is available through the existing /token/:state relay path after successful callback.

Verification

make lint
make deadcode
go test ./...
make coverage COVERAGE_MIN=70
(cd auth-server && go test .)
make build-auth-server

Results:

0 issues.
deadcode baseline check: OK
Go test: 2409 passed in 20 packages
coverage total: 70.1% (min 70.0%)
Go test: 23 passed in 1 packages
make: ok

Dogfood local auth-server:

  • started ./bin/workit-auth-server with:
    • WK_PUBLIC_BASE_URL=https://auth.hv.example
    • fake WK_M365_CLIENT_ID
    • WK_M365_TENANT_ID=hapvida-tenant
  • POST /m365/sessions returned:
    • login_url=https://auth.hv.example/m365/start/<state>
    • normalized expected_email=bernardo@hapvida.com.br
    • no code_verifier
  • GET /m365/start/<state> returned 302 to login.microsoftonline.com
  • redirect contained redirect_uri=https://auth.hv.example/m365/callback
  • redirect had PKCE code_challenge_method=S256
  • redirect had no write scopes

Docker note: Dockerfile is committed and covered by a config test, but this runner cannot connect to /var/run/docker.sock (permission denied), so image build must be run by CI or an environment with Docker daemon access.

Refs #45.

Summary by CodeRabbit

  • New Features

    • Added Microsoft 365 (M365) authentication support with new endpoints: /m365/sessions, /m365/start/{state}, and /m365/callback.
    • M365 login flow now supports automatic URL defaulting in the CLI.
  • Documentation

    • Updated documentation with new M365 API endpoints and required environment variables for configuration.
  • Chores

    • Optimized Docker image deployment using distroless base image.
    • Added new build targets for auth-server compilation and containerization.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

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 25 minutes and 56 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: 062a7b37-2410-49f0-abb9-9f3cee5cb17a

📥 Commits

Reviewing files that changed from the base of the PR and between c77c9f1 and 0439945.

📒 Files selected for processing (12)
  • .deadcode-baseline.txt
  • auth-server/Dockerfile
  • auth-server/README.md
  • auth-server/config.go
  • auth-server/dockerfile_test.go
  • auth-server/handlers.go
  • auth-server/m365.go
  • auth-server/m365_handlers_test.go
  • auth-server/main.go
  • auth-server/main_config_test.go
  • internal/cmd/auth_m365_link.go
  • internal/cmd/m365_login_link_test.go
📝 Walkthrough

Walkthrough

This PR adds a complete Microsoft 365 OAuth 2.0 broker to the auth-server, including PKCE-backed session management, email verification via Microsoft Graph, configuration resolution from environment variables, and CLI integration. The auth-server can now operate in M365-only or combined Google/M365 mode.

Changes

Microsoft 365 OAuth Broker Integration

Layer / File(s) Summary
Build and container configuration
Makefile, auth-server/Dockerfile, auth-server/dockerfile_test.go, auth-server/README.md
Makefile adds build-auth-server and docker-auth-server targets. Dockerfile migrates from Alpine to distroless/static-debian12:nonroot, adds WK_PUBLIC_BASE_URL, WK_M365_CLIENT_ID, WK_M365_TENANT_ID environment variables, and updates entrypoint to absolute binary path. Test validates Dockerfile documents M365 configuration contract. README documents new /m365/* endpoints, M365 environment variables, and Docker build/run examples.
Server configuration resolution
auth-server/config.go, auth-server/handlers.go, auth-server/main.go, auth-server/main_config_test.go
New serverConfigInput and serverConfig structures with resolveServerConfig function derive publicBaseURL and redirectURL from explicit inputs and environment variables, normalizing trailing slashes and computing callback URLs. Server struct gains m365Enabled, m365ClientID, m365TenantID, m365Sessions, and publicBaseURL fields. main.go adds CLI flags for public base URL and M365 settings, validates M365-only or combined deployments, constructs server via NewServerWithOptions, and conditionally logs M365 status. Test coverage validates publicBaseURL derivation, redirect URL computation, and explicit redirect preservation.
Microsoft 365 OAuth implementation
auth-server/m365.go
Implements OAuth 2.0 M365 login broker using PKCE. ServerOptions configures enablement, client/tenant IDs, and base URL. Thread-safe in-memory m365SessionStore with expiry and single-use consumption. NewServerWithOptions initializes token storage and registers three HTTP handlers. POST /m365/sessions validates M365 configuration, creates PKCE session, returns state/login-URL/expiry. GET /m365/start/{state} redirects to Microsoft authorization URL. GET /m365/callback validates params, exchanges code for tokens, fetches email from Microsoft Graph, verifies expected email match, stores token, renders success/failure page. Supporting functions validate M365 config, generate PKCE state/verifier/challenge, create OAuth config with Microsoft identity endpoints, exchange code with refresh token requirement, fetch email from Graph /me, validate email case-insensitive match, and return JSON-formatted errors.
M365 handler tests
auth-server/m365_handlers_test.go
Tests verify POST /m365/sessions returns 200 with login_url and normalized expected_email. Tests verify GET /m365/start returns 302 redirect to Microsoft OAuth authorize with required PKCE parameters. Tests verify fail-closed behavior when M365 configuration missing: POST returns 500 without exposing sensitive "token" substring.
CLI integration for M365 broker
internal/cmd/auth_m365_link.go, internal/cmd/m365_login_link_test.go
New resolveBrokerURLs helper normalizes and fills empty BaseURL/CallbackURL using environment defaults, validates baseURL scheme and host, returning resolved values or usage error. Run calls resolveBrokerURLs, propagates errors, passes resolved URLs to broker session creation, eliminating prior requirement for both explicit flags. Test verifies defaulting to WK_CALLBACK_SERVER environment variable with computed callback URL when broker URLs omitted.

Sequence Diagrams

sequenceDiagram
  participant Client
  participant Server
  participant Graph as Microsoft Graph
  participant Tokens as Token Store
  
  Client->>Server: POST /m365/sessions (expected_email)
  Server->>Server: validate M365 config<br/>generate PKCE session
  Server-->>Client: 200 OK (state, login_url)
  
  Client->>Server: GET /m365/start/{state}
  Server->>Server: fetch session
  Server-->>Client: 302 redirect to authorization
  
  Client->>Server: GET /m365/callback (code, state)
  Server->>Server: exchange code + PKCE<br/>for tokens
  Server->>Graph: GET /me (access_token)
  Graph-->>Server: email
  Server->>Server: validate email matches
  Server->>Tokens: store token
  Server-->>Client: 200 OK success page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A Microsoft broker hops in to play,
With PKCE tokens and sessions to convey,
Email verified from Graph's reply,
Configuration resolved, no hard-coded tie,
One-click login for enterprise, hooray!

🚥 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 'feat: wire M365 broker into auth server' directly and concisely summarizes the main change: integrating M365 broker functionality into the auth-server component.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feat/m365-enterprise-auth-server

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.

Copy link
Copy Markdown

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

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) integration to the auth server, adding endpoints for one-click login sessions, PKCE-based OAuth flow, and profile validation. It also updates the Dockerfile to use a secure, multi-stage distroless build and refactors the CLI command to dynamically resolve broker URLs. Feedback on these changes highlights three key areas for improvement: implementing a passive cleanup mechanism in m365SessionStore to prevent memory leaks from expired sessions, limiting the request body size in the /m365/sessions endpoint using http.MaxBytesReader to mitigate Denial of Service risks, and simplifying the URL resolution logic in the CLI command to avoid potential bugs when only a custom base URL is provided.

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 auth-server/m365.go
Comment on lines +61 to +70
func (s *m365SessionStore) save(session m365Session) {
s.mu.Lock()
defer s.mu.Unlock()

if s.sessions == nil {
s.sessions = make(map[string]m365Session)
}

s.sessions[session.State] = session
}
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

The m365SessionStore does not have a background cleanup routine for expired sessions. If sessions are created but never started or consumed, they will remain in the sessions map indefinitely, leading to a memory leak. We can implement a passive cleanup of expired sessions inside the save method to prevent unbounded memory growth without needing extra background goroutines.

func (s *m365SessionStore) save(session m365Session) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.sessions == nil {
		s.sessions = make(map[string]m365Session)
	}

	// Passive cleanup of expired sessions to prevent memory leaks
	now := time.Now()
	for k, sess := range s.sessions {
		if !sess.ExpiresAt.IsZero() && now.After(sess.ExpiresAt) {
			delete(s.sessions, k)
		}
	}

	s.sessions[session.State] = session
}

Comment thread auth-server/m365.go
Comment on lines +141 to +148
var req struct {
ExpectedEmail string `json:"expected_email"`
ForceConsent bool `json:"force_consent"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, err)
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The request body is decoded without limiting its size. An attacker could send an extremely large payload to POST /m365/sessions, causing high memory consumption and potential denial of service. It is recommended to wrap the request body in an http.MaxBytesReader to enforce a reasonable limit (e.g., 1MB).

Suggested change
var req struct {
ExpectedEmail string `json:"expected_email"`
ForceConsent bool `json:"force_consent"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, err)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // Limit to 1MB
var req struct {
ExpectedEmail string `json:"expected_email"`
ForceConsent bool `json:"force_consent"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSONError(w, http.StatusBadRequest, err)
return
}

Comment on lines +30 to +45
func (c *AuthM365LoginLinkCmd) resolveBrokerURLs() (string, string, error) {
baseURL := strings.TrimSpace(c.BaseURL)
callbackURL := strings.TrimSpace(c.CallbackURL)
if baseURL == "" || callbackURL == "" {
callbackServer, err := googleauth.CallbackServerURL(baseURL)
if err != nil {
return "", "", err
}

if baseURL == "" {
baseURL = strings.TrimRight(callbackServer, "/")
}
if callbackURL == "" {
callbackURL = strings.TrimRight(callbackServer, "/") + "/m365/callback"
}
}
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

The current URL resolution logic is overly complex and potentially buggy. If a user specifies a custom --base-url but omits --callback-url, the callbackURL is derived from googleauth.CallbackServerURL(baseURL) (which might return the default callback server instead of the custom base URL). We can simplify this logic to resolve baseURL first if empty, and then derive callbackURL directly from baseURL if it is empty.

func (c *AuthM365LoginLinkCmd) resolveBrokerURLs() (string, string, error) {
	baseURL := strings.TrimSpace(c.BaseURL)
	callbackURL := strings.TrimSpace(c.CallbackURL)

	if baseURL == "" {
		callbackServer, err := googleauth.CallbackServerURL("")
		if err != nil {
			return "", "", err
		}
		baseURL = strings.TrimRight(callbackServer, "/")
	}

	if callbackURL == "" {
		callbackURL = baseURL + "/m365/callback"
	}

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: c77c9f1ea9

ℹ️ 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 auth-server/m365.go
return s
}

func (s *Server) handleM365Sessions(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Authenticate broker session creation

When the broker is reachable at the configured public base URL, this unauthenticated POST lets any caller create a Microsoft 365 state for any expected email; the response exposes that state/login URL, and after the target completes consent the existing unauthenticated /token/{state} handler returns the refresh token. That makes it possible to generate a link for a victim and then consume their token, so session creation needs to be restricted to a trusted/admin caller or otherwise protected before exposing this endpoint.

Useful? React with 👍 / 👎.

Comment thread internal/cmd/auth_m365_link.go Outdated
Comment on lines 65 to 71
session, err := createM365BrokerSession(ctx, msauth.BrokerSessionOptions{
ExpectedEmail: email,
BaseURL: c.BaseURL,
CallbackURL: c.CallbackURL,
BaseURL: baseURL,
CallbackURL: callbackURL,
Readonly: true,
ForceConsent: c.ForceConsent,
TTL: c.TTL,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Persist CLI-created sessions on the broker

When users run auth m365 login-link, this still creates the state only in the local CLI process via msauth.CreateBrokerSession; the auth server's /m365/start/{state} path only checks its in-memory s.m365Sessions, which is populated by POST /m365/sessions. As a result, the printed one-click URL points at the broker with a state the broker has never seen and returns 404 instead of redirecting to Microsoft; the command needs to create the session on the broker (or otherwise store it server-side) before returning the link.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
auth-server/README.md (1)

104-108: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Compose healthcheck uses wget, which doesn't exist in the new distroless image.

The Dockerfile now builds on gcr.io/distroless/static-debian12:nonroot, which has no shell or wget. This Compose example builds that same image (build: .) but its healthcheck shells out to wget, so the container will be reported unhealthy. Consider removing the healthcheck here or switching to an in-binary health probe.

🤖 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 `@auth-server/README.md` around lines 104 - 108, The Compose healthcheck in
README.md uses wget which is not present in the distroless image; remove or
replace the healthcheck so it doesn't shell out to wget. Either remove the
entire healthcheck block from the Compose example, or replace it with an
in-binary probe that calls your service binary directly (e.g., change the test
to call your application's health probe executable), and update the README to
note that a distroless runtime requires in-binary or container-local probes
instead of shell utilities.
🧹 Nitpick comments (5)
auth-server/main.go (1)

83-86: 💤 Low value

Dead code: redirect URL default is now unreachable.

resolveServerConfig always returns a non-empty redirectURL (it falls back to http://localhost:<port>/callback in config.go), so after Line 51 *redirectURL can never be empty. This block can be removed.

♻️ Proposed cleanup
-	// Default redirect URL if not specified
-	if *redirectURL == "" {
-		*redirectURL = fmt.Sprintf("http://localhost:%d/callback", *port)
-	}
-
 	// Create token store with TTL and start cleanup
🤖 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 `@auth-server/main.go` around lines 83 - 86, The if-block that sets a default
for *redirectURL is dead code because resolveServerConfig always returns a
non-empty redirectURL (it already falls back to
"http://localhost:<port>/callback" in config.go); remove the entire block
starting with "if *redirectURL == "" { ... }" in main.go (references:
redirectURL variable and resolveServerConfig function) to clean up unreachable
logic.
auth-server/Dockerfile (1)

10-10: 💤 Low value

Hardcoded GOARCH=amd64 produces an amd64-only image.

If the deployment target (OCI/HV pod) can be arm64, this build will produce an incompatible binary. Consider letting Docker's TARGETARCH build arg drive the architecture so multi-arch builds work.

♻️ Use BuildKit TARGETARCH
-FROM golang:1.25-alpine AS build
+FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build
+ARG TARGETARCH
 WORKDIR /src
 
 COPY go.mod go.sum ./
 RUN go mod download
 
 COPY . ./
-RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/workit-auth-server .
+RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/workit-auth-server .
🤖 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 `@auth-server/Dockerfile` at line 10, Replace the hardcoded GOARCH=amd64 in the
Dockerfile RUN that builds the Go binary with a build-arg driven value so
multi-arch images work: add an ARG TARGETARCH (or ensure existing ARG is
present) and use GOARCH=${TARGETARCH} in the build invocation (the RUN line that
calls "CGO_ENABLED=0 GOOS=linux GOARCH=... go build -trimpath -ldflags=\"-s -w\"
-o /out/workit-auth-server .") so the architecture comes from Docker BuildKit's
TARGETARCH during multi-platform builds.
auth-server/m365.go (3)

52-107: ⚖️ Poor tradeoff

Expired sessions are never reclaimed unless accessed.

get evicts only on access and consume deletes on use, but a session that is created and then never started/consumed remains in the map until process restart. Over time (or under abuse of POST /m365/sessions) this is an unbounded-growth/memory pressure risk. Consider a background sweep (e.g., a time.Ticker goroutine pruning entries past ExpiresAt) or pruning lazily on each save.

🤖 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 `@auth-server/m365.go` around lines 52 - 107, m365SessionStore currently can
grow unbounded because expired sessions are only removed on get/consume; add
eviction to prevent memory growth by either (A) starting a background goroutine
in newM365SessionStore that runs a time.Ticker and prunes entries whose
m365Session.ExpiresAt is set and before time.Now(), locking s.mu while deleting,
or (B) augment save(session m365Session) to scan and remove expired entries
before inserting; reference the m365SessionStore type and its methods save, get,
consume and the ExpiresAt field when implementing the sweep so expired sessions
are reclaimed without requiring explicit access.

180-185: 💤 Low value

Render an HTML error page instead of JSON for browser-facing start endpoint.

/m365/start/{state} is hit directly by a user's browser (the generated login_url). On an expired/unknown state this returns a raw JSON error via writeJSONError, which is a confusing UX. The callback path already uses renderErrorPage; reuse it here for consistency.

♻️ Proposed change
 	session, err := s.m365Sessions.get(state)
 	if err != nil {
-		writeJSONError(w, http.StatusNotFound, err)
+		s.renderErrorPage(w, "Microsoft 365 login link expired or not found", http.StatusNotFound)
 		return
 	}
🤖 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 `@auth-server/m365.go` around lines 180 - 185, When an unknown/expired state is
detected after calling s.m365Sessions.get(state) replace the browser-facing JSON
response with the same HTML error flow used by the callback: call
renderErrorPage(w, r, http.StatusNotFound, err.Error() or a user-friendly
message) instead of writeJSONError, so the /m365/start/{state} endpoint renders
the HTML error page; keep the HTTP status consistent (http.StatusNotFound) and
reuse the same renderErrorPage signature used elsewhere to maintain UX
consistency.

281-290: 💤 Low value

Consider pinning AuthStyleInParams for the Microsoft token endpoint.

This is a public client (no client secret). With the default AuthStyleAutoDetect, oauth2 first attempts HTTP Basic and only falls back to in-params on failure, producing an extra failed token request against Microsoft on first exchange per client style cache. Microsoft's v2.0 token endpoint expects credentials in the body; setting AuthStyle: oauth2.AuthStyleInParams avoids the wasted round-trip and the brittle auto-detect path.

golang.org/x/oauth2 AuthStyleAutoDetect behavior with empty client secret and Microsoft identity v2.0 token endpoint
🤖 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 `@auth-server/m365.go` around lines 281 - 290, The oauth2.Config returned by
Server.m365OAuthConfig uses the default AuthStyleAutoDetect which causes an
unnecessary failed HTTP Basic attempt against Microsoft's v2 token endpoint;
update m365OAuthConfig to set AuthStyle: oauth2.AuthStyleInParams on the
returned oauth2.Config so the public client sends credentials in the request
body (in-params) and avoids the extra failed token request.
🤖 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 `@auth-server/m365.go`:
- Around line 380-388: validateM365Email currently returns an error that embeds
user emails (want/got) which leaks PII; change validateM365Email to avoid
including the email values in the returned error — either return the sentinel
errM365EmailMismatch directly or wrap it with non-PII context (e.g.,
fmt.Errorf("%w: email mismatch") ) and leave actual values out so the caller
(log.Printf in the M365 flow) will not log user emails; update the function
validateM365Email to use only errM365EmailMismatch (or a non-PII wrapper)
instead of formatting want/got into the error.

In `@internal/cmd/auth_m365_link.go`:
- Around line 47-52: The callbackURL is not validated — parse and validate it
the same way as baseURL: call url.Parse(callbackURL) and ensure the resulting
URL has a non-empty Scheme and Host, returning usage("invalid m365 broker
--callback-url") on error; update the function that currently parses baseURL
(references: url.Parse, baseURL, callbackURL) so both URLs are validated before
returning and before calling createM365BrokerSession.

---

Outside diff comments:
In `@auth-server/README.md`:
- Around line 104-108: The Compose healthcheck in README.md uses wget which is
not present in the distroless image; remove or replace the healthcheck so it
doesn't shell out to wget. Either remove the entire healthcheck block from the
Compose example, or replace it with an in-binary probe that calls your service
binary directly (e.g., change the test to call your application's health probe
executable), and update the README to note that a distroless runtime requires
in-binary or container-local probes instead of shell utilities.

---

Nitpick comments:
In `@auth-server/Dockerfile`:
- Line 10: Replace the hardcoded GOARCH=amd64 in the Dockerfile RUN that builds
the Go binary with a build-arg driven value so multi-arch images work: add an
ARG TARGETARCH (or ensure existing ARG is present) and use GOARCH=${TARGETARCH}
in the build invocation (the RUN line that calls "CGO_ENABLED=0 GOOS=linux
GOARCH=... go build -trimpath -ldflags=\"-s -w\" -o /out/workit-auth-server .")
so the architecture comes from Docker BuildKit's TARGETARCH during
multi-platform builds.

In `@auth-server/m365.go`:
- Around line 52-107: m365SessionStore currently can grow unbounded because
expired sessions are only removed on get/consume; add eviction to prevent memory
growth by either (A) starting a background goroutine in newM365SessionStore that
runs a time.Ticker and prunes entries whose m365Session.ExpiresAt is set and
before time.Now(), locking s.mu while deleting, or (B) augment save(session
m365Session) to scan and remove expired entries before inserting; reference the
m365SessionStore type and its methods save, get, consume and the ExpiresAt field
when implementing the sweep so expired sessions are reclaimed without requiring
explicit access.
- Around line 180-185: When an unknown/expired state is detected after calling
s.m365Sessions.get(state) replace the browser-facing JSON response with the same
HTML error flow used by the callback: call renderErrorPage(w, r,
http.StatusNotFound, err.Error() or a user-friendly message) instead of
writeJSONError, so the /m365/start/{state} endpoint renders the HTML error page;
keep the HTTP status consistent (http.StatusNotFound) and reuse the same
renderErrorPage signature used elsewhere to maintain UX consistency.
- Around line 281-290: The oauth2.Config returned by Server.m365OAuthConfig uses
the default AuthStyleAutoDetect which causes an unnecessary failed HTTP Basic
attempt against Microsoft's v2 token endpoint; update m365OAuthConfig to set
AuthStyle: oauth2.AuthStyleInParams on the returned oauth2.Config so the public
client sends credentials in the request body (in-params) and avoids the extra
failed token request.

In `@auth-server/main.go`:
- Around line 83-86: The if-block that sets a default for *redirectURL is dead
code because resolveServerConfig always returns a non-empty redirectURL (it
already falls back to "http://localhost:<port>/callback" in config.go); remove
the entire block starting with "if *redirectURL == "" { ... }" in main.go
(references: redirectURL variable and resolveServerConfig function) to clean up
unreachable logic.
🪄 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: a4105744-8245-48ac-8cdb-684a3de4ca0f

📥 Commits

Reviewing files that changed from the base of the PR and between da942b5 and c77c9f1.

📒 Files selected for processing (12)
  • Makefile
  • auth-server/Dockerfile
  • auth-server/README.md
  • auth-server/config.go
  • auth-server/dockerfile_test.go
  • auth-server/handlers.go
  • auth-server/m365.go
  • auth-server/m365_handlers_test.go
  • auth-server/main.go
  • auth-server/main_config_test.go
  • internal/cmd/auth_m365_link.go
  • internal/cmd/m365_login_link_test.go

Comment thread auth-server/m365.go
Comment thread internal/cmd/auth_m365_link.go
@namastex888 namastex888 merged commit 9ed729e into main Jun 3, 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.

2 participants