refactor: generalize config to allow more providers; add azure

This commit is contained in:
Trong Huu Nguyen
2021-10-16 12:44:59 +02:00
parent e8e1fc7632
commit c1482d09e1
11 changed files with 273 additions and 108 deletions
+5 -6
View File
@@ -13,7 +13,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-redis/redis/v8"
"github.com/lestrrat-go/jwx/jwk"
"github.com/nais/liberator/pkg/conftools"
"github.com/nais/liberator/pkg/keygen"
log "github.com/sirupsen/logrus"
@@ -22,23 +21,23 @@ import (
"github.com/nais/wonderwall/pkg/cryptutil"
"github.com/nais/wonderwall/pkg/logging"
"github.com/nais/wonderwall/pkg/metrics"
"github.com/nais/wonderwall/pkg/provider"
"github.com/nais/wonderwall/pkg/router"
"github.com/nais/wonderwall/pkg/session"
)
var maskedConfig = []string{
config.IDPortenClientJWK,
config.OpenIDClientJWK,
config.EncryptionKey,
config.RedisPassword,
}
func run() error {
cfg := config.Initialize()
if err := conftools.Load(cfg); err != nil {
cfg, err := config.Initialize()
if err != nil {
return err
}
if err := cfg.FetchWellKnownConfig(); err != nil {
if err := conftools.Load(cfg); err != nil {
return err
}
+31
View File
@@ -0,0 +1,31 @@
package config
import (
"github.com/spf13/viper"
"github.com/nais/wonderwall/pkg/openid"
"github.com/nais/wonderwall/pkg/scopes"
)
type azure struct {
*BaseConfig
}
func azureFlags() {
viper.BindEnv(OpenIDClientID, "AZURE_APP_CLIENT_ID")
viper.BindEnv(OpenIDClientJWK, "AZURE_APP_JWK")
viper.BindEnv(OpenIDRedirectURI, "AZURE_APP_REDIRECT_URI")
viper.BindEnv(OpenIDWellKnownURL, "AZURE_APP_WELL_KNOWN_URL")
}
func (in *BaseConfig) Azure() openid.ClientConfiguration {
return &azure{
BaseConfig: in,
}
}
func (in *azure) GetScopes() scopes.Scopes {
return scopes.Defaults().
WithAzureScope(in.ClientID).
WithAdditional(in.Scopes...)
}
+55 -91
View File
@@ -6,113 +6,77 @@ import (
"github.com/nais/liberator/pkg/conftools"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/nais/wonderwall/pkg/token"
)
type Config struct {
BindAddress string `json:"bind-address"`
MetricsBindAddress string `json:"metrics-bind-address"`
UpstreamHost string `json:"upstream-host"`
EncryptionKey string `json:"encryption-key"`
IDPorten IDPorten `json:"idporten"`
LogFormat string `json:"log-format"`
LogLevel string `json:"log-level"`
Redis Redis `json:"redis"`
Ingress string `json:"ingress"`
ErrorRedirectURI string `json:"error-redirect-uri"`
AutoLogin bool `json:"auto-login"`
}
BindAddress string `json:"bind-address"`
LogFormat string `json:"log-format"`
LogLevel string `json:"log-level"`
MetricsBindAddress string `json:"metrics-bind-address"`
type Redis struct {
Address string `json:"address"`
Username string `json:"username"`
Password string `json:"password"`
TLS bool `json:"tls"`
}
AutoLogin bool `json:"auto-login"`
EncryptionKey string `json:"encryption-key"`
ErrorRedirectURI string `json:"error-redirect-uri"`
Ingress string `json:"ingress"`
SessionMaxLifetime time.Duration `json:"session-max-lifetime"`
UpstreamHost string `json:"upstream-host"`
type IDPorten struct {
ClientID string `json:"client-id"`
ClientJWK string `json:"client-jwk"`
RedirectURI string `json:"redirect-uri"`
WellKnownURL string `json:"well-known-url"`
WellKnown IDPortenWellKnown `json:"well-known"`
Locale IDPortenLocale `json:"locale"`
SecurityLevel IDPortenSecurityLevel `json:"security-level"`
PostLogoutRedirectURI string `json:"post-logout-redirect-uri"`
Scopes []string `json:"scopes"`
SessionMaxLifetime time.Duration `json:"session-max-lifetime"`
}
type IDPortenSecurityLevel struct {
Enabled bool `json:"enabled"`
Value string `json:"value"`
}
type IDPortenLocale struct {
Enabled bool `json:"enabled"`
Value string `json:"value"`
OpenID OpenID `json:"openid"`
Redis Redis `json:"redis"`
}
const (
BindAddress = "bind-address"
MetricsBindAddress = "metrics-bind-address"
UpstreamHost = "upstream-host"
LogFormat = "log-format"
LogLevel = "log-level"
EncryptionKey = "encryption-key"
RedisAddress = "redis.address"
RedisUsername = "redis.username"
RedisPassword = "redis.password"
RedisTLS = "redis.tls"
Ingress = "ingress"
ErrorRedirectURI = "error-redirect-uri"
AutoLogin = "auto-login"
IDPortenClientID = "idporten.client-id"
IDPortenClientJWK = "idporten.client-jwk"
IDPortenRedirectURI = "idporten.redirect-uri"
IDPortenWellKnownURL = "idporten.well-known-url"
IDPortenLocaleEnabled = "idporten.locale.enabled"
IDPortenLocaleValue = "idporten.locale.value"
IDPortenSecurityLevelEnabled = "idporten.security-level.enabled"
IDPortenSecurityLevelValue = "idporten.security-level.value"
IDPortenPostLogoutRedirectURI = "idporten.post-logout-redirect-uri"
IDPortenScopes = "idporten.scopes"
IDPortenSessionMaxLifetime = "idporten.session-max-lifetime"
BindAddress = "bind-address"
LogFormat = "log-format"
LogLevel = "log-level"
MetricsBindAddress = "metrics-bind-address"
AutoLogin = "auto-login"
EncryptionKey = "encryption-key"
ErrorRedirectURI = "error-redirect-uri"
Ingress = "ingress"
SessionMaxLifetime = "session-max-lifetime"
UpstreamHost = "upstream-host"
)
func bindNAIS() {
viper.BindEnv(IDPortenClientID, "IDPORTEN_CLIENT_ID")
viper.BindEnv(IDPortenClientJWK, "IDPORTEN_CLIENT_JWK")
viper.BindEnv(IDPortenRedirectURI, "IDPORTEN_REDIRECT_URI")
viper.BindEnv(IDPortenWellKnownURL, "IDPORTEN_WELL_KNOWN_URL")
}
func Initialize() *Config {
func Initialize() (*Config, error) {
conftools.Initialize("wonderwall")
bindNAIS()
flag.String(BindAddress, "127.0.0.1:8090", "Listen address for public connections.")
flag.String(LogFormat, "json", "Log format, either 'json' or 'text'.")
flag.String(LogLevel, "debug", "Logging verbosity level.")
flag.String(BindAddress, "127.0.0.1:8090", "Listen address for public connections.")
flag.String(MetricsBindAddress, "127.0.0.1:8091", "Listen address for metrics only.")
flag.String(UpstreamHost, "127.0.0.1:8080", "Address of upstream host.")
flag.String(EncryptionKey, "", "Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store.")
flag.String(RedisAddress, "", "Address of Redis. An empty value will use in-memory session storage.")
flag.String(RedisUsername, "", "Username for Redis.")
flag.String(RedisPassword, "", "Password for Redis.")
flag.Bool(RedisTLS, true, "Whether or not to use TLS for connecting to Redis.")
flag.String(Ingress, "/", "Ingress used to access the main application.")
flag.String(ErrorRedirectURI, "", "URI to redirect user to on errors for custom error handling.")
flag.Bool(AutoLogin, false, "Automatically redirect user to login if the user does not have a valid session for all proxied downstream requests.")
flag.String(EncryptionKey, "", "Base64 encoded 256-bit cookie encryption key; must be identical in instances that share session store.")
flag.String(ErrorRedirectURI, "", "URI to redirect user to on errors for custom error handling.")
flag.String(Ingress, "/", "Ingress used to access the main application.")
flag.Duration(SessionMaxLifetime, time.Hour, "Max lifetime for user sessions.")
flag.String(UpstreamHost, "127.0.0.1:8080", "Address of upstream host.")
flag.Bool(IDPortenSecurityLevelEnabled, true, "Toggle for setting the sceurity level (acr_values) parameter for authorization requests.")
flag.String(IDPortenSecurityLevelValue, "Level4", "Requested security level, either Level3 or Level4.")
flag.Bool(IDPortenLocaleEnabled, true, "Toggle for setting the locale parameter for authorization requests.")
flag.String(IDPortenLocaleValue, "nb", "Locale for OAuth2 consent screen.")
flag.String(IDPortenPostLogoutRedirectURI, "https://www.nav.no", "URI for redirecting the user after successful logout at IDPorten.")
flag.StringSlice(IDPortenScopes, []string{token.ScopeOpenID}, "List of scopes that should be used during the Auth Code flow.")
flag.Duration(IDPortenSessionMaxLifetime, time.Hour, "Max lifetime for user sessions.")
redisFlags()
openIDFlags()
return &Config{}
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)
}
return &Config{}, nil
}
+30
View File
@@ -0,0 +1,30 @@
package config
import (
"github.com/spf13/viper"
"github.com/nais/wonderwall/pkg/openid"
)
type idporten struct {
*BaseConfig
}
func idportenFlags() {
viper.BindEnv(OpenIDClientID, "IDPORTEN_CLIENT_ID")
viper.BindEnv(OpenIDClientJWK, "IDPORTEN_CLIENT_JWK")
viper.BindEnv(OpenIDRedirectURI, "IDPORTEN_REDIRECT_URI")
viper.BindEnv(OpenIDWellKnownURL, "IDPORTEN_WELL_KNOWN_URL")
viper.SetDefault(OpenIDPostLogoutRedirectURI, "https://www.nav.no")
viper.SetDefault(OpenIDACRValuesEnabled, true)
viper.SetDefault(OpenIDACRValuesValue, "Level4")
viper.SetDefault(OpenIDUILocalesEnabled, true)
viper.SetDefault(OpenIDUILocalesValue, "nb")
}
func (in *BaseConfig) IDPorten() openid.ClientConfiguration {
return &idporten{
BaseConfig: in,
}
}
+107
View File
@@ -0,0 +1,107 @@
package config
import (
"github.com/lestrrat-go/jwx/jwk"
flag "github.com/spf13/pflag"
"github.com/nais/wonderwall/pkg/openid"
"github.com/nais/wonderwall/pkg/scopes"
)
const (
OpenIDProvider = "openid.provider"
OpenIDClientID = "openid.client-id"
OpenIDClientJWK = "openid.client-jwk"
OpenIDPostLogoutRedirectURI = "openid.post-logout-redirect-uri"
OpenIDRedirectURI = "openid.redirect-uri"
OpenIDScopes = "openid.scopes"
OpenIDWellKnownURL = "openid.well-known-url"
OpenIDACRValuesEnabled = "openid.acr-values.enabled"
OpenIDACRValuesValue = "openid.acr-values.value"
OpenIDUILocalesEnabled = "openid.ui-locales.enabled"
OpenIDUILocalesValue = "openid.ui-locales.value"
)
type OpenID struct {
Provider Provider `json:"provider"`
ClientID string `json:"client-id"`
ClientJWK string `json:"client-jwk"`
PostLogoutRedirectURI string `json:"post-logout-redirect-uri"`
RedirectURI string `json:"redirect-uri"`
Scopes []string `json:"scopes"`
WellKnownURL string `json:"well-known-url"`
ACRValues openid.OptionalConfiguration `json:"acr-values"`
UILocales openid.OptionalConfiguration `json:"ui-locales"`
}
type Provider string
const (
ProviderAzure Provider = "azure"
ProviderIDPorten Provider = "idporten"
ProviderOpenID Provider = "openid"
)
type BaseConfig struct {
OpenID
clientJwk jwk.Key
}
func (in *BaseConfig) GetRedirectURI() string {
return in.RedirectURI
}
func (in *BaseConfig) GetClientID() string {
return in.ClientID
}
func (in *BaseConfig) GetClientJWK() jwk.Key {
return in.clientJwk
}
func (in *BaseConfig) GetPostLogoutRedirectURI() string {
return in.PostLogoutRedirectURI
}
func (in *BaseConfig) GetScopes() scopes.Scopes {
return scopes.Defaults().WithAdditional(in.Scopes...)
}
func (in *BaseConfig) GetACRValues() openid.OptionalConfiguration {
return openid.OptionalConfiguration{
Enabled: in.ACRValues.Enabled,
Value: in.ACRValues.Value,
}
}
func (in *BaseConfig) GetUILocales() openid.OptionalConfiguration {
return openid.OptionalConfiguration{
Enabled: in.UILocales.Enabled,
Value: in.UILocales.Value,
}
}
func (in *BaseConfig) GetWellKnownURL() string {
return in.WellKnownURL
}
func (c *Config) NewBaseConfig(clientJwk jwk.Key) *BaseConfig {
return &BaseConfig{
OpenID: c.OpenID,
clientJwk: clientJwk,
}
}
func openIDFlags() {
flag.String(OpenIDClientID, "", "Client ID for the OpenID client.")
flag.String(OpenIDClientJWK, "", "JWK containing the private key for the OpenID client in string format.")
flag.String(OpenIDPostLogoutRedirectURI, "", "URI for redirecting the user after successful logout at the Identity Provider.")
flag.String(OpenIDRedirectURI, "", "Redirect URI for the OpenID client that should be used in authorization requests.")
flag.StringSlice(OpenIDScopes, []string{}, "List of additional scopes (other than 'openid') that should be used during the login flow.")
flag.String(OpenIDWellKnownURL, "", "URI to the well-known OpenID Configuration metadata document.")
flag.Bool(OpenIDACRValuesEnabled, false, "Toggle for setting the security level (acr_values) parameter for authorization requests.")
flag.String(OpenIDACRValuesValue, "", "Space separated string that configures the requested acr_values.")
flag.Bool(OpenIDUILocalesEnabled, false, "Toggle for setting the UI locale parameter for authorization requests.")
flag.String(OpenIDUILocalesValue, "", "Space-separated string that configures the default locales for OAuth2 consent screen.")
}
+26
View File
@@ -0,0 +1,26 @@
package config
import (
flag "github.com/spf13/pflag"
)
const (
RedisAddress = "redis.address"
RedisPassword = "redis.password"
RedisTLS = "redis.tls"
RedisUsername = "redis.username"
)
type Redis struct {
Address string `json:"address"`
Username string `json:"username"`
Password string `json:"password"`
TLS bool `json:"tls"`
}
func redisFlags() {
flag.String(RedisAddress, "", "Address of Redis. An empty value will use in-memory session storage.")
flag.String(RedisPassword, "", "Password for Redis.")
flag.Bool(RedisTLS, true, "Whether or not to use TLS for connecting to Redis.")
flag.String(RedisUsername, "", "Username for Redis.")
}
+2 -2
View File
@@ -46,8 +46,8 @@ func (in Supported) Contains(value string) bool {
return false
}
func FetchWellKnownConfig(wellKnownURI string) (*Configuration, error) {
response, err := http.Get(wellKnownURI)
func FetchWellKnownConfig(config ClientConfiguration) (*Configuration, error) {
response, err := http.Get(config.GetWellKnownURL())
if err != nil {
return nil, fmt.Errorf("fetching well known configuration: %w", err)
}
+3 -3
View File
@@ -48,9 +48,9 @@ func NewProvider(cfg *config.Config) (Provider, error) {
baseConfig := cfg.NewBaseConfig(clientJwk)
var clientConfig openid.ClientConfiguration
switch cfg.OpenID.Provider {
case "idporten":
case config.ProviderIDPorten:
clientConfig = baseConfig.IDPorten()
case "azure":
case config.ProviderAzure:
clientConfig = baseConfig.Azure()
case "":
return nil, fmt.Errorf("missing required config %s", config.OpenIDProvider)
@@ -70,7 +70,7 @@ func NewProvider(cfg *config.Config) (Provider, error) {
return nil, fmt.Errorf("missing required config %s", config.OpenIDRedirectURI)
}
configuration, err := openid.FetchWellKnownConfig(clientConfig.GetWellKnownURL())
configuration, err := openid.FetchWellKnownConfig(clientConfig)
if err != nil {
return nil, fmt.Errorf("fetching well known config: %w", err)
}
+12 -4
View File
@@ -1,13 +1,16 @@
package router
import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httplog"
"github.com/nais/wonderwall/pkg/request"
"html/template"
"net/http"
"net/url"
"strconv"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httplog"
log "github.com/sirupsen/logrus"
"github.com/nais/wonderwall/pkg/request"
)
type ErrorPage struct {
@@ -32,7 +35,12 @@ func (h *Handler) respondError(w http.ResponseWriter, r *http.Request, statusCod
func (h *Handler) defaultErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int) {
w.WriteHeader(statusCode)
t, _ := template.ParseFiles("templates/error.html")
t, err := template.ParseFiles("templates/error.html")
if err != nil {
log.Errorf("parsing error template: %+v", err)
return
}
loginCookie, err := h.getLoginCookie(r)
if err != nil {
loginCookie = nil
+1 -1
View File
@@ -22,7 +22,7 @@ func (h *Handler) Default(w http.ResponseWriter, r *http.Request) {
}
director := func(upstreamRequest *http.Request) {
modifyRequest(upstreamRequest, r, h.UpstreamHost)
modifyRequest(upstreamRequest, r, h.Config.UpstreamHost)
if isAuthenticated {
withAuthentication(upstreamRequest, sessionData)
+1 -1
View File
@@ -55,7 +55,7 @@ func (h *Handler) getSessionFromCookie(w http.ResponseWriter, r *http.Request) (
}
func (h *Handler) getSessionLifetime(accessToken string) (time.Duration, error) {
defaultSessionLifetime := h.Config.IDPorten.SessionMaxLifetime
defaultSessionLifetime := h.Config.SessionMaxLifetime
tok, err := jwt.Parse([]byte(accessToken))
if err != nil {