mirror of
https://github.com/nais/wonderwall.git
synced 2026-08-19 02:56:15 +00:00
refactor: generalize config to allow more providers; add azure
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user