Skip to content

feat(auth): Inject auth claims into request context and authenticate audience#540

Merged
laouji merged 6 commits into
mainfrom
EN-118
Jan 28, 2026
Merged

feat(auth): Inject auth claims into request context and authenticate audience#540
laouji merged 6 commits into
mainfrom
EN-118

Conversation

@laouji
Copy link
Copy Markdown
Contributor

@laouji laouji commented Jan 22, 2026

Relates to EN-118

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 22, 2026

Walkthrough

Adds audience claim validation, a ControlPlaneAgent abstraction and AuthenticateOnControlPlane flow, ControlPlane middleware that injects org/client IDs into context, a NoAgent implementation, and support for annotated FX modules to create multiple distinct authenticator instances.

Changes

Cohort / File(s) Summary
Audience Claim Validation
auth/additional_checks.go, auth/additional_checks_test.go
New public CheckAudienceClaim(expectedAudienceUrl string) returns a closure validating claims.GetAudience() against the expected URL. Tests cover nil claims, matching/non-matching audiences, and multi-audience scenarios.
ControlPlaneAgent Abstraction
auth/control_plane_agent.go, auth/control_plane_agent_generated.go, auth/control_plane_agent_test.go
New ControlPlaneAgent interface and DefaultControlPlaneAgent wrapping oidc.AccessTokenClaims; implementations for scopes, organization ID, client ID, subject; generated gomock and unit tests added.
Authentication Flow Updates
auth/auth.go, auth/auth_test.go
Internal authenticate refactored to return (ControlPlaneAgent, error); new public AuthenticateOnControlPlane(r *http.Request) (ControlPlaneAgent, error); Authenticate() now delegates to internal flow. Tests and token helpers updated to include audience handling and audience-related test cases.
Authenticator Interface & Mocks
auth/authenticator.go, auth/authenticator_generated.go
New public Authenticator interface declaring Authenticate(...) (bool, error) and AuthenticateOnControlPlane(r *http.Request) (ControlPlaneAgent, error) with updated mock methods/recorders.
Middleware & Context Keys
auth/middleware.go, auth/middleware_test.go
Removed inline Authenticator definition; added ContextKeyAuthClaimOrganizationID and ContextKeyAuthClaimClientID; new ControlPlaneMiddleware uses AuthenticateOnControlPlane(), maps oidc errors to 403, and injects org/client IDs into request context. Tests updated/added for control-plane middleware behavior.
NoAuth / NoAgent
auth/no_auth.go
noAuth now implements AuthenticateOnControlPlane returning NoAgent; new NoAgent implements ControlPlaneAgent with empty/default responses.
Dependency Injection / Annotated Modules
auth/module.go, auth/module_test.go
ModuleOptions(nameAnnotation string) added; new AnnotatedModule(cfg, annotationTag) to wire annotated fx providers (http client, keySet, authenticator). newKeySet() and newAuthenticator() providers extracted. Test validates two annotated modules with separate OIDC servers.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Middleware as ControlPlaneMiddleware
    participant Auth as JWTAuth
    participant OIDC as oidc.AccessTokenClaims
    participant Agent as ControlPlaneAgent

    Client->>Middleware: HTTP Request
    Middleware->>Auth: AuthenticateOnControlPlane(request)
    Auth->>OIDC: Extract & validate token claims
    alt Claims Valid
        Auth->>Auth: Run additional checks (audience, scopes)
        alt All checks pass
            Auth->>Agent: NewDefaultControlPlaneAgent(claims)
            Agent-->>Auth: ControlPlaneAgent
            Auth-->>Middleware: (agent, nil)
        else Check fails
            Auth-->>Middleware: (agent, error)
            Middleware-->>Client: 403 Forbidden
        end
    else Claims invalid
        Auth-->>Middleware: (agent, error)
        Middleware-->>Client: 401/403
    end
    alt Success
        Middleware->>Middleware: Inject org/client IDs into context
        Middleware->>Client: Call next handler with updated context
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I sniffed the audience, checked each claim,

I wrapped the token, gave the agent a name,
Two modules hopping, distinct and neat,
Org and client IDs tucked in the seat,
A tiny rabbit cheers this auth-tidy feat! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% 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 PR title accurately describes the main changes: it adds audience claim authentication and injects auth claims into request context via ControlPlaneMiddleware and related infrastructure.
Description check ✅ Passed The PR description references a related issue (EN-118) which indicates this change is linked to tracked work, though minimal implementation details are provided.

✏️ 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-118

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • EN-118: 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 22, 2026 17:44
@laouji laouji requested a review from a team as a code owner January 22, 2026 17:44
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 64.20455% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.22%. Comparing base (d042fba) to head (84c6e11).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
auth/control_plane_agent_generated.go 0.00% 46 Missing ⚠️
auth/no_auth.go 0.00% 12 Missing ⚠️
auth/middleware.go 82.35% 2 Missing and 1 partial ⚠️
auth/module.go 95.91% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #540      +/-   ##
==========================================
+ Coverage   29.14%   29.22%   +0.07%     
==========================================
  Files         166      174       +8     
  Lines        6714     6943     +229     
==========================================
+ Hits         1957     2029      +72     
- Misses       4640     4797     +157     
  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: 1

🤖 Fix all issues with AI agents
In `@auth/additional_checks.go`:
- Around line 48-69: The CheckAudienceClaim function should guard against an
expectedAudience with an empty Host (caused by a scheme-less URL); normalize or
validate expectedAudience at the start of CheckAudienceClaim by ensuring it has
a scheme (e.g., if expectedAudience.Scheme == "" prepend "http://" and re-parse
or construct a url.URL) and return an error if normalization fails, or document
the requirement—update the function to use the normalized expectedAudience.Host
for comparisons so host checks never compare against an empty string.
🧹 Nitpick comments (4)
auth/agent_test.go (1)

22-30: Consider adding edge case tests for GetOrganizationID.

The test covers the happy path. Consider adding cases for:

  • nil Claims map (should return empty string)
  • Non-string organization_id value (should return empty string)
Example edge case tests
func TestDefaultAgent_GetOrganizationID_NilClaims(t *testing.T) {
	t.Parallel()
	claims := oidc.AccessTokenClaims{
		Claims: nil,
	}
	agent := auth.NewDefaultAgent(claims)

	assert.Equal(t, "", agent.GetOrganizationID())
}

func TestDefaultAgent_GetOrganizationID_NonStringValue(t *testing.T) {
	t.Parallel()
	claims := oidc.AccessTokenClaims{
		Claims: map[string]interface{}{"organization_id": 123},
	}
	agent := auth.NewDefaultAgent(claims)

	assert.Equal(t, "", agent.GetOrganizationID())
}
auth/additional_checks.go (1)

56-58: Consider handling https:// explicitly in the comment.

The check !strings.HasPrefix(audienceStr, "http") correctly handles both http:// and https:// schemes. The inline comment could be slightly clearer.

Minor comment clarification
-			if !strings.HasPrefix(audienceStr, "http") { // urlParse requires a scheme
+			if !strings.HasPrefix(audienceStr, "http") { // url.Parse requires a scheme (handles both http:// and https://)
auth/middleware.go (1)

11-14: Consider using typed context keys to prevent collisions.

String-based context keys can collide with keys from other packages. Using unexported typed keys is more idiomatic in Go.

Example using typed context keys
+type contextKey string
+
 const (
-	ContextKeyAuthClaimOrganizationID = "AuthClaim-OrganizationID"
-	ContextKeyAuthClaimClientID       = "AuthClaim-ClientID"
+	ContextKeyAuthClaimOrganizationID contextKey = "AuthClaim-OrganizationID"
+	ContextKeyAuthClaimClientID       contextKey = "AuthClaim-ClientID"
 )
auth/additional_checks_test.go (1)

81-90: Consider enabling parallel test execution and adding edge cases.

The test is well-structured. A few optional improvements:

  1. Add t.Parallel() for faster test execution
  2. Consider adding a test case for empty audience list
Suggested improvements
 	for name, tt := range tests {
 		t.Run(name, func(t *testing.T) {
+			t.Parallel()
 			expectedAudience, err := url.Parse(tt.expectedAudienceStr)

Additional test case for empty audience:

"EmptyAudienceList": {
	expectedAudienceStr: "http://example.com",
	claims: &oidc.AccessTokenClaims{
		TokenClaims: oidc.TokenClaims{
			Audience: []string{},
		},
	},
	expectedError: oidc.ErrAudience,
},

Comment thread auth/additional_checks.go Outdated
Comment thread auth/agent.go Outdated
Comment thread auth/middleware.go
gfyrag
gfyrag previously approved these changes Jan 22, 2026
@gfyrag gfyrag self-requested a review January 22, 2026 20:00
Copy link
Copy Markdown
Contributor

@gfyrag gfyrag left a comment

Choose a reason for hiding this comment

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

Starting to think we should have a "go-libs-controlplane" repository...
Feels like we are mixing different stack in the same bag.

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

🤖 Fix all issues with AI agents
In `@auth/auth.go`:
- Around line 36-40: JWTAuth.authenticate currently returns nil on any
ClaimsFromRequest error, discarding claims that may accompany
issuer/signature/expiration validation failures; modify authenticate to capture
the claims returned by ClaimsFromRequest, build the ControlPlaneAgent (using
those claims) even when ClaimsFromRequest returns an error, and return that
agent together with the error so callers can access claims for context/logging
(only return nil agent for unrecoverable header/format errors—detect those
separately and keep existing nil behavior). Ensure changes are made in the
JWTAuth.authenticate function and use the returned claims from ClaimsFromRequest
when constructing the agent.
🧹 Nitpick comments (3)
auth/middleware.go (1)

11-14: Prefer typed context keys to avoid collisions.
Using plain strings risks collisions with other context values. A small typed key keeps it safer and idiomatic.

♻️ Suggested change
+type contextKey string
+
 const (
-	ContextKeyAuthClaimOrganizationID = "AuthClaim-OrganizationID"
-	ContextKeyAuthClaimClientID       = "AuthClaim-ClientID"
+	ContextKeyAuthClaimOrganizationID contextKey = "AuthClaim-OrganizationID"
+	ContextKeyAuthClaimClientID       contextKey = "AuthClaim-ClientID"
 )
auth/control_plane_agent.go (1)

22-27: Align org ID extraction with existing helper.
Reusing oidc.OrganizationAwareAccessTokenClaims keeps semantics consistent with CheckOrganizationIDClaim.

♻️ Suggested change
 func (a DefaultControlPlaneAgent) GetOrganizationID() string {
-	organizationID := a.claims.Claims["organization_id"]
-	if organizationIDStr, ok := organizationID.(string); ok {
-		return organizationIDStr
-	}
-	return ""
+	claims := &oidc.OrganizationAwareAccessTokenClaims{AccessTokenClaims: a.claims}
+	return claims.GetOrganizationID()
 }
auth/module.go (1)

32-40: Guard empty annotationTag to avoid name:"" collisions.
If callers pass an empty tag, fall back to the unannotated module to prevent ambiguous naming.

♻️ Suggested change
 func AnnotatedModule(cfg ModuleConfig, annotationTag string) fx.Option {
+	if annotationTag == "" {
+		return Module(cfg)
+	}
 	nameAnnotation := `name:"` + annotationTag + `"`
 	options := ModuleOptions(nameAnnotation)

Comment thread auth/auth.go
@laouji laouji requested a review from gfyrag January 26, 2026 15:44
Comment thread auth/module.go
@laouji laouji added this pull request to the merge queue Jan 28, 2026
Merged via the queue into main with commit 79fe6a4 Jan 28, 2026
7 checks passed
@laouji laouji deleted the EN-118 branch January 28, 2026 14:46
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