[Proposal] Replace AuthContext map[string]string with Typed Struct in Policy SDK #1048
Replies: 9 comments 12 replies
-
|
@renuka-fernando But let's say a user want to use a different claim rather than the default |
Beta Was this translation helpful? Give feedback.
-
|
The audience should be an array |
Beta Was this translation helpful? Give feedback.
-
|
I don't know whether we can do this change now but let me just suggest this. The current flat struct mixes general fields with token-specific fields. With a flat structure, it's unclear which fields are populated for which auth type — e.g., does Suggestion: keep only truly common fields flat and use auth-type-specific sub-structs for the rest: type AuthContext struct {
// Authenticated indicates whether the request passed authentication.
Authenticated bool
// AuthType identifies the mechanism that authenticated the request.
// Values: "jwt", "oauth2", "apikey", "basic", or empty if unauthenticated.
AuthType string
// UserID is the user identifier extracted from the authentication source.
UserID string
// AppID is the application identifier associated with the request.
// e.g., client_id for OAuth2, application owning the API key, etc.
AppID string
// Scopes contains granted scopes as a set for O(1) lookup.
// Applicable for OAuth2/JWT and potentially API key auth.
Scopes map[string]bool
// Auth-type-specific details. Only the relevant one is non-nil.
JWT *JWTAuthDetails
APIKey *APIKeyAuthDetails
Basic *BasicAuthDetails
// Properties holds additional key-value data for inter-policy communication
// that does not fit the typed fields above.
Properties map[string]string
}
// JWTAuthDetails holds fields specific to JWT/OAuth2 token-based auth.
type JWTAuthDetails struct {
// Subject is the "sub" claim from the token.
Subject string
// Issuer is the token issuer ("iss" claim, IdP URL).
Issuer string
// Audience is the intended audience ("aud" claim). Can be multiple values.
Audience []string
// Claims holds additional token claims that don't fit the typed fields.
Claims map[string]string
}This way, a |
Beta Was this translation helpful? Give feedback.
-
|
@malinthaprasan Thanks for the feedback on the struct design. Let me explain the rationale behind the flat structure approach. Why Flat Structure?The key reason I opted for a flat structure without auth-type-specific sub-structs is to enable auth-agnostic consumption by downstream policies. The goal is that policies consuming the Example: Analytics System PolicyConsider the analytics policy. With the flat structure: // Analytics doesn't need to know or care what auth type was used
if ctx.SharedContext.AuthContext.Authenticated {
analyticsData := map[string]string{
"user_id": ctx.SharedContext.AuthContext.UserID,
"issuer": ctx.SharedContext.AuthContext.Issuer, // empty for non-token auth
"app_id": ctx.SharedContext.AuthContext.AppID, // empty if not applicable
}
publishAnalytics(analyticsData)
}The analytics policy simply reads the fields it needs. If With nested auth-type-specific sub-structs, the analytics policy would need to do: // Analytics now needs to know about auth types
var issuer string
if ctx.SharedContext.AuthContext.JWT != nil {
issuer = ctx.SharedContext.AuthContext.JWT.Issuer
} else if ctx.SharedContext.AuthContext.OAuth2 != nil {
issuer = ctx.SharedContext.AuthContext.OAuth2.Issuer
}
// ... and so on for other auth typesThis creates coupling between the analytics policy and the auth policy implementations. Future ExtensibilityWhen we introduce new authentication types (mTLS, SAML, OAuth2 with DPoP, etc.), the flat structure means:
With nested sub-structs, every new auth type would require:
Field NamingRegarding the field names (
If these JWT-specific terms are confusing for non-token auth types, I'm happy to rename them to more generic alternatives. |
Beta Was this translation helpful? Give feedback.
-
|
Where do we put the keyname during APIKey auth? |
Beta Was this translation helpful? Give feedback.
-
Multiple Auth Policies ChallengeI have another scenario to consider for the The ProblemConsider an API operation that requires both API Key AND JWT authentication to succeed: operations:
- path: /secure-resource
method: GET
policies:
- name: apikey-auth
- name: jwt-authIn this setup:
The current flat struct design doesn't support representing multiple successful authentications. Current BehaviorWith the proposed flat structure, the second auth policy would overwrite fields set by the first: // API Key policy executes first
ctx.SharedContext.AuthContext.Authenticated = true
ctx.SharedContext.AuthContext.AuthType = "apikey"
ctx.SharedContext.AuthContext.CredentialID = "my-key-name"
// JWT policy executes second, overwrites AuthType
ctx.SharedContext.AuthContext.Authenticated = true
ctx.SharedContext.AuthContext.AuthType = "jwt" // Lost "apikey" info
ctx.SharedContext.AuthContext.Subject = "user-456"
ctx.SharedContext.AuthContext.Issuer = "https://idp.example.com"Result: We lose information about the API Key authentication. Questions to Address
Proposed SolutionsOption 1: Hybrid - Array + Unified Fields (Recommended)Preserve complete auth history in type AuthContext struct {
// Authenticated indicates whether the request passed authentication
Authenticated bool
// Attempts contains all successful auth attempts in execution order
// Complete audit trail of what auth mechanisms were used
Attempts []AuthAttempt
// Unified fields for auth-agnostic consumption by downstream policies
// Auth policies decide whether to update these fields or leave them as-is
Subject string // User/principal identifier
Issuer string // Credential issuer
Audience []string // Intended audience
Scopes map[string]bool // Granted scopes
CredentialID string // Application/key/client identifier
// Properties holds additional key-value data for inter-policy communication
Properties map[string]string
}
type AuthAttempt struct {
AuthType string // "jwt", "oauth2", "apikey", "basic"
Subject string
Issuer string
Audience []string
Scopes map[string]bool
CredentialID string
Metadata map[string]string // Auth-type-specific data
}Design Principle: Each auth policy decides whether to update unified fields based on its own logic. No automatic merge rules. Usage: // API Key policy - sets CredentialID (the key name), doesn't touch Subject (no user identity)
attempt := policy.AuthAttempt{
AuthType: "apikey",
CredentialID: "my-key-name",
Metadata: map[string]string{"header": "x-api-key", "owner": "app-123"},
}
ctx.SharedContext.AuthContext.Authenticated = true
ctx.SharedContext.AuthContext.Attempts = append(ctx.SharedContext.AuthContext.Attempts, attempt)
// Policy decides: populate CredentialID in unified fields
ctx.SharedContext.AuthContext.CredentialID = attempt.CredentialID
// JWT policy - sets Subject/Issuer/Scopes, preserves existing CredentialID
attempt := policy.AuthAttempt{
AuthType: "jwt",
Subject: "user-456",
Issuer: "https://idp.example.com",
Audience: []string{"api.example.com"},
Scopes: map[string]bool{"read": true, "write": true},
}
ctx.SharedContext.AuthContext.Attempts = append(ctx.SharedContext.AuthContext.Attempts, attempt)
// Policy decides: populate Subject/Issuer/Scopes, but DON'T overwrite CredentialID
ctx.SharedContext.AuthContext.Subject = attempt.Subject
ctx.SharedContext.AuthContext.Issuer = attempt.Issuer
ctx.SharedContext.AuthContext.Audience = attempt.Audience
ctx.SharedContext.AuthContext.Scopes = attempt.Scopes
// Note: NOT setting CredentialID - preserving API Key's value
// Downstream policy (AUTH-AGNOSTIC - just reads unified fields!)
if ctx.SharedContext.AuthContext.Authenticated {
analyticsData["user_id"] = ctx.SharedContext.AuthContext.Subject // "user-456" from JWT
analyticsData["key_name"] = ctx.SharedContext.AuthContext.CredentialID // "my-key-name" from API Key
analyticsData["issuer"] = ctx.SharedContext.AuthContext.Issuer // from JWT
}
// Analytics doesn't know or care that both API Key and JWT were used!Alternative strategy: A JWT policy could choose to overwrite CredentialID with client_id from the token if that's the desired behavior: // JWT policy decides to use client_id as CredentialID
ctx.SharedContext.AuthContext.CredentialID = claims.ClientID // Overwrites API Key valueOption 2: Flat Struct with AuthTypes Arraytype AuthContext struct {
Authenticated bool
// AuthTypes contains all successful auth mechanisms in execution order
AuthTypes []string // e.g., ["apikey", "jwt"]
// Fields are merged from all auth policies (later policies override)
Subject string
Issuer string
Audience []string
Scopes map[string]bool
CredentialID string
// Properties can hold auth-type-prefixed values to avoid conflicts
// e.g., "apikey:credential_id", "jwt:subject"
Properties map[string]string
}Usage: // API Key policy
ctx.SharedContext.AuthContext.Authenticated = true
ctx.SharedContext.AuthContext.AuthTypes = append(ctx.SharedContext.AuthContext.AuthTypes, "apikey")
ctx.SharedContext.AuthContext.CredentialID = "my-key-name"
ctx.SharedContext.AuthContext.Properties["apikey:credential_id"] = "my-key-name"
ctx.SharedContext.AuthContext.Properties["apikey:owner"] = "app-123"
// JWT policy (later, overwrites conflicting fields)
ctx.SharedContext.AuthContext.AuthTypes = append(ctx.SharedContext.AuthContext.AuthTypes, "jwt")
ctx.SharedContext.AuthContext.Subject = "user-456"
ctx.SharedContext.AuthContext.Issuer = "https://idp.example.com"
ctx.SharedContext.AuthContext.Properties["jwt:subject"] = "user-456"
// CredentialID from apikey is preserved since JWT doesn't set it
// Downstream policies must know to check Properties for complete data
if credID, ok := ctx.SharedContext.AuthContext.Properties["apikey:credential_id"]; ok {
analyticsData["key_name"] = credID
}Option 3: Keep It Simple - Last Writer WinsAccept that in multi-auth scenarios, the last successful auth policy's data takes precedence:
RecommendationI recommend Option 1 (Hybrid - Array + Unified Fields) because: Advantages:
How it works:
Why not the other options:
Policy flexibility example:
What are your thoughts on this approach? |
Beta Was this translation helpful? Give feedback.
-
|
@malinthaprasan, @IsuruGunarathne, and me disuccussed and finalized the Auth Context as follows. // AuthContext holds authentication data produced by auth policies (jwt, oauth2, apikey, basic-auth)
// and consumed by downstream policies (analytics, rate limiting, etc.).
type AuthContext struct {
// Authenticated indicates whether the request passed authentication.
Authenticated bool
// AuthType identifies the mechanism that authenticated the request.
// Values: "jwt", "oauth2", "apikey", "basic", or empty if unauthenticated.
AuthType string
// Subject is the authenticated principal identifier.
// JWT "sub" claim, basic-auth username, API key owner/app ID, etc.
Subject string
// Issuer is the token issuer (JWT "iss" claim, IdP URL).
// Empty for auth types that don't support (e.g. basic-auth).
Issuer string
// Audience is the intended audience (JWT "aud" claim).
// Empty for auth types that don't support (e.g. basic-auth). Can be a single value or multiple audiences.
Audience []string
// Scopes contains granted OAuth2/JWT scopes as a set for O(1) lookup.
// Nil for auth types that don't support scopes (e.g. basic-auth).
Scopes map[string]bool
// CredentialID is the application identifier associated with the authenticated request.
// For API key auth, this is the application that owns the key.
// For OAuth2, this may be the client_id. Empty if not applicable.
CredentialID string
// Properties holds additional auth-related key-value data for
// inter-policy communication that does not fit the typed fields above.
// Examples: email, custom JWT claims, API key tier.
Properties map[string]string
// Previous points to the previous AuthContext in the chain for multi-layer auth.
// Example: API Key auth followed by JWT validation would create a chain.
// nil if this is the only/last auth context.
Previous *AuthContext
} |
Beta Was this translation helpful? Give feedback.
-
AuthContext Population per Policy — Sample Values1.
|
| AuthContext Field | JWT Source |
|---|---|
Subject |
claims["sub"] |
Issuer |
claims["iss"] |
Audience |
parseAudience(claims["aud"]) — handles both string and []string |
Scopes |
claims["scope"] (space-delimited) or claims["scp"] (array) → converted to map[string]bool |
CredentialID |
claims["azp"] or claims["client_id"] (if present) |
2. basic-auth
Basic auth only has a username — no token, no issuer, no scopes.
AuthContext{
Authenticated: true,
AuthType: "basic",
PolicyName: "basic-auth",
Subject: "admin", // the authenticated username
Issuer: "", // not applicable for basic-auth
Audience: nil, // not applicable for basic-auth
Scopes: nil, // not applicable for basic-auth
CredentialID: "", // not applicable for basic-auth
Properties: nil, // no additional properties
}Field mapping:
| AuthContext Field | Basic Auth Source |
|---|---|
Subject |
the provided username (currently stored as ctx.Metadata["auth.username"]) |
| All other fields | empty/nil — basic-auth has no token claims |
3. api-key-auth
API keys are shared secrets — they grant access but don't identify a user. API Key is not bound to a user. Fields like Issuer, Audience, and Scopes are not applicable since API keys are not tokens.
AuthContext{
Authenticated: true,
AuthType: "apikey",
PolicyName: "api-key-auth",
Subject: "", // empty — API keys don't identify a user
Issuer: "", // N/A — API keys don't have an issuer
Audience: nil, // N/A — API keys don't have an audience
Scopes: nil, // N/A — API keys use operation-level access control, not scopes
CredentialID: "weather-api-key", // APIKey.Name — unique, immutable, URL-safe identifier
Properties: map[string]string{
"displayName": "My Weather Key", // APIKey.DisplayName
"createdBy": "john@acme.com", // APIKey.CreatedBy
},
}Field mapping:
| AuthContext Field | API Key Source |
|---|---|
Subject |
empty — API keys don't identify a user |
Issuer, Audience, Scopes |
N/A — not applicable for API key auth |
CredentialID |
APIKey.Name — unique per API, immutable, used in API paths |
Properties["displayName"] |
APIKey.DisplayName — human-readable name |
Properties["createdBy"] |
APIKey.CreatedBy — user who created the key |
4. mcp-auth
mcp-auth delegates to jwt-auth internally, so its AuthContext is identical to jwt-auth. The only difference is the PolicyName.
AuthContext{
// All fields same as jwt-auth, only PolicyName differs:
PolicyName: "mcp-auth",
// ...
}5. mcp-authz
New field: Authorized bool
This requires adding a new Authorized field to the AuthContext struct. The existing Authenticated field only covers authentication (did the user prove their identity?), but not authorization (is the user allowed to perform this action?). When an authz policy is configured as non-blocking (soft deny), the request continues even if authorization fails. Without a separate Authorized field, downstream policies have no way to know whether the request was authorized — they only see Authenticated: true from the preceding auth policy.
mcp-authz is an authorization policy, not an authentication policy. It reads the AuthContext set by a preceding auth policy (typically mcp-auth or jwt-auth) and sets only the Authorized field.
mcp-auth (authn) → sets AuthContext → mcp-authz (authz) → reads AuthContext, sets Authorized
AuthContext{
// Only sets the Authorized field:
Authorized: true, // or false if authorization fails
PolicyName: "mcp-authz",
}- If authorization fails and the policy is blocking,
mcp-authzreturns a 403 immediately — the request never reaches upstream. - If authorization fails but the policy is non-blocking, the request continues with
Authorized: falseso downstream policies can act accordingly. - If authorization passes,
Authorized: true.
Why two separate fields (Authenticated + Authorized) instead of a single field?
A single field like Accepted or Denied would conflate two distinct concerns.
A single Denied bool would lose this distinction — a downstream policy like analytics couldn't tell whether the request failed at authn or authz.
Beta Was this translation helpful? Give feedback.
-
|
@malinthaprasan, @IsuruGunarathne discussed doing the following
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Updated: 2026-02-15 - Added
UserIDfield for custom claim mapping support and changedAudienceto[]stringarray type.Summary
AuthContext map[string]stringin the policy SDK'sSharedContextwith a typedAuthContextstruct containing named fields for common auth dataProperties map[string]stringescape hatch on the struct for custom/policy-specific metadata that does not fit the typed fieldsMotivation
AuthContextusing magic string keys like"x-wso2-user-id"with no compile-time safety. A typo in a key name silently produces missing data rather than a build error.Proposal
AuthContextstruct insdk/gateway/policy/v1alpha/context.gowith typed fields for the most common auth attributesSharedContextProperties map[string]stringfield as an escape hatch for custom dataThe proposed struct:
Examples
Before (auth policy writing to AuthContext):
After (auth policy writing to AuthContext):
Before (downstream policy reading AuthContext):
After (downstream policy reading AuthContext):
References
AuthContext map[string]stringdefinitionAuthContextis initialized asmake(map[string]string)AuthContext["x-wso2-user-id"]Beta Was this translation helpful? Give feedback.
All reactions