Skip to content

feat(auth): Inject organizationID claim into request header#539

Closed
laouji wants to merge 1 commit into
mainfrom
EN-518-claim-in-header
Closed

feat(auth): Inject organizationID claim into request header#539
laouji wants to merge 1 commit into
mainfrom
EN-518-claim-in-header

Conversation

@laouji
Copy link
Copy Markdown
Contributor

@laouji laouji commented Jan 8, 2026

Relates to EN-518

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 8, 2026

Walkthrough

Introduces a new constant for an organization ID HTTP header and propagates organization ID from JWT claims into request headers within the authentication middleware. Includes a test case validating that the organization ID from claims is correctly set as a request header.

Changes

Cohort / File(s) Summary
Header Constant & Claim Propagation
auth/additional_checks.go
Added exported constant FormanceHeaderOrganizationID ("X-Formance-OrganizationID"). Modified CheckOrganizationIDClaim to set the organization ID into the request header when derived from JWT claims.
Middleware Tests
auth/middleware_test.go
Added import of testify/assert. Introduced new subtest validating that organization ID extracted from JWT claims is correctly propagated as the FormanceHeaderOrganizationID request header, with HTTP 200 OK response.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A header hops through claims so bright,
Organization ID takes flight,
The middleware propagates with care,
Header constants fill the air! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: injecting organizationID claim into request headers.
Description check ✅ Passed The description references ticket EN-518 which is related to the PR objectives, showing connection to the changeset purpose.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch EN-518-claim-in-header

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • EN-518: Authentication required, not authenticated - You need to authenticate to access this operation.

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.

@laouji laouji marked this pull request as ready for review January 8, 2026 13:45
@laouji laouji requested a review from a team as a code owner January 8, 2026 13:45
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 28.51%. Comparing base (d042fba) to head (b3ae5f9).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #539      +/-   ##
==========================================
- Coverage   29.14%   28.51%   -0.64%     
==========================================
  Files         166      166              
  Lines        6714     7943    +1229     
==========================================
+ Hits         1957     2265     +308     
- Misses       4640     5561     +921     
  Partials      117      117              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

🧹 Nitpick comments (3)
auth/additional_checks.go (2)

42-44: Simplify redundant condition.

The check expectedOrgID != "" is redundant because we already return early on line 33 when expectedOrgID == "". At this point in the code, we know expectedOrgID is not empty.

♻️ Simplify the condition
-	if expectedOrgID != "" && orgID != expectedOrgID {
+	if orgID != expectedOrgID {
 		return oidc.ErrOrgIDInvalid
 	}

36-44: Consider setting header after validation succeeds.

Currently, the header is set on line 40 before the validation check on line 42. This means if the orgID doesn't match expectedOrgID, the header is still set on the request even though an error is returned. While this may not cause issues in practice (since the request will be rejected), it's cleaner to only modify the request after all validation passes.

♻️ Set header after validation
 	orgID := claims.GetOrganizationID()
 	if orgID == "" {
 		return oidc.ErrOrgIDNotPresent
 	}
-	r.Header.Set(FormanceHeaderOrganizationID, orgID)
 
 	if expectedOrgID != "" && orgID != expectedOrgID {
 		return oidc.ErrOrgIDInvalid
 	}
+	r.Header.Set(FormanceHeaderOrganizationID, orgID)
 	return nil
auth/middleware_test.go (1)

65-94: Good test coverage for the happy path, but consider edge cases.

The test correctly verifies that the organization ID from the token is set as a request header when validation succeeds. The test structure is clear and well-organized.

Consider adding test cases for these scenarios:

  1. When expectedOrgID == "" (no validation required) - Verify whether the header should be set when the token contains an org ID but the endpoint doesn't require validation (relates to the verification request in additional_checks.go)

  2. When validation fails - Verify behavior when orgID in token doesn't match expectedOrgID

  3. When orgID is missing from token - Verify the error case when token lacks organization claims

📝 Example test cases
t.Run("orgID not set when validation not required", func(t *testing.T) {
	t.Parallel()
	keySet, privateKey, issuer := setupTestKeySet(t)

	// Provider returns empty string (no validation required)
	provider := func(*http.Request) (orgID string, err error) {
		return "", nil
	}
	additionalChecks := []AdditionalCheck{CheckOrganizationIDClaim(provider)}
	authenticator := NewJWTAuth(keySet, issuer, "test-service", false, additionalChecks)

	token := createAccessTokenWithOrgClaims(t, privateKey, issuer, []string{}, "test-user", "some-org-id")

	handler := Middleware(authenticator)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("OK"))
	}))

	req := httptest.NewRequest("GET", "/test", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	req = req.WithContext(logging.TestingContext())

	rr := httptest.NewRecorder()
	handler.ServeHTTP(rr, req)

	require.Equal(t, http.StatusOK, rr.Code)
	// Verify expected behavior - should header be set or not?
	// assert.Equal(t, "", req.Header.Get(FormanceHeaderOrganizationID))
})

t.Run("validation fails when orgID mismatch", func(t *testing.T) {
	t.Parallel()
	keySet, privateKey, issuer := setupTestKeySet(t)

	provider := func(*http.Request) (orgID string, err error) {
		return "expected-org", nil
	}
	additionalChecks := []AdditionalCheck{CheckOrganizationIDClaim(provider)}
	authenticator := NewJWTAuth(keySet, issuer, "test-service", false, additionalChecks)

	token := createAccessTokenWithOrgClaims(t, privateKey, issuer, []string{}, "test-user", "different-org")

	handler := Middleware(authenticator)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))

	req := httptest.NewRequest("GET", "/test", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	req = req.WithContext(logging.TestingContext())

	rr := httptest.NewRecorder()
	handler.ServeHTTP(rr, req)

	require.Equal(t, http.StatusForbidden, rr.Code)
})
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d042fba and b3ae5f9.

📒 Files selected for processing (2)
  • auth/additional_checks.go
  • auth/middleware_test.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-18T08:02:30.517Z
Learnt from: gfyrag
Repo: formancehq/go-libs PR: 532
File: auth/auth.go:92-110
Timestamp: 2025-11-18T08:02:30.517Z
Learning: In formancehq/go-libs auth package, the ClaimsFromRequest function intentionally returns claims alongside errors when issuer, signature, or expiration validation fails. This allows callers to access claim data even when validation doesn't pass completely.

Applied to files:

  • auth/additional_checks.go
  • auth/middleware_test.go
📚 Learning: 2025-11-05T15:43:35.659Z
Learnt from: gfyrag
Repo: formancehq/go-libs PR: 524
File: auth/module.go:28-43
Timestamp: 2025-11-05T15:43:35.659Z
Learning: In the formancehq/go-libs auth module, retry logic via retryablehttp is only needed for OIDC discovery at startup (e.g., during service reboots). The RemoteKeySet used for runtime JWT verification has its own HTTP request handling and does not require the retryable HTTP client.

Applied to files:

  • auth/middleware_test.go
🔇 Additional comments (3)
auth/additional_checks.go (2)

10-11: LGTM! Well-defined header constant.

The constant follows Go naming conventions and uses the standard X- prefix for custom HTTP headers.


31-40: Verify: Should header be set when validation is not required?

The PR objective states to "propagate organization ID from JWT claims into request headers." However, when expectedOrgID == "" (no validation required), the function returns early on line 33 without setting the header. This means the header is only set when org ID validation is explicitly required.

Consider whether the header should be propagated whenever an orgID is present in the token claims, regardless of whether validation is required for that endpoint. This could be useful for downstream services, logging, or auditing even when the endpoint doesn't enforce org ID checks.

If the header should be set whenever orgID is present (regardless of validation requirement), consider this approach:

+	orgID := claims.GetOrganizationID()
+	if orgID != "" {
+		r.Header.Set(FormanceHeaderOrganizationID, orgID)
+	}
+
 	expectedOrgID, err := fn(r)
 	if err != nil {
 		return err
 	}
 
 	// if the endpoint doesn't require a particular orgID we consider it valid
 	if expectedOrgID == "" {
 		return nil
 	}
 
-	orgID := claims.GetOrganizationID()
 	if orgID == "" {
 		return oidc.ErrOrgIDNotPresent
 	}
-	r.Header.Set(FormanceHeaderOrganizationID, orgID)
 
 	if expectedOrgID != "" && orgID != expectedOrgID {
 		return oidc.ErrOrgIDInvalid
 	}
 	return nil
auth/middleware_test.go (1)

9-9: LGTM! Appropriate test library import.

The assert package is correctly imported for the new assertion on line 93 and is consistent with existing test dependencies.

@laouji laouji closed this Jan 22, 2026
@laouji laouji deleted the EN-518-claim-in-header branch January 22, 2026 16:15
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.

1 participant