mirror of
https://github.com/nais/wonderwall.git
synced 2026-02-14 17:49:54 +00:00
feat: Add option for propagating id_token to upstream app
Fixes #315 Co-authored-by: tronghn <trong.huu.nguyen@nav.no>
This commit is contained in:
@@ -16,52 +16,53 @@ openid.client-id -> WONDERWALL_OPENID_CLIENT_ID
|
||||
|
||||
The following flags are available:
|
||||
|
||||
| Flag | Type | Default Value | Description |
|
||||
|:----------------------------------|:---------|:---------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `auto-login` | boolean | `false` | Enforce authentication if the user does not have a valid session for all matching upstream paths. Automatically redirects HTTP navigation requests to login, otherwise responds with 401 with the Location header set. |
|
||||
| `auto-login-ignore-paths` | strings | | Comma separated list of absolute paths to ignore when `auto-login` is enabled. Supports basic wildcard matching with glob-style asterisks. Invalid patterns are ignored. |
|
||||
| `bind-address` | string | `127.0.0.1:3000` | Listen address for public connections. |
|
||||
| `cookie.prefix` | string | `io.nais.wonderwall` | Prefix for cookie names. |
|
||||
| `cookie.same-site` | string | `Lax` | SameSite attribute for session cookies. One of [Strict, Lax, None]. |
|
||||
| `cookie.secure` | string | `true` | Set secure flag on session cookies. Can only be disabled when `ingress` only consist of localhost hosts. Generally, disabling this is only necessary when using Safari. |
|
||||
| `encryption-key` | string | | Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store. |
|
||||
| `ingress` | strings | | Comma separated list of ingresses used to access the main application. |
|
||||
| `log-format` | string | `json` | Log format, either `json` or `text`. |
|
||||
| `log-level` | string | `info` | Logging verbosity level. |
|
||||
| `metrics-bind-address` | string | `127.0.0.1:3001` | Listen address for metrics only. |
|
||||
| `openid.acr-values` | string | | Space separated string that configures the default security level (`acr_values`) parameter for authorization requests. |
|
||||
| `openid.audiences` | strings | | List of additional trusted audiences (other than the client_id) for OpenID Connect id_token validation. |
|
||||
| `openid.client-id` | string | | Client ID for the OpenID client. |
|
||||
| `openid.client-jwk` | string | | JWK containing the private key for the OpenID client in string format. If configured, this takes precedence over `openid.client-secret`. |
|
||||
| `openid.client-secret` | string | | Client secret for the OpenID client. Overridden by `openid.client-jwk`, if configured. |
|
||||
| `openid.id-token-signing-alg` | string | `RS256` | Expected JWA value (as defined in RFC 7518) of public keys for validating id_token signatures. This only applies where the key's `alg` header is not set. |
|
||||
| `openid.post-logout-redirect-uri` | string | | URI for redirecting the user after successful logout at the Identity Provider. |
|
||||
| `openid.provider` | string | `openid` | Provider configuration to load and use, either `openid`, `azure`, `idporten`. |
|
||||
| `openid.resource-indicator` | string | | OAuth2 resource indicator to include in authorization request for acquiring audience-restricted tokens. |
|
||||
| `openid.scopes` | strings | | Comma separated list of additional scopes (other than `openid`) that should be used during the login flow. |
|
||||
| `openid.ui-locales` | string | | Space-separated string that configures the default UI locale (`ui_locales`) parameter for OAuth2 consent screen. |
|
||||
| `openid.well-known-url` | string | | URI to the well-known OpenID Configuration metadata document. |
|
||||
| `redis.address` | string | | Deprecated: prefer using `redis.uri`. Address of the Redis instance (host:port). An empty value will use in-memory session storage. Does not override address set by `redis.uri`. |
|
||||
| `redis.connection-idle-timeout` | int | `0` | Idle timeout for Redis connections, in seconds. If non-zero, the value should be less than the client timeout configured at the Redis server. A value of -1 disables timeout. If zero, the default value from go-redis is used (30 minutes). Overrides options set by `redis.uri`. |
|
||||
| `redis.password` | string | | Password for Redis. Overrides password set by `redis.uri`. |
|
||||
| `redis.tls` | boolean | `true` | Whether or not to use TLS for connecting to Redis. Does not override TLS config set by `redis.uri`. |
|
||||
| `redis.uri` | string | | Redis URI string. An empty value will fall back to `redis-address`. |
|
||||
| `redis.username` | string | | Username for Redis. Overrides username set by `redis.uri`. |
|
||||
| `session.forward-auth` | boolean | `false` | Enable endpoint for forward authentication. |
|
||||
| `session.inactivity` | boolean | `false` | Automatically expire user sessions if they have not refreshed their tokens within a given duration. |
|
||||
| `session.inactivity-timeout` | duration | `30m` | Inactivity timeout for user sessions. |
|
||||
| `session.max-lifetime` | duration | `10h` | Max lifetime for user sessions. |
|
||||
| `shutdown-graceful-period` | duration | `30s` | Graceful shutdown period when receiving a shutdown signal after which the server is forcibly exited. |
|
||||
| `shutdown-wait-before-period` | duration | `0s` | Wait period when receiving a shutdown signal before actually starting a graceful shutdown. Useful for allowing propagation of Endpoint updates in Kubernetes. |
|
||||
| `sso.domain` | string | | The domain that the session cookies should be set for, usually the second-level domain name (e.g. `example.com`). |
|
||||
| `sso.enabled` | boolean | `false` | Enable single sign-on mode; one server acting as the OIDC Relying Party, and N proxies. The proxies delegate most endpoint operations to the server, and only implements a reverse proxy that reads the user's session data from the shared store. |
|
||||
| `sso.mode` | string | `server` | The SSO mode for this instance. Must be one of `server` or `proxy`. |
|
||||
| `sso.server-default-redirect-url` | string | | The URL that the SSO server should redirect to by default if a given redirect query parameter is invalid. |
|
||||
| `sso.server-url` | string | | The URL used by the proxy to point to the SSO server instance. |
|
||||
| `sso.session-cookie-name` | string | | Session cookie name. Must be the same across all SSO Servers and Proxies that should share sessions. |
|
||||
| `upstream-host` | string | `127.0.0.1:8080` | Address of upstream host. |
|
||||
| `upstream-ip` | string | | IP of upstream host. Overrides `upstream-host` if set. |
|
||||
| `upstream-port` | int | | Port of upstream host. Overrides `upstream-host` if set. |
|
||||
| Flag | Type | Default Value | Description |
|
||||
|:-------------------------------------------|:---------|:----------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `auto-login` | boolean | `false` | Enforce authentication if the user does not have a valid session for all matching upstream paths. Automatically redirects HTTP navigation requests to login, otherwise responds with 401 with the Location header set. |
|
||||
| `auto-login-ignore-paths` | strings | | Comma separated list of absolute paths to ignore when `auto-login` is enabled. Supports basic wildcard matching with glob-style asterisks. Invalid patterns are ignored. |
|
||||
| `bind-address` | string | `127.0.0.1:3000` | Listen address for public connections. |
|
||||
| `cookie.prefix` | string | `io.nais.wonderwall` | Prefix for cookie names. |
|
||||
| `cookie.same-site` | string | `Lax` | SameSite attribute for session cookies. One of [Strict, Lax, None]. |
|
||||
| `cookie.secure` | string | `true` | Set secure flag on session cookies. Can only be disabled when `ingress` only consist of localhost hosts. Generally, disabling this is only necessary when using Safari. |
|
||||
| `encryption-key` | string | | Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store. |
|
||||
| `ingress` | strings | | Comma separated list of ingresses used to access the main application. |
|
||||
| `log-format` | string | `json` | Log format, either `json` or `text`. |
|
||||
| `log-level` | string | `info` | Logging verbosity level. |
|
||||
| `metrics-bind-address` | string | `127.0.0.1:3001` | Listen address for metrics only. |
|
||||
| `openid.acr-values` | string | | Space separated string that configures the default security level (`acr_values`) parameter for authorization requests. |
|
||||
| `openid.audiences` | strings | | List of additional trusted audiences (other than the client_id) for OpenID Connect id_token validation. |
|
||||
| `openid.client-id` | string | | Client ID for the OpenID client. |
|
||||
| `openid.client-jwk` | string | | JWK containing the private key for the OpenID client in string format. If configured, this takes precedence over `openid.client-secret`. |
|
||||
| `openid.client-secret` | string | | Client secret for the OpenID client. Overridden by `openid.client-jwk`, if configured. |
|
||||
| `openid.id-token-signing-alg` | string | `RS256` | Expected JWA value (as defined in RFC 7518) of public keys for validating id_token signatures. This only applies where the key's `alg` header is not set. |
|
||||
| `openid.post-logout-redirect-uri` | string | | URI for redirecting the user after successful logout at the Identity Provider. |
|
||||
| `openid.provider` | string | `openid` | Provider configuration to load and use, either `openid`, `azure`, `idporten`. |
|
||||
| `openid.resource-indicator` | string | | OAuth2 resource indicator to include in authorization request for acquiring audience-restricted tokens. |
|
||||
| `openid.scopes` | strings | | Comma separated list of additional scopes (other than `openid`) that should be used during the login flow. |
|
||||
| `openid.ui-locales` | string | | Space-separated string that configures the default UI locale (`ui_locales`) parameter for OAuth2 consent screen. |
|
||||
| `openid.well-known-url` | string | | URI to the well-known OpenID Configuration metadata document. |
|
||||
| `redis.address` | string | | Deprecated: prefer using `redis.uri`. Address of the Redis instance (host:port). An empty value will use in-memory session storage. Does not override address set by `redis.uri`. |
|
||||
| `redis.connection-idle-timeout` | int | `0` | Idle timeout for Redis connections, in seconds. If non-zero, the value should be less than the client timeout configured at the Redis server. A value of -1 disables timeout. If zero, the default value from go-redis is used (30 minutes). Overrides options set by `redis.uri`. |
|
||||
| `redis.password` | string | | Password for Redis. Overrides password set by `redis.uri`. |
|
||||
| `redis.tls` | boolean | `true` | Whether or not to use TLS for connecting to Redis. Does not override TLS config set by `redis.uri`. |
|
||||
| `redis.uri` | string | | Redis URI string. An empty value will fall back to `redis-address`. |
|
||||
| `redis.username` | string | | Username for Redis. Overrides username set by `redis.uri`. |
|
||||
| `session.forward-auth` | boolean | `false` | Enable endpoint for forward authentication. |
|
||||
| `session.inactivity` | boolean | `false` | Automatically expire user sessions if they have not refreshed their tokens within a given duration. |
|
||||
| `session.inactivity-timeout` | duration | `30m` | Inactivity timeout for user sessions. |
|
||||
| `session.max-lifetime` | duration | `10h` | Max lifetime for user sessions. |
|
||||
| `shutdown-graceful-period` | duration | `30s` | Graceful shutdown period when receiving a shutdown signal after which the server is forcibly exited. |
|
||||
| `shutdown-wait-before-period` | duration | `0s` | Wait period when receiving a shutdown signal before actually starting a graceful shutdown. Useful for allowing propagation of Endpoint updates in Kubernetes. |
|
||||
| `sso.domain` | string | | The domain that the session cookies should be set for, usually the second-level domain name (e.g. `example.com`). |
|
||||
| `sso.enabled` | boolean | `false` | Enable single sign-on mode; one server acting as the OIDC Relying Party, and N proxies. The proxies delegate most endpoint operations to the server, and only implements a reverse proxy that reads the user's session data from the shared store. |
|
||||
| `sso.mode` | string | `server` | The SSO mode for this instance. Must be one of `server` or `proxy`. |
|
||||
| `sso.server-default-redirect-url` | string | | The URL that the SSO server should redirect to by default if a given redirect query parameter is invalid. |
|
||||
| `sso.server-url` | string | | The URL used by the proxy to point to the SSO server instance. |
|
||||
| `sso.session-cookie-name` | string | | Session cookie name. Must be the same across all SSO Servers and Proxies that should share sessions. |
|
||||
| `upstream-host` | string | `127.0.0.1:8080` | Address of upstream host. |
|
||||
| `upstream-ip` | string | | IP of upstream host. Overrides `upstream-host` if set. |
|
||||
| `upstream-port` | int | | Port of upstream host. Overrides `upstream-host` if set. |
|
||||
| `upstream-include-id-token` | boolean | `false` | Include ID token in upstream requests in 'X-Wonderwall-Id-Token' header. |
|
||||
|
||||
Boolean flags are by default set to `false` unless noted otherwise.
|
||||
|
||||
|
||||
@@ -24,16 +24,17 @@ type Config struct {
|
||||
ShutdownWaitBeforePeriod time.Duration `json:"shutdown-wait-before-period"`
|
||||
Version string `json:"version"`
|
||||
|
||||
AutoLogin bool `json:"auto-login"`
|
||||
AutoLoginIgnorePaths []string `json:"auto-login-ignore-paths"`
|
||||
Cookie Cookie `json:"cookie"`
|
||||
EncryptionKey string `json:"encryption-key"`
|
||||
Ingresses []string `json:"ingress"`
|
||||
LegacyCookie bool `json:"legacy-cookie"`
|
||||
UpstreamAccessLogs bool `json:"upstream-access-logs"`
|
||||
UpstreamHost string `json:"upstream-host"`
|
||||
UpstreamIP string `json:"upstream-ip"`
|
||||
UpstreamPort int `json:"upstream-port"`
|
||||
AutoLogin bool `json:"auto-login"`
|
||||
AutoLoginIgnorePaths []string `json:"auto-login-ignore-paths"`
|
||||
Cookie Cookie `json:"cookie"`
|
||||
EncryptionKey string `json:"encryption-key"`
|
||||
Ingresses []string `json:"ingress"`
|
||||
LegacyCookie bool `json:"legacy-cookie"`
|
||||
UpstreamAccessLogs bool `json:"upstream-access-logs"`
|
||||
UpstreamHost string `json:"upstream-host"`
|
||||
UpstreamIP string `json:"upstream-ip"`
|
||||
UpstreamPort int `json:"upstream-port"`
|
||||
UpstreamIncludeIdToken bool `json:"upstream-include-id-token"`
|
||||
|
||||
OpenTelemetry OpenTelemetry `json:"otel"`
|
||||
OpenID OpenID `json:"openid"`
|
||||
@@ -50,13 +51,14 @@ const (
|
||||
ShutdownGracefulPeriod = "shutdown-graceful-period"
|
||||
ShutdownWaitBeforePeriod = "shutdown-wait-before-period"
|
||||
|
||||
AutoLogin = "auto-login"
|
||||
AutoLoginIgnorePaths = "auto-login-ignore-paths"
|
||||
Ingress = "ingress"
|
||||
UpstreamAccessLogs = "upstream-access-logs"
|
||||
UpstreamHost = "upstream-host"
|
||||
UpstreamIP = "upstream-ip"
|
||||
UpstreamPort = "upstream-port"
|
||||
AutoLogin = "auto-login"
|
||||
AutoLoginIgnorePaths = "auto-login-ignore-paths"
|
||||
Ingress = "ingress"
|
||||
UpstreamAccessLogs = "upstream-access-logs"
|
||||
UpstreamHost = "upstream-host"
|
||||
UpstreamIP = "upstream-ip"
|
||||
UpstreamPort = "upstream-port"
|
||||
UpstreamIncludeIdToken = "upstream-include-id-token"
|
||||
)
|
||||
|
||||
var logger = log.WithField("logger", "wonderwall.config")
|
||||
@@ -78,6 +80,7 @@ func Initialize() (*Config, error) {
|
||||
flag.String(UpstreamHost, "127.0.0.1:8080", "Address of upstream host.")
|
||||
flag.String(UpstreamIP, "", "IP of upstream host. Overrides 'upstream-host' if set.")
|
||||
flag.Int(UpstreamPort, 0, "Port of upstream host. Overrides 'upstream-host' if set.")
|
||||
flag.Bool(UpstreamIncludeIdToken, false, "Include ID token in upstream requests in 'X-Wonderwall-Id-Token' header.")
|
||||
|
||||
cookieFlags()
|
||||
openidFlags()
|
||||
|
||||
@@ -82,7 +82,7 @@ func NewStandalone(
|
||||
Ingresses: ingresses,
|
||||
Redirect: url.NewStandaloneRedirect(),
|
||||
SessionManager: sessionManager,
|
||||
UpstreamProxy: NewUpstreamProxy(upstream, cfg.UpstreamAccessLogs),
|
||||
UpstreamProxy: NewUpstreamProxy(upstream, cfg.UpstreamAccessLogs, cfg.UpstreamIncludeIdToken),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ func NewSSOProxy(cfg *config.Config, crypter crypto.Crypter) (*SSOProxy, error)
|
||||
SSOServerURL: serverURL,
|
||||
SSOServerReverseProxy: NewReverseProxy(serverURL, false),
|
||||
SessionReader: sessionReader,
|
||||
UpstreamProxy: NewUpstreamProxy(upstream, cfg.UpstreamAccessLogs),
|
||||
UpstreamProxy: NewUpstreamProxy(upstream, cfg.UpstreamAccessLogs, cfg.UpstreamIncludeIdToken),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -29,11 +29,13 @@ type ReverseProxySource interface {
|
||||
type ReverseProxy struct {
|
||||
*httputil.ReverseProxy
|
||||
EnableAccessLogs bool
|
||||
IncludeIdToken bool
|
||||
}
|
||||
|
||||
func NewUpstreamProxy(upstream *urllib.URL, enableAccessLogs bool) *ReverseProxy {
|
||||
func NewUpstreamProxy(upstream *urllib.URL, enableAccessLogs bool, includeIdToken bool) *ReverseProxy {
|
||||
rp := NewReverseProxy(upstream, true)
|
||||
rp.EnableAccessLogs = enableAccessLogs
|
||||
rp.IncludeIdToken = includeIdToken
|
||||
return rp
|
||||
}
|
||||
|
||||
@@ -68,6 +70,11 @@ func NewReverseProxy(upstream *urllib.URL, preserveInboundHostHeader bool) *Reve
|
||||
if ok {
|
||||
r.Out.Header.Set("authorization", "Bearer "+accessToken)
|
||||
}
|
||||
|
||||
idToken, ok := mw.IdTokenFrom(r.In.Context())
|
||||
if ok {
|
||||
r.Out.Header.Set("X-Wonderwall-Id-Token", idToken)
|
||||
}
|
||||
},
|
||||
Transport: server.DefaultTransport(),
|
||||
}
|
||||
@@ -117,6 +124,10 @@ func (rp *ReverseProxy) Handler(src ReverseProxySource, w http.ResponseWriter, r
|
||||
|
||||
if isAuthenticated {
|
||||
ctx = mw.WithAccessToken(ctx, accessToken)
|
||||
if rp.IncludeIdToken {
|
||||
idToken := sess.IDToken()
|
||||
ctx = mw.WithIdToken(ctx, idToken)
|
||||
}
|
||||
|
||||
if rp.EnableAccessLogs && isRelevantAccessLog(r) {
|
||||
logger.Info("default: authenticated request")
|
||||
|
||||
@@ -437,4 +437,45 @@ func TestReverseProxy(t *testing.T) {
|
||||
}...)
|
||||
assertUpstreamOKResponse(t, resp)
|
||||
})
|
||||
|
||||
t.Run("request should not include idToken by default", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// acquire session
|
||||
login(t, rpClient, idp)
|
||||
|
||||
up.requestCallback = func(r *http.Request) {
|
||||
assert.Empty(t, r.Header.Get("x-wonderwall-id-token"))
|
||||
}
|
||||
|
||||
resp := get(t, rpClient, idp.RelyingPartyServer.URL)
|
||||
assertUpstreamOKResponse(t, resp)
|
||||
})
|
||||
|
||||
t.Run("request should include idToken", func(t *testing.T) {
|
||||
cfg := mock.Config()
|
||||
cfg.UpstreamHost = up.URL.Host
|
||||
cfg.UpstreamIncludeIdToken = true
|
||||
idp := mock.NewIdentityProvider(cfg)
|
||||
defer idp.Close()
|
||||
|
||||
up.SetIdentityProvider(idp)
|
||||
rpClient := idp.RelyingPartyClient()
|
||||
|
||||
// acquire session
|
||||
login(t, rpClient, idp)
|
||||
|
||||
up.requestCallback = func(r *http.Request) {
|
||||
assert.NotEmpty(t, r.Header.Get("x-wonderwall-id-token"))
|
||||
}
|
||||
|
||||
resp := get(t, rpClient, idp.RelyingPartyServer.URL)
|
||||
assertUpstreamOKResponse(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type contextKey string
|
||||
|
||||
const (
|
||||
ctxAccessToken = contextKey("AccessToken")
|
||||
ctxIdToken = contextKey("IdToken")
|
||||
ctxIngress = contextKey("Ingress")
|
||||
ctxPath = contextKey("Path")
|
||||
)
|
||||
@@ -24,6 +25,15 @@ func WithAccessToken(ctx context.Context, accessToken string) context.Context {
|
||||
return context.WithValue(ctx, ctxAccessToken, accessToken)
|
||||
}
|
||||
|
||||
func IdTokenFrom(ctx context.Context) (string, bool) {
|
||||
idToken, ok := ctx.Value(ctxIdToken).(string)
|
||||
return idToken, ok
|
||||
}
|
||||
|
||||
func WithIdToken(ctx context.Context, idToken string) context.Context {
|
||||
return context.WithValue(ctx, ctxIdToken, idToken)
|
||||
}
|
||||
|
||||
func IngressFrom(ctx context.Context) (ingress.Ingress, bool) {
|
||||
i, ok := ctx.Value(ctxIngress).(ingress.Ingress)
|
||||
return i, ok
|
||||
|
||||
Reference in New Issue
Block a user