Files
wonderwall/pkg/config/config.go
Trong Huu Nguyen 350d7ff780 feat(cookie): allow configuration of name prefix
This is to alleviate issues with deployments on different
subdomains using overlapping cookie names where browsers
behave unpredictably.
2023-05-08 10:23:27 +02:00

206 lines
7.3 KiB
Go

package config
import (
"fmt"
"net/url"
"time"
"github.com/nais/liberator/pkg/conftools"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/nais/wonderwall/pkg/logging"
)
type Config struct {
BindAddress string `json:"bind-address"`
LogFormat string `json:"log-format"`
LogLevel string `json:"log-level"`
MetricsBindAddress string `json:"metrics-bind-address"`
AutoLogin bool `json:"auto-login"`
AutoLoginIgnorePaths []string `json:"auto-login-ignore-paths"`
CookiePrefix string `json:"cookie-prefix"`
EncryptionKey string `json:"encryption-key"`
Ingresses []string `json:"ingress"`
Session Session `json:"session"`
UpstreamHost string `json:"upstream-host"`
OpenID OpenID `json:"openid"`
Redis Redis `json:"redis"`
SSO SSO `json:"sso"`
}
type Session struct {
Inactivity bool `json:"inactivity"`
InactivityTimeout time.Duration `json:"inactivity-timeout"`
MaxLifetime time.Duration `json:"max-lifetime"`
Refresh bool `json:"refresh"`
}
type SSO struct {
Enabled bool `json:"enabled"`
Domain string `json:"domain"`
Mode SSOMode `json:"mode"`
SessionCookieName string `json:"session-cookie-name"`
ServerURL string `json:"server-url"`
ServerDefaultRedirectURL string `json:"server-default-redirect-url"`
}
func (in SSO) IsServer() bool {
return in.Enabled && in.Mode == SSOModeServer
}
type SSOMode string
const (
SSOModeServer SSOMode = "server"
SSOModeProxy SSOMode = "proxy"
)
const (
BindAddress = "bind-address"
LogFormat = "log-format"
LogLevel = "log-level"
MetricsBindAddress = "metrics-bind-address"
AutoLogin = "auto-login"
AutoLoginIgnorePaths = "auto-login-ignore-paths"
CookiePrefix = "cookie-prefix"
EncryptionKey = "encryption-key"
Ingress = "ingress"
UpstreamHost = "upstream-host"
SessionCookieName = "session.cookie-name"
SessionInactivity = "session.inactivity"
SessionInactivityTimeout = "session.inactivity-timeout"
SessionMaxLifetime = "session.max-lifetime"
SessionRefresh = "session.refresh"
SSOEnabled = "sso.enabled"
SSODomain = "sso.domain"
SSOModeFlag = "sso.mode"
SSOServerDefaultRedirectURL = "sso.server-default-redirect-url"
SSOSessionCookieName = "sso.session-cookie-name"
SSOServerURL = "sso.server-url"
)
func Initialize() (*Config, error) {
conftools.Initialize("wonderwall")
flag.String(BindAddress, "127.0.0.1:3000", "Listen address for public connections.")
flag.String(LogFormat, "json", "Log format, either 'json' or 'text'.")
flag.String(LogLevel, "info", "Logging verbosity level.")
flag.String(MetricsBindAddress, "127.0.0.1:3001", "Listen address for metrics only.")
flag.Bool(AutoLogin, false, "Automatically redirect all HTTP GET requests to login if the user does not have a valid session for all matching upstream paths.")
flag.StringSlice(AutoLoginIgnorePaths, []string{}, "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.")
flag.String(CookiePrefix, "io.nais.wonderwall", "Prefix for cookie names.")
flag.String(EncryptionKey, "", "Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store.")
flag.StringSlice(Ingress, []string{}, "Comma separated list of ingresses used to access the main application.")
flag.String(UpstreamHost, "127.0.0.1:8080", "Address of upstream host.")
flag.Bool(SessionInactivity, false, "Automatically expire user sessions if they have not refreshed their tokens within a given duration.")
flag.Duration(SessionInactivityTimeout, 30*time.Minute, "Inactivity timeout for user sessions.")
flag.Duration(SessionMaxLifetime, time.Hour, "Max lifetime for user sessions.")
flag.Bool(SessionRefresh, false, "Enable refresh tokens. In standalone mode, will automatically refresh tokens if they are expired as long as the session is valid (i.e. not exceeding 'session.max-lifetime' or 'session.inactivity-timeout').")
flag.Bool(SSOEnabled, 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.")
flag.String(SSODomain, "", "The domain that the session cookies should be set for, usually the second-level domain name (e.g. example.com).")
flag.String(SSOModeFlag, string(SSOModeServer), "The SSO mode for this instance. Must be one of 'server' or 'proxy'.")
flag.String(SSOSessionCookieName, "", "Session cookie name. Must be the same across all SSO Servers and Proxies.")
flag.String(SSOServerDefaultRedirectURL, "", "The URL that the SSO server should redirect to by default if a given redirect query parameter is invalid.")
flag.String(SSOServerURL, "", "The URL used by the proxy to point to the SSO server instance.")
redisFlags()
openIDFlags()
flag.String(OpenIDProvider, string(ProviderOpenID), "Provider configuration to load and use, either 'openid', 'azure', 'idporten'.")
flag.Parse()
if err := viper.ReadInConfig(); err != nil {
if err.(viper.ConfigFileNotFoundError) != err {
return nil, err
}
}
if err := viper.BindPFlags(flag.CommandLine); err != nil {
return nil, err
}
switch Provider(viper.GetString(OpenIDProvider)) {
case ProviderIDPorten:
idportenFlags()
case ProviderAzure:
azureFlags()
default:
viper.Set(OpenIDProvider, ProviderOpenID)
}
cfg := new(Config)
if err := conftools.Load(cfg); err != nil {
return nil, err
}
if err := logging.Setup(cfg.LogLevel, cfg.LogFormat); err != nil {
return nil, err
}
log.Tracef("Trace logging enabled")
maskedConfig := []string{
OpenIDClientJWK,
EncryptionKey,
RedisPassword,
}
for _, line := range conftools.Format(maskedConfig) {
log.WithField("logger", "wonderwall.config").Info(line)
}
err := cfg.Validate()
if err != nil {
return nil, fmt.Errorf("validating config: %w", err)
}
return cfg, nil
}
func (c *Config) Validate() error {
if c.Session.Inactivity && !c.Session.Refresh {
return fmt.Errorf("%q cannot be enabled without %q", SessionInactivity, SessionRefresh)
}
if c.SSO.Enabled {
if len(c.Redis.Address) == 0 {
return fmt.Errorf("%q must not be empty when %s is set", RedisAddress, SSOEnabled)
}
if len(c.SSO.SessionCookieName) == 0 {
return fmt.Errorf("%q must not be empty when %s is set", SSOSessionCookieName, SSOEnabled)
}
switch c.SSO.Mode {
case SSOModeProxy:
_, err := url.ParseRequestURI(c.SSO.ServerURL)
if err != nil {
return fmt.Errorf("%q must be a valid url: %w", SSOServerURL, err)
}
case SSOModeServer:
if len(c.SSO.Domain) == 0 {
return fmt.Errorf("%q cannot be empty", SSODomain)
}
_, err := url.ParseRequestURI(c.SSO.ServerDefaultRedirectURL)
if err != nil {
return fmt.Errorf("%q must be a valid url: %w", SSOServerDefaultRedirectURL, err)
}
default:
return fmt.Errorf("%q must be one of [%q, %q]", SSOModeFlag, SSOModeServer, SSOModeProxy)
}
}
return nil
}