Skip to content

feat(auth.m2m): per-client custom claims in client_credentials tokens#236

Merged
intel352 merged 2 commits intomainfrom
copilot/extend-m2m-token-claims
Mar 3, 2026
Merged

feat(auth.m2m): per-client custom claims in client_credentials tokens#236
intel352 merged 2 commits intomainfrom
copilot/extend-m2m-token-claims

Conversation

Copy link
Contributor

Copilot AI commented Mar 3, 2026

auth.m2m tokens carried only standard JWT claims, forcing downstream services to trust untrusted request headers (e.g. X-Tenant-Id) for tenant identity. This adds a per-client claims map that is cryptographically embedded in issued tokens.

Changes

  • M2MClient — adds Claims map[string]any field
  • handleClientCredentials — passes client.Claims to issueToken; existing standard-claim protection (iss, sub, iat, exp, scope) blocks any override attempts
  • auth.m2m factory — parses claims: map[string]any from each client config entry
  • Schema — updates clients field description to document claims

Example

- name: m2m-auth
  type: auth.m2m
  config:
    algorithm: ES256
    clients:
      - clientId: "client-org-alpha"
        clientSecret: "secret1"
        scopes: [read, write]
        claims:
          tenant_id: "org-alpha"
      - clientId: "client-org-beta"
        clientSecret: "secret2"
        scopes: [read, write]
        claims:
          tenant_id: "org-beta"

Issued tokens will include tenant_id alongside standard claims. step.auth_validate already extracts all JWT claims into pipeline context, so downstream steps get tenant_id automatically without trusting headers.

Original prompt

This section details on the original issue you should resolve

<issue_title>Feature: Per-client tenant/affiliate claims in auth.m2m tokens</issue_title>
<issue_description>## Problem

In a multi-tenant deployment where the workflow engine serves multiple tenants (e.g., different organizations), the auth.m2m module issues client_credentials tokens that carry only standard JWT claims: sub (client_id), iss, iat, exp, scope.

There is no mechanism to bind a specific M2M client to a specific tenant/organization, and no way to include tenant identity in the issued token. This means applications must rely on untrusted request headers (e.g., X-Tenant-Id) to determine which tenant a request belongs to — any authenticated client can claim any tenant identity.

Proposed Solution

Extend the M2M client configuration to support an optional claims map (or a dedicated tenant_id/affiliate_id field) that gets included in issued tokens:

- name: m2m-auth
  type: auth.m2m
  config:
    algorithm: ES256
    clients:
      - clientId: "client-org-alpha"
        clientSecret: "secret1"
        scopes: [read, write]
        claims:
          tenant_id: "org-alpha"
      - clientId: "client-org-beta"
        clientSecret: "secret2"
        scopes: [read, write]
        claims:
          tenant_id: "org-beta"

When a token is issued via client_credentials, the custom claims would be included in the JWT payload alongside the standard claims. This allows downstream pipeline steps (step.auth_validate) to extract the tenant identity from the token itself rather than trusting a request header.

Use Case

  1. Multi-tenant SaaS application with per-organization API clients
  2. Each client should only be able to access its own tenant's data
  3. DB queries use WHERE tenant_id = $X — the tenant_id should come from the verified token, not an untrusted header
  4. step.auth_validate already extracts all JWT claims into the pipeline context, so downstream steps would automatically have access to the tenant_id claim

Security Impact

Without this feature, tenant isolation depends entirely on the application trusting HTTP headers, which is a weaker security boundary than cryptographically signed JWT claims.</issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copilot AI changed the title [WIP] Add tenant identity to auth.m2m tokens feat(auth.m2m): per-client custom claims in client_credentials tokens Mar 3, 2026
@intel352 intel352 marked this pull request as ready for review March 3, 2026 04:23
Copilot AI review requested due to automatic review settings March 3, 2026 04:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for embedding per-client custom claims into auth.m2m client_credentials JWTs so downstream workflows can rely on signed tenant/affiliate identity instead of untrusted headers.

Changes:

  • Extends M2MClient with a Claims map[string]any field and includes it during token issuance.
  • Updates the auth.m2m module factory to parse claims from client config entries.
  • Adds tests for custom-claim inclusion and protected-claim non-overridability; updates schema text to document claims.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
plugins/auth/plugin.go Parses per-client claims from config and wires them into registered M2M clients; updates schema description.
plugins/auth/plugin_test.go Adds a factory-level test constructing auth.m2m with client claims.
module/auth_m2m.go Adds Claims to M2MClient and passes them into issueToken for client_credentials.
module/auth_m2m_test.go Adds unit tests validating custom claims appear in issued tokens and cannot override protected standard claims.
Comments suppressed due to low confidence (1)

plugins/auth/plugin_test.go:170

  • TestModuleFactoryM2MWithClaims only asserts a 200 response from the token endpoint, which would still pass even if the claims config isn’t parsed/applied. To actually cover the new behavior in the plugin factory, decode the JSON response, authenticate/parse the returned access token, and assert the expected custom claim (e.g. tenant_id) is present (and optionally that protected claims still can’t be overridden).
func TestModuleFactoryM2MWithClaims(t *testing.T) {
	p := New()
	factories := p.ModuleFactories()

	mod := factories["auth.m2m"]("m2m-test", map[string]any{
		"algorithm": "HS256",
		"secret":    "this-is-a-valid-secret-32-bytes!",
		"clients": []any{
			map[string]any{
				"clientId":     "org-alpha",
				"clientSecret": "secret-alpha",
				"scopes":       []any{"read"},
				"claims": map[string]any{
					"tenant_id": "alpha",
				},
			},
		},
	})
	if mod == nil {
		t.Fatal("auth.m2m factory returned nil")
	}

	m2mMod, ok := mod.(*module.M2MAuthModule)
	if !ok {
		t.Fatal("expected *module.M2MAuthModule")
	}

	// Issue a token via the Handle method.
	params := url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {"org-alpha"},
		"client_secret": {"secret-alpha"},
	}
	req := httptest.NewRequest(http.MethodPost, "/oauth/token", strings.NewReader(params.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	w := httptest.NewRecorder()
	m2mMod.Handle(w, req)

	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d; body: %s", w.Code, w.Body.String())
	}
}

@intel352 intel352 merged commit 75f259d into main Mar 3, 2026
18 checks passed
@intel352 intel352 deleted the copilot/extend-m2m-token-claims branch March 3, 2026 04:47
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.

Feature: Per-client tenant/affiliate claims in auth.m2m tokens

3 participants