Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Root URL server config property to replace auth.publicurl #2633

Merged
merged 8 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ The following options can be configured on the server:
internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode.
loggerformat text Log format (text, json)
strictmode true When set, insecure settings are forbidden.
url Public facing URL of the server (required). Must be HTTPS when strictmode is set. Superseeds 'auth.publicurl', which is deprecated.
verbosity info Log level (trace, debug, info, warn, error)
tls.certfile PEM file containing the certificate for the server (also used as client certificate).
tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded.
Expand All @@ -195,7 +196,6 @@ The following options can be configured on the server:
auth.accesstokenlifespan 60 defines how long (in seconds) an access token is valid. Uses default in strict mode.
auth.clockskew 5000 allowed JWT Clock skew in milliseconds
auth.contractvalidators [irma,uzi,dummy,employeeid] sets the different contract validators to use
auth.publicurl public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy.
auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client
auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes.
auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'.
Expand Down Expand Up @@ -288,7 +288,7 @@ See :ref:`getting started <configure-node>` on how to set this up correctly.

The incorporated `IRMA server <https://irma.app/docs/irma-server/#production-mode>`_ is automatically changed to production mode.
In fact, running in strict mode is the only way to enable IRMA's production mode.
In addition, it requires ``auth.irma.schememanager=pbdf`` and the ``auth.publicurl`` where the IRMA client can reach the server must be set.
In addition, it requires ``auth.irma.schememanager=pbdf`` and the ``url`` where the IRMA client can reach the server must be set.

As a general safety precaution ``auth.contractvalidators`` ignores the ``dummy`` option if configured,
requesting an access token from another node on ``/n2n/auth/v1/accesstoken`` does not return any error details,
Expand Down
9 changes: 3 additions & 6 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,14 @@ func (auth *Auth) Configure(config core.ServerConfig) error {
return errors.New("in strictmode the only valid irma-scheme-manager is 'pbdf'")
}

if auth.config.PublicURL == "" {
return errors.New("invalid auth.publicurl: must provide url")
}
var err error
auth.publicURL, err = core.ParsePublicURL(auth.config.PublicURL, config.Strictmode)
auth.publicURL, err = config.ServerURL()
if err != nil {
return fmt.Errorf("invalid auth.publicurl: %w", err)
return err
}

auth.contractNotary = notary.NewNotary(notary.Config{
PublicURL: auth.config.PublicURL,
PublicURL: auth.publicURL.String(),
IrmaConfigPath: path.Join(config.Datadir, "irma"),
IrmaSchemeManager: auth.config.Irma.SchemeManager,
AutoUpdateIrmaSchemas: auth.config.Irma.AutoUpdateSchemas,
Expand Down
52 changes: 2 additions & 50 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ import (

func TestAuth_Configure(t *testing.T) {
tlsServerConfig := *core.NewServerConfig()
tlsServerConfig.URL = "https://nuts.nl"
tlsServerConfig.LegacyTLS.TrustStoreFile = "test/certs/ca.pem"
tlsServerConfig.LegacyTLS.CertKeyFile = "test/certs/example.com.key"
tlsServerConfig.LegacyTLS.CertFile = "test/certs/example.com.pem"

t.Run("ok", func(t *testing.T) {
config := DefaultConfig()
config.ContractValidators = []string{"uzi"}
config.PublicURL = "https://nuts.nl"
ctrl := gomock.NewController(t)
pkiMock := pki.NewMockProvider(ctrl)
pkiMock.EXPECT().AddTruststore(gomock.Any()) // uzi
Expand All @@ -53,28 +53,6 @@ func TestAuth_Configure(t *testing.T) {
require.NoError(t, i.Configure(tlsServerConfig))
})

t.Run("publicUrl", func(t *testing.T) {
t.Run("error - missing", func(t *testing.T) {
authCfg := TestConfig()
authCfg.PublicURL = ""
authCfg.Irma.SchemeManager = "pbdf"
i := testInstance(t, authCfg)
cfg := core.NewServerConfig()
cfg.Strictmode = true
cfg.TLS.CertFile = "certificate.pem"
assert.EqualError(t, i.Configure(*cfg), "invalid auth.publicurl: must provide url")
})
t.Run("error - invalid URL (must be hostname, not IP)", func(t *testing.T) {
authCfg := TestConfig()
authCfg.Irma.SchemeManager = "pbdf"
authCfg.PublicURL = "https://127.0.0.1"
i := testInstance(t, authCfg)
cfg := core.NewServerConfig()
cfg.Strictmode = true
assert.EqualError(t, i.Configure(*cfg), "invalid auth.publicurl: hostname is IP")
})
})

t.Run("error - IRMA config failure", func(t *testing.T) {
authCfg := TestConfig()
authCfg.Irma.SchemeManager = "non-existing"
Expand Down Expand Up @@ -106,6 +84,7 @@ func TestAuth_Configure(t *testing.T) {
i := testInstance(t, authCfg)
serverConfig := core.NewServerConfig()
serverConfig.Strictmode = true
serverConfig.URL = "https://nuts.nl"
err := i.Configure(*serverConfig)
assert.EqualError(t, err, "in strictmode TLS must be enabled")
})
Expand All @@ -118,33 +97,6 @@ func TestAuth_Configure(t *testing.T) {
err := i.Configure(tlsServerConfig)
assert.ErrorIs(t, err, assert.AnError)
})
t.Run("public url", func(t *testing.T) {
type test struct {
strict bool
pURL string
errStr string
}
tt := []test{
{true, "", "invalid auth.publicurl: must provide url"},
{true, ":invalid", "invalid auth.publicurl: parse \":invalid\": missing protocol scheme"},
{true, "https://127.0.0.1", "invalid auth.publicurl: hostname is IP"},
{true, "https://example.com", "invalid auth.publicurl: hostname is RFC2606 reserved"},
{true, "https://localhost", "invalid auth.publicurl: hostname is RFC2606 reserved"},
{true, "http://nuts.nl", "invalid auth.publicurl: scheme must be https"},

{false, "", "invalid auth.publicurl: must provide url"},
{false, ":invalid", "invalid auth.publicurl: parse \":invalid\": missing protocol scheme"},
{false, "something://nuts.nl", "invalid auth.publicurl: scheme must be http or https"},
}
authCfg := TestConfig()
cfg := core.NewServerConfig()
for _, test := range tt {
authCfg.PublicURL = test.pURL
i := testInstance(t, authCfg)
cfg.Strictmode = test.strict
assert.EqualError(t, i.Configure(*cfg), test.errStr, "test config: url=%s; strict=%s", test.pURL, test.strict)
}
})
}

func TestAuth_Name(t *testing.T) {
Expand Down
4 changes: 0 additions & 4 deletions auth/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ import (
"github.com/spf13/pflag"
)

// ConfPublicURL is the config key for the public URL the http/irma server can be discovered
const ConfPublicURL = "auth.publicurl"

// ConfClockSkew is the config key for allowed JWT clockskew (deviance of iat, exp) in milliseconds
const ConfClockSkew = "auth.clockskew"

Expand Down Expand Up @@ -55,7 +52,6 @@ func FlagSet() *pflag.FlagSet {

defs := auth.DefaultConfig()
flags.String(ConfIrmaSchemeManager, defs.Irma.SchemeManager, "IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'.")
flags.String(ConfPublicURL, defs.PublicURL, "public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy.")
flags.Bool(ConfAutoUpdateIrmaSchemas, defs.Irma.AutoUpdateSchemas, "set if you want automatically update the IRMA schemas every 60 minutes.")
flags.Int(ConfHTTPTimeout, defs.HTTPTimeout, "HTTP timeout (in seconds) used by the Auth API HTTP client")
flags.Int(ConfClockSkew, defs.ClockSkew, "allowed JWT Clock skew in milliseconds")
Expand Down
1 change: 0 additions & 1 deletion auth/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ func TestFlagSet(t *testing.T) {
ConfAutoUpdateIrmaSchemas,
ConfIrmaSchemeManager,
ConfPresentationExchangeMappingFile,
ConfPublicURL,
ConfV2APIEnabled,
}, keys)
}
Expand Down
1 change: 0 additions & 1 deletion auth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
type Config struct {
Irma IrmaConfig `koanf:"irma"`
HTTPTimeout int `koanf:"http.timeout"`
PublicURL string `koanf:"publicurl"`
ClockSkew int `koanf:"clockskew"`
ContractValidators []string `koanf:"contractvalidators"`
AccessTokenLifeSpan int `koanf:"accesstokenlifespan"`
Expand Down
1 change: 0 additions & 1 deletion auth/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import (
func TestConfig() Config {
config := DefaultConfig()
config.ContractValidators = []string{"dummy"}
config.PublicURL = "https://nuts.nl"
return config
}

Expand Down
37 changes: 36 additions & 1 deletion core/server_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import (
"bytes"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"github.com/knadh/koanf"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/posflag"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"net/url"
"reflect"
"strings"
)
Expand Down Expand Up @@ -58,7 +60,10 @@ type ServerConfig struct {
Datadir string `koanf:"datadir"`
TLS TLSConfig `koanf:"tls"`
LegacyTLS *NetworkTLSConfig `koanf:"network"`
configMap *koanf.Koanf
LegacyAuth LegacyAuthConfig `koanf:"auth"`
// URL contains the base URL for public-facing HTTP services.
URL string `koanf:"url"`
configMap *koanf.Koanf
}

// TLSConfig specifies how TLS should be configured for connections.
Expand Down Expand Up @@ -229,6 +234,12 @@ func (ngc *ServerConfig) Load(flags *pflag.FlagSet) (err error) {
return nil
}

// LegacyAuthConfig is here since we're moving auth.publicurl to the "url" root property.
// This way we can validate the property at the correct (future) place.
type LegacyAuthConfig struct {
PublicURL string `koanf:"publicurl"`
}

// resolveConfigFilePath resolves the path of the config file using the following sources:
// 1. commandline params (using the given flags)
// 2. environment vars,
Expand Down Expand Up @@ -261,6 +272,7 @@ func FlagSet() *pflag.FlagSet {
flagSet.Bool("strictmode", true, "When set, insecure settings are forbidden.")
flagSet.Bool("internalratelimiter", true, "When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode.")
flagSet.String("datadir", "./data", "Directory where the node stores its files.")
flagSet.String("url", "", "Public facing URL of the server (required). Must be HTTPS when strictmode is set. Superseeds 'auth.publicurl', which is deprecated.")
woutslakhorst marked this conversation as resolved.
Show resolved Hide resolved
flagSet.String("tls.certfile", "", "PEM file containing the certificate for the server (also used as client certificate).")
flagSet.String("tls.certkeyfile", "", "PEM file containing the private key of the server certificate.")
flagSet.String("tls.truststorefile", "truststore.pem", "PEM file containing the trusted CA certificates for authenticating remote servers.")
Expand All @@ -278,12 +290,14 @@ func FlagSet() *pflag.FlagSet {
"Required when 'network.enabletls' is 'true'.")
flagSet.String("network.truststorefile", "", "Deprecated: use 'tls.truststorefile'. PEM file containing the trusted CA certificates for authenticating remote gRPC servers.")
flagSet.Int("network.maxcrlvaliditydays", 0, "Deprecated: use 'tls.crl.maxvaliditydays'. The number of days a CRL can be outdated, after that it will hard-fail.")
flagSet.String("auth.publicurl", "", "Public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy.")
woutslakhorst marked this conversation as resolved.
Show resolved Hide resolved

flagSet.MarkDeprecated("tls.crl.maxvaliditydays", "CRLs can no longer be accepted after the time in NextUpdate has past")
flagSet.MarkDeprecated("network.certfile", "use 'tls.certfile' instead")
flagSet.MarkDeprecated("network.certkeyfile", "use 'tls.certkeyfile' instead")
flagSet.MarkDeprecated("network.truststorefile", "use 'tls.truststorefile' instead")
flagSet.MarkDeprecated("network.maxcrlvaliditydays", "use 'tls.crl.maxvaliditydays' instead")
flagSet.MarkDeprecated("auth.publicurl", "use 'url' instead")

return flagSet
}
Expand Down Expand Up @@ -315,6 +329,27 @@ func (ngc *ServerConfig) InjectIntoEngine(e Injectable) error {
return unmarshalRecursive([]string{strings.ToLower(e.Name())}, e.Config(), ngc.configMap)
}

// ServerURL returns the parsed URL of the server
func (ngc *ServerConfig) ServerURL() (*url.URL, error) {
// Validate server URL
if ngc.LegacyAuth.PublicURL != "" {
coreLogger.Warn("Deprecated: use 'url' instead of 'auth.publicurl', which will be removed in the removed")
}
serverURL := ngc.LegacyAuth.PublicURL
if ngc.URL != "" {
// give precedence over new property
serverURL = ngc.URL
}
if serverURL == "" {
return nil, errors.New("'url' must be configured")
}
result, err := ParsePublicURL(serverURL, ngc.Strictmode)
if err != nil {
return nil, fmt.Errorf("invalid 'url': %w", err)
}
return result, nil
}

func elemType(ty reflect.Type) (reflect.Type, bool) {
isPtr := ty.Kind() == reflect.Ptr

Expand Down
43 changes: 43 additions & 0 deletions core/server_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,46 @@ func TestTLSConfig_LoadTrustStore(t *testing.T) {
assert.EqualError(t, err, "unable to read trust store (file=test/non-existent.pem): open test/non-existent.pem: no such file or directory")
})
}

func TestServerConfig_ServerURL(t *testing.T) {
t.Run("url", func(t *testing.T) {
cfg := ServerConfig{LegacyAuth: LegacyAuthConfig{PublicURL: "https://example.com"}}
actual, err := cfg.ServerURL()
assert.NoError(t, err)
assert.Equal(t, "https://example.com", actual.String())
})
t.Run("deprecated auth.publicurl", func(t *testing.T) {
cfg := ServerConfig{URL: "https://example.com"}
actual, err := cfg.ServerURL()
assert.NoError(t, err)
assert.Equal(t, "https://example.com", actual.String())
})
t.Run("precedence to url", func(t *testing.T) {
cfg := ServerConfig{URL: "https://nuts.nl", LegacyAuth: LegacyAuthConfig{PublicURL: "https://example.com"}}
actual, err := cfg.ServerURL()
assert.NoError(t, err)
assert.Equal(t, "https://nuts.nl", actual.String())
})
t.Run("public URL can be http when not in strict mode", func(t *testing.T) {
cfg := ServerConfig{URL: "http://nuts.nl"}
actual, err := cfg.ServerURL()
assert.NoError(t, err)
assert.Equal(t, "http://nuts.nl", actual.String())
})
t.Run("url is required", func(t *testing.T) {
cfg := ServerConfig{}
_, err := cfg.ServerURL()
assert.EqualError(t, err, "'url' must be configured")
})
t.Run("url is invalid", func(t *testing.T) {
cfg := ServerConfig{URL: "nuts.nl"}
_, err := cfg.ServerURL()
assert.EqualError(t, err, "invalid 'url': url must contain scheme and host")
})
t.Run("deprecated auth.publicurl is still supported", func(t *testing.T) {
cfg := ServerConfig{LegacyAuth: LegacyAuthConfig{PublicURL: "https://example.com"}}
actual, err := cfg.ServerURL()
assert.NoError(t, err)
assert.Equal(t, "https://example.com", actual.String())
})
}
Loading
Loading