Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 79 additions & 67 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,37 +225,41 @@ type Config struct {
DefaultFiltersDir string `yaml:"default-filters-dir"`

// Auth:
EnableOAuth2GrantFlow bool `yaml:"enable-oauth2-grant-flow"`
Oauth2AuthURL string `yaml:"oauth2-auth-url"`
Oauth2TokenURL string `yaml:"oauth2-token-url"`
Oauth2RevokeTokenURL string `yaml:"oauth2-revoke-token-url"`
Oauth2TokeninfoURL string `yaml:"oauth2-tokeninfo-url"`
Oauth2TokeninfoTimeout time.Duration `yaml:"oauth2-tokeninfo-timeout"`
Oauth2TokeninfoCacheSize int `yaml:"oauth2-tokeninfo-cache-size"`
Oauth2TokeninfoCacheTTL time.Duration `yaml:"oauth2-tokeninfo-cache-ttl"`
Oauth2SecretFile string `yaml:"oauth2-secret-file"`
Oauth2ClientID string `yaml:"oauth2-client-id"`
Oauth2ClientSecret string `yaml:"oauth2-client-secret"`
Oauth2ClientIDFile string `yaml:"oauth2-client-id-file"`
Oauth2ClientSecretFile string `yaml:"oauth2-client-secret-file"`
Oauth2AuthURLParameters mapFlags `yaml:"oauth2-auth-url-parameters"`
Oauth2CallbackPath string `yaml:"oauth2-callback-path"`
Oauth2TokenintrospectionTimeout time.Duration `yaml:"oauth2-tokenintrospect-timeout"`
Oauth2AccessTokenHeaderName string `yaml:"oauth2-access-token-header-name"`
Oauth2TokeninfoSubjectKey string `yaml:"oauth2-tokeninfo-subject-key"`
Oauth2GrantTokeninfoKeys *listFlag `yaml:"oauth2-grant-tokeninfo-keys"`
Oauth2TokenCookieName string `yaml:"oauth2-token-cookie-name"`
Oauth2TokenCookieRemoveSubdomains int `yaml:"oauth2-token-cookie-remove-subdomains"`
Oauth2GrantInsecure bool `yaml:"oauth2-grant-insecure"`
WebhookTimeout time.Duration `yaml:"webhook-timeout"`
OidcSecretsFile string `yaml:"oidc-secrets-file"`
OIDCCookieValidity time.Duration `yaml:"oidc-cookie-validity"`
OidcDistributedClaimsTimeout time.Duration `yaml:"oidc-distributed-claims-timeout"`
OIDCCookieRemoveSubdomains int `yaml:"oidc-cookie-remove-subdomains"`
CredentialPaths *listFlag `yaml:"credentials-paths"`
CredentialsUpdateInterval time.Duration `yaml:"credentials-update-interval"`
HTTPMessageSignatureKeyFile string `yaml:"http-message-signature-key-file"`
HTTPMessageSignatureKeyID string `yaml:"http-message-signature-key-id"`
EnableOAuth2GrantFlow bool `yaml:"enable-oauth2-grant-flow"`
Oauth2AuthURL string `yaml:"oauth2-auth-url"`
Oauth2TokenURL string `yaml:"oauth2-token-url"`
Oauth2RevokeTokenURL string `yaml:"oauth2-revoke-token-url"`
Oauth2TokeninfoURL string `yaml:"oauth2-tokeninfo-url"`
Oauth2TokeninfoTimeout time.Duration `yaml:"oauth2-tokeninfo-timeout"`
Oauth2TokeninfoCacheSize int `yaml:"oauth2-tokeninfo-cache-size"`
Oauth2TokeninfoCacheTTL time.Duration `yaml:"oauth2-tokeninfo-cache-ttl"`
Oauth2SecretFile string `yaml:"oauth2-secret-file"`
Oauth2ClientID string `yaml:"oauth2-client-id"`
Oauth2ClientSecret string `yaml:"oauth2-client-secret"`
Oauth2ClientIDFile string `yaml:"oauth2-client-id-file"`
Oauth2ClientSecretFile string `yaml:"oauth2-client-secret-file"`
Oauth2AuthURLParameters mapFlags `yaml:"oauth2-auth-url-parameters"`
Oauth2CallbackPath string `yaml:"oauth2-callback-path"`
Oauth2TokenintrospectionTimeout time.Duration `yaml:"oauth2-tokenintrospect-timeout"`
Oauth2AccessTokenHeaderName string `yaml:"oauth2-access-token-header-name"`
Oauth2TokeninfoSubjectKey string `yaml:"oauth2-tokeninfo-subject-key"`
Oauth2GrantTokeninfoKeys *listFlag `yaml:"oauth2-grant-tokeninfo-keys"`
Oauth2TokenCookieName string `yaml:"oauth2-token-cookie-name"`
Oauth2TokenCookieRemoveSubdomains int `yaml:"oauth2-token-cookie-remove-subdomains"`
Oauth2GrantInsecure bool `yaml:"oauth2-grant-insecure"`
WebhookTimeout time.Duration `yaml:"webhook-timeout"`
OAuthTokenExchangeURL string `yaml:"oauth2-token-exchange-url"`
OAuthTokenExchangeClientID string `yaml:"oauth2-token-exchange-client-id"`
OAuthTokenExchangeClientSecretFile string `yaml:"oauth2-token-exchange-client-secret-file"`
OAuthTokenExchangeTimeout time.Duration `yaml:"oauth2-token-exchange-timeout"`
OidcSecretsFile string `yaml:"oidc-secrets-file"`
OIDCCookieValidity time.Duration `yaml:"oidc-cookie-validity"`
OidcDistributedClaimsTimeout time.Duration `yaml:"oidc-distributed-claims-timeout"`
OIDCCookieRemoveSubdomains int `yaml:"oidc-cookie-remove-subdomains"`
CredentialPaths *listFlag `yaml:"credentials-paths"`
CredentialsUpdateInterval time.Duration `yaml:"credentials-update-interval"`
HTTPMessageSignatureKeyFile string `yaml:"http-message-signature-key-file"`
HTTPMessageSignatureKeyID string `yaml:"http-message-signature-key-id"`

// TLS configuration for the validation webhook
ValidationWebhookEnabled bool `yaml:"validation-webhook-enabled"`
Expand Down Expand Up @@ -645,6 +649,10 @@ func NewConfig() *Config {
flag.IntVar(&cfg.Oauth2TokenCookieRemoveSubdomains, "oauth2-token-cookie-remove-subdomains", 1, "sets the number of subdomains to remove from the callback request hostname to obtain token cookie domain")
flag.BoolVar(&cfg.Oauth2GrantInsecure, "oauth2-grant-insecure", false, "omits Secure attribute of the token cookie and uses http scheme for callback url")
flag.DurationVar(&cfg.WebhookTimeout, "webhook-timeout", 2*time.Second, "sets the webhook request timeout duration")
flag.StringVar(&cfg.OAuthTokenExchangeURL, "oauth2-token-exchange-url", "", "sets the RFC 8693 token exchange endpoint for the tokenExchange() filter")
flag.StringVar(&cfg.OAuthTokenExchangeClientID, "oauth2-token-exchange-client-id", "", "sets the client ID for the tokenExchange() filter")
flag.StringVar(&cfg.OAuthTokenExchangeClientSecretFile, "oauth2-token-exchange-client-secret-file", "", "path to a file containing the client secret for the tokenExchange() filter; supports hot-reload")
flag.DurationVar(&cfg.OAuthTokenExchangeTimeout, "oauth2-token-exchange-timeout", 2*time.Second, "sets the HTTP timeout for calls to the token exchange endpoint")
Comment thread
szuecs marked this conversation as resolved.
flag.BoolVar(&cfg.ValidationWebhookEnabled, "validation-webhook-enabled", false, "enables validation webhook for incoming requests")
flag.StringVar(&cfg.ValidationWebhookAddress, "validation-webhook-address", ":9000", "address of the validation webhook service")
flag.StringVar(&cfg.ValidationWebhookCertFile, "validation-webhook-cert-file", "", "path to the certificate file for the validation webhook")
Expand Down Expand Up @@ -1149,42 +1157,46 @@ func (c *Config) ToOptions() skipper.Options {
RouteServerFilters: c.RouteServerFilters.filters,

// Auth:
EnableOAuth2GrantFlow: c.EnableOAuth2GrantFlow,
OAuth2AuthURL: c.Oauth2AuthURL,
OAuth2TokenURL: c.Oauth2TokenURL,
OAuth2RevokeTokenURL: c.Oauth2RevokeTokenURL,
OAuthTokeninfoURL: c.Oauth2TokeninfoURL,
OAuthTokeninfoTimeout: c.Oauth2TokeninfoTimeout,
OAuthTokeninfoCacheSize: c.Oauth2TokeninfoCacheSize,
OAuthTokeninfoCacheTTL: c.Oauth2TokeninfoCacheTTL,
OAuth2SecretFile: c.Oauth2SecretFile,
OAuth2ClientID: c.Oauth2ClientID,
OAuth2ClientSecret: c.Oauth2ClientSecret,
OAuth2ClientIDFile: c.Oauth2ClientIDFile,
OAuth2ClientSecretFile: c.Oauth2ClientSecretFile,
OAuth2CallbackPath: c.Oauth2CallbackPath,
OAuthTokenintrospectionTimeout: c.Oauth2TokenintrospectionTimeout,
OAuth2AuthURLParameters: c.Oauth2AuthURLParameters.values,
OAuth2AccessTokenHeaderName: c.Oauth2AccessTokenHeaderName,
OAuth2TokeninfoSubjectKey: c.Oauth2TokeninfoSubjectKey,
OAuth2GrantTokeninfoKeys: c.Oauth2GrantTokeninfoKeys.values,
OAuth2TokenCookieName: c.Oauth2TokenCookieName,
OAuth2TokenCookieRemoveSubdomains: c.Oauth2TokenCookieRemoveSubdomains,
OAuth2GrantInsecure: c.Oauth2GrantInsecure,
WebhookTimeout: c.WebhookTimeout,
OIDCSecretsFile: c.OidcSecretsFile,
OIDCCookieValidity: c.OIDCCookieValidity,
OIDCDistributedClaimsTimeout: c.OidcDistributedClaimsTimeout,
OIDCCookieRemoveSubdomains: c.OIDCCookieRemoveSubdomains,
CredentialsPaths: c.CredentialPaths.values,
CredentialsUpdateInterval: c.CredentialsUpdateInterval,
ValidationWebhookEnabled: c.ValidationWebhookEnabled,
ValidationWebhookAddress: c.ValidationWebhookAddress,
ValidationWebhookCertFile: c.ValidationWebhookCertFile,
ValidationWebhookKeyFile: c.ValidationWebhookKeyFile,
EnableAdvancedValidation: c.EnableAdvancedValidation,
HTTPMessageSignatureKeyFile: c.HTTPMessageSignatureKeyFile,
HTTPMessageSignatureKeyID: c.HTTPMessageSignatureKeyID,
EnableOAuth2GrantFlow: c.EnableOAuth2GrantFlow,
OAuth2AuthURL: c.Oauth2AuthURL,
OAuth2TokenURL: c.Oauth2TokenURL,
OAuth2RevokeTokenURL: c.Oauth2RevokeTokenURL,
OAuthTokeninfoURL: c.Oauth2TokeninfoURL,
OAuthTokeninfoTimeout: c.Oauth2TokeninfoTimeout,
OAuthTokeninfoCacheSize: c.Oauth2TokeninfoCacheSize,
OAuthTokeninfoCacheTTL: c.Oauth2TokeninfoCacheTTL,
OAuth2SecretFile: c.Oauth2SecretFile,
OAuth2ClientID: c.Oauth2ClientID,
OAuth2ClientSecret: c.Oauth2ClientSecret,
OAuth2ClientIDFile: c.Oauth2ClientIDFile,
OAuth2ClientSecretFile: c.Oauth2ClientSecretFile,
OAuth2CallbackPath: c.Oauth2CallbackPath,
OAuthTokenintrospectionTimeout: c.Oauth2TokenintrospectionTimeout,
OAuth2AuthURLParameters: c.Oauth2AuthURLParameters.values,
OAuth2AccessTokenHeaderName: c.Oauth2AccessTokenHeaderName,
OAuth2TokeninfoSubjectKey: c.Oauth2TokeninfoSubjectKey,
OAuth2GrantTokeninfoKeys: c.Oauth2GrantTokeninfoKeys.values,
OAuth2TokenCookieName: c.Oauth2TokenCookieName,
OAuth2TokenCookieRemoveSubdomains: c.Oauth2TokenCookieRemoveSubdomains,
OAuth2GrantInsecure: c.Oauth2GrantInsecure,
WebhookTimeout: c.WebhookTimeout,
OAuthTokenExchangeURL: c.OAuthTokenExchangeURL,
OAuthTokenExchangeClientID: c.OAuthTokenExchangeClientID,
OAuthTokenExchangeClientSecretFile: c.OAuthTokenExchangeClientSecretFile,
OAuthTokenExchangeTimeout: c.OAuthTokenExchangeTimeout,
OIDCSecretsFile: c.OidcSecretsFile,
OIDCCookieValidity: c.OIDCCookieValidity,
OIDCDistributedClaimsTimeout: c.OidcDistributedClaimsTimeout,
OIDCCookieRemoveSubdomains: c.OIDCCookieRemoveSubdomains,
CredentialsPaths: c.CredentialPaths.values,
CredentialsUpdateInterval: c.CredentialsUpdateInterval,
ValidationWebhookEnabled: c.ValidationWebhookEnabled,
ValidationWebhookAddress: c.ValidationWebhookAddress,
ValidationWebhookCertFile: c.ValidationWebhookCertFile,
ValidationWebhookKeyFile: c.ValidationWebhookKeyFile,
EnableAdvancedValidation: c.EnableAdvancedValidation,
HTTPMessageSignatureKeyFile: c.HTTPMessageSignatureKeyFile,
HTTPMessageSignatureKeyID: c.HTTPMessageSignatureKeyID,

// connections, timeouts:
WaitForHealthcheckInterval: c.WaitForHealthcheckInterval,
Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func defaultConfig(with func(*Config)) *Config {
Oauth2GrantTokeninfoKeys: commaListFlag(),
Oauth2TokenCookieName: "oauth2-grant",
Oauth2TokenCookieRemoveSubdomains: 1,
OAuthTokenExchangeTimeout: 2 * time.Second,
WebhookTimeout: 2 * time.Second,
OidcDistributedClaimsTimeout: 2 * time.Second,
OIDCCookieValidity: time.Hour,
Expand Down
53 changes: 53 additions & 0 deletions docs/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -1857,6 +1857,59 @@ Read client-id and client-secret from environment variables
secureOauthTokenintrospectionAllKV("issuerURL", "", "", "k1", "v1", "k2", "v2")
```

### Token Exchange
Comment thread
a4180p marked this conversation as resolved.

Token exchange performed by calling an external OAuth2 token endpoint.
The filter implements [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693)
and always responds directly to the client — the backend is never reached.

On success the filter serves the raw RFC 8693 JSON response from the token
endpoint (`200 OK`, `Content-Type: application/json`). On error it responds:

- No `Authorization: Bearer <token>` header → `401 Unauthorized`
- Token endpoint returns non-200 → the IdP's status code and body are forwarded
as-is with `Content-Type: application/json` (RFC 8693 §2.2.2 / RFC 6749 §5.2)
- Transport or network error → `502 Bad Gateway` with RFC 6749 §5.2 JSON error body
- Token endpoint returns 200 but the body is unparseable or missing
`access_token` → `502 Bad Gateway` with RFC 6749 §5.2 JSON error body

The token endpoint URL, client ID is static, and client secret is a
file to be externally rotated. The configuration supplied at
startup via `-oauth2-token-exchange-url`,
`-oauth2-token-exchange-client-id`, and
`-oauth2-token-exchange-client-secret-file` flags. The filter itself takes
the per-route arguments described below.

#### tokenExchange

If skipper is started with `-oauth2-token-exchange-url` flag, you can use
this filter.

The filter accepts zero, one, or two optional string arguments:

- First argument (optional): `audience` — the target service or resource
the issued token should be scoped to.
- Second argument (optional): `scope` — a space-separated list of scopes
to request for the issued token.

The filter reads the incoming `Authorization: Bearer <token>` header as
the subject token, posts an RFC 8693 token exchange request to the
configured endpoint using the operator-supplied client credentials, and
serves the token endpoint's JSON response body directly to the client.

Examples:

```
// exchange with no additional constraints
tokenExchange()

// exchange targeting a specific audience
tokenExchange("https://my-internal-service.example.org")

// exchange targeting a specific audience and requesting specific scopes
tokenExchange("https://my-internal-service.example.org", "read write")
```

### JWT
#### jwtValidation

Expand Down
13 changes: 4 additions & 9 deletions filters/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strings"

"github.com/zalando/skipper/filters"
Expand Down Expand Up @@ -163,16 +164,10 @@ func getStrings(args []any) ([]string, error) {
// right. Right can be a superset of left.
func all(left, right []string) bool {
for _, l := range left {
var found bool
for _, r := range right {
if l == r {
found = true
break
}
}
if !found {
return false
if slices.Contains(right, l) {
continue
}
return false
}
return true
}
4 changes: 4 additions & 0 deletions filters/auth/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ func cleanupAuthClients() {
value.(*net.Client).Close()
return true
})

for _, c := range tokenExchangeClients {
c.Close()
}
}
Loading
Loading