Skip to content

feat: OIDC configurable profiles with template support and Kubernetes annotation passing#3917

Open
mjungsbluth wants to merge 8 commits into
masterfrom
oidc-profiles
Open

feat: OIDC configurable profiles with template support and Kubernetes annotation passing#3917
mjungsbluth wants to merge 8 commits into
masterfrom
oidc-profiles

Conversation

@mjungsbluth
Copy link
Copy Markdown
Collaborator

@mjungsbluth mjungsbluth commented Mar 13, 2026

Usage

Define profiles

Pass named profiles via -oidc-profiles (inline YAML) or the new -oidc-profiles-file flag (path to a YAML file; the two flags are mutually exclusive):

# profiles.yaml
myprofile:
  idp-url: https://idp.example.com
  client-id: my-client-id
  client-secret: secretRef:my-client-secret
  callback-url: https://{{.Request.Host}}/.well-known/oauth2-callback
  scopes: email profile
skipper -oidc-secrets-file /path/to/secrets \
        -oidc-profiles-file /path/to/profiles.yaml

All fields except idp-url support Go text/template expressions resolved at request time:

Expression Value
{{.Request.Host}} Hostname from the incoming request
{{index .Annotations "key"}} Value set by a preceding annotate() filter

client-id and client-secret also accept the existing secretRef: prefix and for client-secret this is the secure default.

Reference profiles in routes

Use profile:<name> as the first argument to any oauthOidc* filter. Claims are still passed as a second argument:

myroute: * -> oauthOidcAnyClaims("profile:myprofile", "groups") -> "https://backend";

Multi-tenant with Kubernetes annotations

Define a template-driven profile:

oidc-profiles:
  multi-tenant:
    idp-url: https://idp.example.com
    client-id: '{{index .Annotations "oidc/client-id"}}'
    client-secret: 'secretRef:{{index .Annotations "oidc/client-secret"}}'
    callback-url: https://{{.Request.Host}}/.well-known/oauth2-callback
    scopes: email profile

Start Skipper with the new -kubernetes-annotations-to-route-annotations flag to automatically inject Kubernetes resource annotations as annotate() filters into generated routes:

skipper -kubernetes-annotations-to-route-annotations=oidc/client-id,oidc/client-secret

An optional -kubernetes-annotations-to-route-annotations-prefix prepends a string to each key in the generated annotate() call (no separator added; K8s annotation lookup key is unchanged):

# annotation "oidc/client-id" produces annotate("k8s:oidc/client-id", value)
skipper -kubernetes-annotations-to-route-annotations=oidc/client-id \
        -kubernetes-annotations-to-route-annotations-prefix=k8s:

Annotate Kubernetes Ingress or RouteGroup resources:

metadata:
  annotations:
    oidc/client-id: tenant-abc-client
    oidc/client-secret: tenant-abc
    zalando.org/skipper-filter: 'oauthOidcAnyClaims("profile:multi-tenant", "groups")'

New flags

Flag Description
-oidc-profiles Named OIDC profiles as inline YAML. Mutually exclusive with -oidc-profiles-file.
-oidc-profiles-file Path to a YAML file of named OIDC profiles. Mutually exclusive with -oidc-profiles.
-kubernetes-annotations-to-route-annotations Comma-separated annotation keys to inject as annotate() filters into routes generated from Kubernetes resources.
-kubernetes-annotations-to-route-annotations-prefix Optional prefix prepended to the key in generated annotate() filter calls (no separator added).

Architecture

  • filters/auth/oidcprofile.go (new) — OidcProfile struct with YAML tags; tokenOidcProfileFilter which resolves Go text/template fields at request time and caches fully-constructed tokenOidcFilter delegates in a sync.Map keyed by SHA-256 hash of resolved parameters. Provider discovery runs once at CreateFilter time so it is shared across all delegates.
  • filters/auth/oidc.goCreateFilter detects the profile: prefix and delegates to createProfileFilter, which looks up the profile, discovers the provider, and returns a tokenOidcProfileFilter. OidcOptions gains a Profiles map[string]OidcProfile field.
  • dataclients/kubernetes/annotations.go — new injectAnnotateFilters(annotations, keys, prefix, route) helper; prepends annotate(prefix+key, value) filters for each configured key present on the resource. Called from both Ingress (ingressv1.go) and RouteGroup (routegroup.go) conversion paths.
  • config/config.go / skipper.go — four new flags wired through skipper.Options → kubernetes.Options / auth.OidcOptions.

Comment thread filters/auth/oidc.go
Comment thread filters/auth/oidcprofile.go
Comment thread filters/auth/oidcprofile.go
@mjungsbluth mjungsbluth added major moderate risk, for example new API, small filter changes that have no risk like refactoring or logs do-not-merge and removed documentation labels Mar 13, 2026
@mjungsbluth mjungsbluth marked this pull request as ready for review March 13, 2026 21:42
Comment thread filters/auth/oidcprofile.go Outdated
Comment thread filters/auth/oidcprofile.go
…ing templating

Signed-off-by: Magnus Jungsbluth <magnus.jungsbluth@zalando.de>
Signed-off-by: magnus-jungsbluth_zse <magnus.jungsbluth@zalando.de>
Signed-off-by: Magnus Jungsbluth <magnus.jungsbluth@zalando.de>
Signed-off-by: magnus-jungsbluth_zse <magnus.jungsbluth@zalando.de>
Signed-off-by: magnus-jungsbluth_zse <magnus.jungsbluth@zalando.de>
Signed-off-by: magnus-jungsbluth_zse <magnus.jungsbluth@zalando.de>
Signed-off-by: magnus-jungsbluth_zse <magnus.jungsbluth@zalando.de>
Signed-off-by: magnus-jungsbluth_zse <magnus.jungsbluth@zalando.de>
Comment thread filters/auth/oidcprofile_e2e_test.go Outdated
}

mu.Lock()
lastRedirectURI = redirectURI
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This should be rather set with a test fixture (i.e. the test case should state which redirect_uri is configured. Then the redirect_uri derived from the OIDC profile needs to match.

- Fix filter ordering: injectAnnotateFilters now runs after
  ic.annotationFilters and default-filter prepends so that annotate()
  filters are at the head of the chain and their values are visible to
  downstream filters such as oauthOidc* profile filters.
  Add a fixture that exercises the combined zalando.org/skipper-filter
  + annotation-injection path to protect the ordering going forward.

- Validate profiles at startup: OidcProfile.Validate() is now called
  in oidcProfilesMap() so malformed templates, missing IdpURL, and
  template expressions inside IdpURL are caught at process start rather
  than on the first request. The IdpURL-static check is also moved into
  Validate() itself, removing the duplicate in createProfileFilter().

- Explicit redirect_uri in e2e tests: createFlexOIDCServer now returns
  a *flexOIDCServer struct with a setExpectedRedirectURI method. Test
  cases that go through the full auth-code flow set the expected
  redirect_uri derived from proxy.URL after the proxy is started, so
  the OIDC server actively rejects a wrong callback URL instead of
  silently accepting whatever comes in.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge documentation major moderate risk, for example new API, small filter changes that have no risk like refactoring or logs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant