OpenID Connect (OIDC) identity and OAuth 2.0 provider with pluggable connectors
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

35 KiB

Dex Enhancement Proposal (DEP) - 2026-02-28 - CEL (Common Expression Language) Integration

Table of Contents

Summary

This DEP proposes integrating CEL (Common Expression Language) into Dex as a first-class expression engine for policy evaluation, claim mapping, and token customization. A new reusable pkg/cel package will provide a safe, sandboxed CEL environment with Kubernetes-grade compatibility guarantees, cost budgets, and a curated set of extension libraries. Subsequent phases will leverage this package to implement authentication policies, token policies, advanced claim mapping in connectors, and per-client/global access rules — replacing the need for ad-hoc configuration fields and external policy engines.

Context

  • #1583 Add allowedGroups option for clients config — a long-standing request for a configuration option to allow a client to specify a list of allowed groups.
  • #1635 Connector Middleware — long-standing request for a policy/middleware layer between connectors and the server for claim transformations and access control.
  • #1052 Allow restricting connectors per client — frequently requested feature to restrict which connectors are available to specific OAuth2 clients.
  • #2178 Custom claims in ID tokens — requests for including additional payload in issued tokens.
  • #2812 Token Exchange DEP — mentions CEL/Rego as future improvement for policy-based assertions on exchanged tokens.
  • The OIDC connector already has a growing set of ad-hoc claim mutation options (ClaimMapping, ClaimMutations.NewGroupFromClaims, FilterGroupClaims, ModifyGroupNames) that would benefit from a unified expression language.
  • Previous community discussions explored OPA/Rego and JMESPath, but CEL offers a better fit (see Alternatives).

Motivation

Goals/Pain

  1. Complex query/filter capabilities — Dex needs a way to express complex validations and mutations in multiple places (authentication flow, token issuance, claim mapping). Today each feature requires new Go code, new config fields, and a new release cycle. CEL allows operators to express these rules declaratively without code changes.

  2. Authentication policies — Operators want to control who can log in based on rich conditions: restrict specific connectors to specific clients, require group membership for certain clients, deny login based on email domain, enforce MFA claims, etc. Currently there is no unified mechanism; users rely on downstream applications or external proxies.

  3. Token policies — Operators want to customize issued tokens: add extra claims to ID tokens, restrict scopes per client, modify aud claims, include upstream connector metadata, etc. Today this requires forking Dex or using a reverse proxy.

  4. Claim mapping in OIDC connector — The OIDC connector has accumulated multiple ad-hoc config options for claim mapping and group mutations (ClaimMapping, NewGroupFromClaims, FilterGroupClaims, ModifyGroupNames). A single CEL expression field would replace all of these with a more powerful and composable approach.

  5. Per-client and global policies — One of the most frequent requests is allowing different connectors for different clients and restricting group-based access per client. CEL policies at the global and per-client level address this cleanly.

  6. CNCF ecosystem alignment — CEL has massive adoption across the CNCF ecosystem:

    Project CEL Usage Evidence
    Kubernetes ValidatingAdmissionPolicy, CRD validation rules (x-kubernetes-validations), AuthorizationPolicy, field selectors, CEL-based match conditions in webhooks KEP-3488, CRD Validation Rules, AuthorizationPolicy KEP-3221
    Kyverno CEL expressions in validation/mutation policies (v1.12+), preconditions Kyverno CEL docs
    OPA Gatekeeper Partially added support for CEL in constraint templates Gatekeeper CEL
    Istio AuthorizationPolicy conditions, request routing, telemetry Istio CEL docs
    Envoy / Envoy Gateway RBAC filter, ext_authz, rate limiting, route matching, access logging Envoy CEL docs
    Tekton Pipeline when expressions, CEL custom tasks Tekton CEL Interceptor
    Knative Trigger filters using CEL expressions Knative CEL filters
    Google Cloud IAM Conditions, Cloud Deploy, Security Command Center Google IAM CEL
    Cert-Manager CertificateRequestPolicy approval using CEL cert-manager approver-policy CEL
    Cilium Hubble CEL filter logic Cilium CEL docs
    Crossplane Composition functions with CEL-based patch transforms Crossplane CEL transforms
    Kube-OVN Network policy extensions using CEL Kube-OVN CEL

    By choosing CEL, Dex operators who already use Kubernetes or other CNCF tools can reuse their existing knowledge of the expression language.

Non-Goals

  • Full policy engine — This DEP does not aim to replace dedicated external policy engines (OPA, Kyverno). CEL in Dex is scoped to identity and token operations.
  • Breaking changes to existing configuration — All existing config fields (ClaimMapping, ClaimMutations, etc.) will continue to work. CEL expressions are additive/opt-in.
  • Authorization (beyond Dex scope) — Dex is an identity provider; downstream authorization decisions remain the responsibility of relying parties. CEL policies in Dex are limited to authentication and token issuance concerns.
  • Multi-phase CEL in a single DEP — Only Phase 1 (pkg/cel package) is targeted for immediate implementation. Phases 2-4 are included here for design context and will have their own implementation PRs.
  • Multi-step logic — CEL in Dex is scoped to single-expression evaluation. Each expression is a standalone, stateless computation with no intermediate variables, chaining, or multi-step transformations. If a use case requires sequential logic or conditionally chained expressions, it belongs outside Dex (e.g. in an external policy engine or middleware). This boundary protects the design from scope creep that pushes CEL beyond what it's good at.

Proposal

User Experience

Authentication Policy (Phase 2)

Operators can define global and per-client authentication policies in the Dex config:

# Global authentication policy — each expression evaluates to bool.
# If true — the request is denied. Evaluated in order; first match wins.
authPolicy:
  - expression: "!identity.email.endsWith('@example.com')"
    message: "'Login restricted to example.com domain'"
  - expression: "!identity.email_verified"
    message: "'Email must be verified'"

staticClients:
  - id: admin-app
    name: Admin Application
    secret: ...
    redirectURIs: [...]
    # Per-client policy — same structure as global
    authPolicy:
      - expression: "!(request.connector_id in ['okta', 'ldap'])"
        message: "'This application requires Okta or LDAP login'"
      - expression: "!('admin' in identity.groups)"
        message: "'Admin group membership required'"

Token Policy (Phase 3)

Operators can add extra claims or mutate token contents:

tokenPolicy:
  # Global mutations applied to all ID tokens
  claims:
    # Add a custom claim based on group membership
    - key: "'role'"
      value: "identity.groups.exists(g, g == 'admin') ? 'admin' : 'user'"
    # Include connector ID as a claim
    - key: "'idp'"
      value: "request.connector_id"
    # Add department from upstream claims (only if present)
    - key: "'department'"
      value: "identity.extra['department']"
      condition: "'department' in identity.extra"

staticClients:
  - id: internal-api
    name: Internal API
    secret: ...
    redirectURIs: [...]
    tokenPolicy:
      claims:
        - key: "'custom-claim.company.com/team'"
          value: "identity.extra['team'].orValue('engineering')"
        # Only add on-call claim for ops group members
        - key: "'on_call'"
          value: "true"
          condition: "identity.groups.exists(g, g == 'ops')"
      # Restrict scopes
      filter:
        expression: "request.scopes.all(s, s in ['openid', 'email', 'profile'])"
        message: "'Unsupported scope requested'"

OIDC Connector Claim Mapping (Phase 4)

Replace ad-hoc claim mapping with CEL:

connectors:
  - type: oidc
    id: corporate-idp
    name: Corporate IdP
    config:
      issuer: https://idp.example.com
      clientID: dex-client
      clientSecret: ...
      # CEL-based claim mapping — replaces claimMapping and claimModifications
      claimMappingExpressions:
        username: "claims.preferred_username.orValue(claims.email)"
        email: "claims.email"
        groups: >
          claims.groups
            .filter(g, g.startsWith('dex:'))
            .map(g, g.trimPrefix('dex:'))          
        emailVerified: "claims.email_verified.orValue(true)"
        # Extra claims to pass through to token policies
        extra:
          department: "claims.department.orValue('unknown')"
          cost_center: "claims.cost_center.orValue('')"

Implementation Details/Notes/Constraints

Phase 1: pkg/cel — Core CEL Library

This is the foundation that all subsequent phases build upon. The package provides a safe, reusable CEL environment with Kubernetes-grade guarantees.

Package Structure

pkg/
  cel/
    cel.go              # Core Environment, compilation, evaluation
    types.go            # CEL type declarations (Identity, Request, etc.)
    cost.go             # Cost estimation and budgeting
    doc.go              # Package documentation
    library/
      email.go          # Email-related CEL functions
      groups.go         # Group-related CEL functions

Dependencies

github.com/google/cel-go v0.27.0

The cel-go library is the canonical Go implementation maintained by Google, used by Kubernetes and all major CNCF projects. It follows semantic versioning and provides strong backward compatibility guarantees.

Core API Design

Public types:

// CompilationResult holds a compiled CEL program ready for evaluation.
type CompilationResult struct {
    Program    cel.Program
    OutputType *cel.Type
    Expression string
}

// Compiler compiles CEL expressions against a specific environment.
type Compiler struct { /* ... */ }

// CompilerOption configures a Compiler.
type CompilerOption func(*compilerConfig)

Compilation pipeline:

Each Compile* call performs these steps sequentially:

  1. Reject expressions exceeding MaxExpressionLength (10,240 chars).
  2. Compile and type-check the expression via cel-go.
  3. Validate output type matches the expected type (for typed variants).
  4. Estimate cost using defaultCostEstimator with size hints — reject if estimated max cost exceeds the cost budget.
  5. Create an optimized cel.Program with runtime cost limit.

Presence tests (has(field), 'key' in map) have zero cost, matching Kubernetes CEL behavior.

Variable Declarations

Variables are declared via VariableDeclaration{Name, Type} and registered with NewCompiler. Helper constructors provide pre-defined variable sets:

IdentityVariables() — the identity variable (from connector.Identity), typed as cel.ObjectType:

Field CEL Type Source
identity.user_id string connector.Identity.UserID
identity.username string connector.Identity.Username
identity.preferred_username string connector.Identity.PreferredUsername
identity.email string connector.Identity.Email
identity.email_verified bool connector.Identity.EmailVerified
identity.groups list(string) connector.Identity.Groups

RequestVariables() — the request variable (from RequestContext), typed as cel.ObjectType:

Field CEL Type
request.client_id string
request.connector_id string
request.scopes list(string)
request.redirect_uri string

ClaimsVariable() — the claims variable for raw upstream claims as map(string, dyn).

Typing strategy:

identity and request use cel.ObjectType with explicitly declared fields. This gives compile-time type checking: a typo like identity.emial is rejected at config load time rather than silently evaluating to null in production — critical for an auth system where a misconfigured policy could lock users out.

claims remains map(string, dyn) because its shape is genuinely unknown — it carries arbitrary upstream IdP data.

Compatibility Guarantees

Following the Kubernetes CEL compatibility model (KEP-3488: CEL for Admission Control, Kubernetes CEL Migration Guide):

  1. Environment versioning — The CEL environment is versioned. When new functions or variables are added, they are introduced under a new environment version. Existing expressions compiled against an older version continue to work.

    // EnvironmentVersion represents the version of the CEL environment.
    // New variables, functions, or libraries are introduced in new versions.
    type EnvironmentVersion uint32
    
    const (
        // EnvironmentV1 is the initial CEL environment.
        EnvironmentV1 EnvironmentVersion = 1
    )
    
    // WithVersion sets the target environment version for the compiler.
    func WithVersion(v EnvironmentVersion) CompilerOption
    

    This is directly modeled on k8s.io/apiserver/pkg/cel/environment.

  2. Library stability — Custom functions in the pkg/cel/library subpackage follow these rules:

    • Functions MUST NOT be removed once released.
    • Function signatures MUST NOT change once released.
    • New functions MUST be added under a new EnvironmentVersion.
    • If a function needs to be replaced, the old one is deprecated but kept forever.
  3. Type stability — CEL types (Identity, Request, Claims) follow the same rules:

    • Fields MUST NOT be removed.
    • Field types MUST NOT change.
    • New fields are added in a new EnvironmentVersion.
  4. Semantic versioning of cel-go — The cel-go dependency follows semver. Dex pins to a minor version range and updates are tested for behavioral changes. This is exactly the approach Kubernetes takes: k8s.io/apiextensions-apiserver pins cel-go and gates new features behind environment versions.

  5. Feature gates — New CEL-powered features are gated behind Dex feature flags (using the existing pkg/featureflags mechanism) during their alpha phase.

Cost Estimation and Budgets

Like Kubernetes, Dex CEL expressions must be bounded to prevent denial-of-service.

Constants:

Constant Value Description
DefaultCostBudget 10_000_000 Max cost units per evaluation (aligned with Kubernetes)
MaxExpressionLength 10_240 Max expression string length in characters
DefaultStringMaxLength 256 Estimated max string size for cost estimation
DefaultListMaxLength 100 Estimated max list size for cost estimation

How it works:

A defaultCostEstimator (implementing checker.CostEstimator) provides size hints for known variables (identity, request, claims) so the cel-go cost estimator doesn't assume unbounded sizes. It also provides call cost estimates for custom Dex functions (dex.emailDomain, dex.emailLocalPart, dex.groupMatches, dex.groupFilter).

Expressions are validated at three levels:

  1. Length check — reject expressions exceeding MaxExpressionLength.
  2. Compile-time cost estimation — reject expressions whose estimated max cost exceeds the cost budget.
  3. Runtime cost limit — abort evaluation if actual cost exceeds the budget.

Extension Libraries

The pkg/cel environment includes these cel-go standard extensions (same set as Kubernetes):

Library Description Examples
ext.Strings() Extended string functions "hello".upperAscii(), "foo:bar".split(':'), s.trim(), s.replace('a','b')
ext.Encoders() Base64 encoding/decoding base64.encode(bytes), base64.decode(str)
ext.Lists() Extended list functions list.slice(1, 3), list.flatten()
ext.Sets() Set operations on lists sets.contains(a, b), sets.intersects(a, b), sets.equivalent(a, b)
ext.Math() Math functions math.greatest(a, b), math.least(a, b)

Plus custom Dex libraries in the pkg/cel/library subpackage, each implementing the cel.Library interface:

library.Email — email-related helpers:

Function Signature Description
dex.emailDomain (string) -> string Returns the domain portion of an email address. dex.emailDomain("user@example.com") == "example.com"
dex.emailLocalPart (string) -> string Returns the local part of an email address. dex.emailLocalPart("user@example.com") == "user"

library.Groups — group-related helpers:

Function Signature Description
dex.groupMatches (list(string), string) -> list(string) Returns groups matching a glob pattern. dex.groupMatches(identity.groups, "team:*")
dex.groupFilter (list(string), list(string)) -> list(string) Returns only groups present in the allowed list. dex.groupFilter(identity.groups, ["admin", "ops"])

Example: Compile and Evaluate

// 1. Create a compiler with identity and request variables
compiler, _ := cel.NewCompiler(
    append(cel.IdentityVariables(), cel.RequestVariables()...),
)

// 2. Compile a policy expression (type-checked, cost-estimated)
prog, _ := compiler.CompileBool(
    `identity.email.endsWith('@example.com') && 'admin' in identity.groups`,
)

// 3. Evaluate against real data
result, _ := cel.EvalBool(ctx, prog, map[string]any{
    "identity": cel.IdentityFromConnector(connectorIdentity),
    "request":  cel.RequestFromContext(cel.RequestContext{...}),
})
// result == true

Phase 2: Authentication Policies

Config Model:

// AuthPolicy is a list of deny expressions evaluated after a user
// authenticates with a connector. Each expression evaluates to bool.
// If true — the request is denied. Evaluated in order; first match wins.
type AuthPolicy []PolicyExpression

// PolicyExpression is a CEL expression with an optional human-readable message.
type PolicyExpression struct {
    // Expression is a CEL expression that evaluates to bool.
    Expression string `json:"expression"`
    // Message is a CEL expression that evaluates to string (displayed to the user on deny).
    // If empty, a generic message is shown.
    Message string `json:"message,omitempty"`
}

Evaluation point: After connector.CallbackConnector.HandleCallback() or connector.PasswordConnector.Login() returns an identity, and before the auth request is finalized. Implemented in server/handlers.go at handleConnectorCallback.

Available CEL variables: identity (from connector), request (client_id, connector_id, scopes, redirect_uri).

Compilation: All policy expressions are compiled once at config load time (in cmd/dex/serve.go) and stored in the Server struct. This ensures:

  • Syntax/type errors are caught at startup, not at runtime.
  • No compilation overhead per request.
  • Cost estimation can warn operators about expensive expressions at startup.

Evaluation flow:

User authenticates via connector
         │
         v
connector.HandleCallback() returns Identity
         │
         v
Evaluate global authPolicy (in order)
  - For each expression: evaluate → bool
  - If true → deny with message, HTTP 403
         │
         v
Evaluate per-client authPolicy (in order)
  - Same logic as global
         │
         v
Continue normal flow (approval screen or redirect)

Phase 3: Token Policies

Config Model:

// TokenPolicy defines policies for token issuance.
type TokenPolicy struct {
    // Claims adds or overrides claims in the issued ID token.
    Claims []ClaimExpression `json:"claims,omitempty"`
    // Filter validates the token request. If expression evaluates to false,
    // the request is denied.
    Filter *PolicyExpression `json:"filter,omitempty"`
}

type ClaimExpression struct {
    // Key is a CEL expression evaluating to string — the claim name.
    Key string `json:"key"`
    // Value is a CEL expression evaluating to dyn — the claim value.
    Value string `json:"value"`
    // Condition is an optional CEL expression evaluating to bool.
    // When set, the claim is only included in the token if the condition
    // evaluates to true. If omitted, the claim is always included.
    Condition string `json:"condition,omitempty"`
}

Evaluation point: In server/oauth2.go during ID token construction, after standard claims are built but before JWT signing.

Available CEL variables: identity, request, existing_claims (the standard claims already computed as map(string, dyn)).

Claim merge order:

  1. Standard Dex claims (sub, iss, aud, email, groups, etc.)
  2. Global tokenPolicy.claims evaluated and merged
  3. Per-client tokenPolicy.claims evaluated and merged (overrides global)

Reserved (forbidden) claim names:

Certain claim names are reserved and MUST NOT be set or overridden by CEL token policy expressions. Attempting to use a reserved claim key will result in a config validation error at startup. This prevents operators from accidentally breaking the OIDC/OAuth2 contract or undermining Dex's security guarantees.

// ReservedClaimNames is the set of claim names that CEL token policy
// expressions are forbidden from setting. These are core OIDC/OAuth2 claims
// managed exclusively by Dex.
var ReservedClaimNames = map[string]struct{}{
    "iss":       {},  // Issuer — always set by Dex to its own issuer URL
    "sub":       {},  // Subject — derived from connector identity, must not be spoofed
    "aud":       {},  // Audience — determined by the OAuth2 client, not policy
    "exp":       {},  // Expiration — controlled by Dex token TTL configuration
    "iat":       {},  // Issued At — set by Dex at signing time
    "nbf":       {},  // Not Before — set by Dex at signing time
    "jti":       {},  // JWT ID — generated by Dex for token revocation/uniqueness
    "auth_time": {},  // Authentication Time — set by Dex from the auth session
    "nonce":     {},  // Nonce — echoed from the client's authorization request
    "at_hash":   {},  // Access Token Hash — computed by Dex from the access token
    "c_hash":    {},  // Code Hash — computed by Dex from the authorization code
}

The reserved list is enforced in two places:

  1. Config load time — When compiling token policy ClaimExpression entries, Dex statically evaluates the Key expression (which must be a string literal or constant-foldable) and rejects it if the result is in ReservedClaimNames.
  2. Runtime (defense in depth) — Before merging evaluated claims into the ID token, Dex checks each key against ReservedClaimNames and logs a warning + skips the claim if it matches. This guards against dynamic key expressions that couldn't be statically checked.

Phase 4: OIDC Connector Claim Mapping

Config Model:

In connector/oidc/oidc.go:

type Config struct {
    // ... existing fields ...

    // ClaimMappingExpressions provides CEL-based claim mapping.
    // When set, these take precedence over ClaimMapping and ClaimMutations.
    ClaimMappingExpressions *ClaimMappingExpression `json:"claimMappingExpressions,omitempty"`
}

type ClaimMappingExpression struct {
    // Username is a CEL expression evaluating to string.
    // Available variable: 'claims' (map of upstream claims).
    Username string `json:"username,omitempty"`
    // Email is a CEL expression evaluating to string.
    Email string `json:"email,omitempty"`
    // Groups is a CEL expression evaluating to list(string).
    Groups string `json:"groups,omitempty"`
    // EmailVerified is a CEL expression evaluating to bool.
    EmailVerified string `json:"emailVerified,omitempty"`
    // Extra is a map of claim names to CEL expressions evaluating to dyn.
    // These are carried through to token policies.
    Extra map[string]string `json:"extra,omitempty"`
}

Available CEL variable: claimsmap(string, dyn) containing all raw upstream claims from the ID token and/or UserInfo endpoint.

This replaces the need for ClaimMapping, NewGroupFromClaims, FilterGroupClaims, and ModifyGroupNames with a single, more powerful mechanism.

Backward compatibility: When claimMappingExpressions is nil, the existing ClaimMapping and ClaimMutations logic is used unchanged. When claimMappingExpressions is set, a startup warning is logged if legacy mapping fields are also configured.

Policy Application Flow

The following diagram shows the order in which CEL policies are applied. Each step is optional — if not configured, it is skipped.

Connector Authentication
  │
  │ upstream claims → connector.Identity
  │
  v
Authentication Policies
  │
  │  Global authPolicy
  │  Per-client authPolicy
  │
  v
Token Issuance
  │
  │  Global tokenPolicy.filter
  │  Per-client tokenPolicy.filter
  │
  │  Global tokenPolicy.claims
  │  Per-client tokenPolicy.claims
  │
  │  Sign JWT
  │
  v
Token Response
Step Policy Scope Action on match
2 authPolicy (global) Global Expression → true = DENY login
3 authPolicy (per-client) Per-client Expression → true = DENY login
4 tokenPolicy.filter (global) Global Expression → false = DENY token
5 tokenPolicy.filter (per-client) Per-client Expression → false = DENY token
6 tokenPolicy.claims (global) Global Adds/overrides claims (with optional condition)
7 tokenPolicy.claims (per-client) Per-client Adds/overrides claims (overrides global)

Risks and Mitigations

Risk Mitigation
CEL expression complexity / DoS Cost budgets with configurable limits (default aligned with Kubernetes). Expressions are validated at config load time. Runtime evaluation is aborted if cost exceeds budget.
Learning curve for operators CEL has excellent documentation, playground (cel.dev), and massive CNCF adoption. Dex docs will include a dedicated CEL guide with examples. Most operators already know CEL from Kubernetes.
cel-go dependency size cel-go adds ~5MB to binary. This is acceptable for the functionality provided. Kubernetes, Istio, Envoy all accept this trade-off.
Breaking changes in cel-go Pin to semver minor range. Environment versioning ensures existing expressions continue to work across upgrades.
Security: CEL expression injection CEL expressions are defined by operators in the server config, not by end users. No CEL expression is ever constructed from user input at runtime.
Config migration Old config fields (ClaimMapping, ClaimMutations) continue to work. CEL expressions are opt-in. If both are specified, CEL takes precedence with a config-time warning.
Error messages exposing internals CEL deny message expressions are controlled by the operator. Default messages are generic. Evaluation errors are logged server-side, not exposed to end users.
Performance Expressions are compiled once at startup. Evaluation is sub-millisecond for typical identity operations. Cost budgets prevent pathological cases. Benchmarks will be included in pkg/cel tests.

Alternatives

OPA/Rego

OPA was previously considered (#1635, token exchange DEP). While powerful, it has significant drawbacks for Dex:

  • Separate daemon — OPA typically runs as a sidecar or daemon; adds operational complexity. Even the embedded Go library (github.com/open-policy-agent/opa/rego) is significantly heavier than cel-go.
  • Rego learning curve — Rego is a Datalog-derived language unfamiliar to most developers. CEL syntax is closer to C/Java/Go and is immediately readable.
  • Overkill — Dex needs simple expression evaluation, not a full policy engine with data loading, bundles, and partial evaluation.
  • No inline expressions — Rego policies are typically separate files, not inline config expressions. This makes the config harder to understand and deploy.
  • Smaller CNCF footprint for embedding — While OPA is a graduated CNCF project, CEL has broader adoption as an embedded language (Kubernetes, Istio, Envoy, Kyverno, etc.).

JMESPath

JMESPath was proposed for claim mapping. Drawbacks:

  • Query-only — JMESPath is a JSON query language. It cannot express boolean conditions, mutations, or string operations naturally.
  • Limited type system — No type checking at compile time. Errors are only caught at runtime.
  • Small ecosystem — Limited adoption compared to CEL. No CNCF projects use JMESPath for policy evaluation.
  • No cost estimation — No way to bound execution time.

Hardcoded Go Logic

The current approach: each feature requires new Go structs, config fields, and code. This is unsustainable:

  • ClaimMapping, NewGroupFromClaims, FilterGroupClaims, ModifyGroupNames are each separate features that could be one CEL expression.
  • Every new policy need requires a Dex code change and release.
  • Combinatorial explosion of config options.

No Change

Without CEL or an equivalent:

  • Operators continue to request per-client connector restrictions, custom claims, claim transformations, and access policies — issues remain open indefinitely.
  • Dex accumulates more ad-hoc config fields, increasing maintenance burden.
  • Complex use cases require external reverse proxies, forking Dex, or middleware.

Future Improvements

  • CEL in other connectors — Extend CEL claim mapping beyond OIDC to LDAP (attribute mapping), SAML (assertion mapping), and other connectors with complex attribute mapping needs.
  • Policy testing framework — Unit test framework for operators to validate their CEL expressions against fixture data before deployment.
  • Connector selection via CEL — Replace the static connector-per-client mapping with a CEL expression that dynamically determines which connectors to show based on request attributes.