Skip to content

Commit

Permalink
Merge pull request #635 from teamhanko/feat-simplify-config
Browse files Browse the repository at this point in the history
Feat simplify config
  • Loading branch information
like-a-bause authored Apr 12, 2023
2 parents 552077d + 79e5268 commit 1def6cf
Show file tree
Hide file tree
Showing 18 changed files with 110 additions and 102 deletions.
38 changes: 27 additions & 11 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ To get the Hanko backend up and running you need to:
4. [Run and configure an SMTP server](#run-and-configure-an-smtp-server)
5. [Configure JSON Web Key Set generation](#configure-json-web-key-set-generation)
6. [Configure WebAuthn](#configure-webauthn)
8. [Configure CORS](#configure-cors)
9. [Start the backend](#run-the-backend)
7. [Configure CORS](#configure-cors)
8. [Start the backend](#run-the-backend)

### Run a database

Expand Down Expand Up @@ -237,7 +237,8 @@ webauthn:
relying_party:
id: "localhost"
display_name: "Hanko Authentication Service"
origin: "http://localhost"
origins:
- "http://localhost"
```

so no further configuration changes need to be made to your configuration file.
Expand All @@ -252,7 +253,8 @@ webauthn:
relying_party:
id: "example.com"
display_name: "Example Project"
origin: "https://example.com"
origins:
- "https://example.com"
```

If the login should be available at `https://login.example.com` instead, then the WebAuthn config would look like this:
Expand All @@ -262,7 +264,8 @@ webauthn:
relying_party:
id: "login.example.com"
display_name: "Example Project"
origin: "https://login.example.com"
origins:
- "https://login.example.com"
```

Given the above scenario, you still may want to bind your users WebAuthn credentials to `example.com` if you plan to
Expand All @@ -275,11 +278,10 @@ webauthn:
relying_party:
id: "example.com"
display_name: "Example Project"
origin: "https://login.example.com"
origins:
- "https://login.example.com"
```

> **Note**: Currently, only a single origin is supported. We plan to add support for a list of origins at some point.

### Configure CORS

Because the backend and your application(s) consuming backend API most likely have different origins, i.e.
Expand All @@ -290,12 +292,26 @@ Cross-Origin Resource Sharing (CORS) and specify your application(s) as allowed
server:
public:
cors:
enabled: true
allow_credentials: true
allow_origins:
- "https://example.com"
- https://example.com
```

When you include a wildcard `*` origin you need to set `unsafe_wildcard_origin_allowed: true`:

```yaml
server:
public:
cors:
allow_origins:
- "*"
unsafe_wildcard_origin_allowed: true
```

Wildcard `*` origins can lead to cross-site attacks and when you include a `*` wildcard origin,
we want to make sure, that you understand what you are doing, hence this flag.

> **Note** In most cases, the `allow_origins` list here should contain the same entries as the `webauthn.relying_party.origins` list. Only when you have an Android app you will have an extra entry (`android:apk-key-hash:...`) in the `webauthn.relying_party.origins` list.

### Start the backend

The Hanko backend consists of a public and an administrative API (currently providing user management
Expand Down
55 changes: 32 additions & 23 deletions backend/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/sethvargo/go-limiter/httplimit"
"log"
"strings"
"time"
Expand Down Expand Up @@ -76,14 +75,6 @@ func DefaultConfig() *Config {
Server: Server{
Public: ServerSettings{
Address: ":8000",
Cors: Cors{
ExposeHeaders: []string{
httplimit.HeaderRateLimitLimit,
httplimit.HeaderRateLimitRemaining,
httplimit.HeaderRateLimitReset,
httplimit.HeaderRetryAfter,
},
},
},
Admin: ServerSettings{
Address: ":8001",
Expand All @@ -93,7 +84,7 @@ func DefaultConfig() *Config {
RelyingParty: RelyingParty{
Id: "localhost",
DisplayName: "Hanko Authentication Service",
Origins: []string{"http://localhost"},
Origins: []string{"http://localhost:8888"},
},
Timeout: 60000,
},
Expand Down Expand Up @@ -239,19 +230,39 @@ type ServerSettings struct {
}

type Cors struct {
Enabled bool `yaml:"enabled" json:"enabled" koanf:"enabled"`
AllowCredentials bool `yaml:"allow_credentials" json:"allow_credentials" koanf:"allow_credentials" split_words:"true"`
AllowOrigins []string `yaml:"allow_origins" json:"allow_origins" koanf:"allow_origins" split_words:"true"`
AllowMethods []string `yaml:"allow_methods" json:"allow_methods" koanf:"allow_methods" split_words:"true"`
AllowHeaders []string `yaml:"allow_headers" json:"allow_headers" koanf:"allow_headers" split_words:"true"`
ExposeHeaders []string `yaml:"expose_headers" json:"expose_headers" koanf:"expose_headers" split_words:"true"`
MaxAge int `yaml:"max_age" json:"max_age" koanf:"max_age" split_words:"true"`
// AllowOrigins determines the value of the Access-Control-Allow-Origin
// response header. This header defines a list of origins that may access the
// resource. The wildcard characters '*' and '?' are supported and are
// converted to regex fragments '.*' and '.' accordingly.
AllowOrigins []string `yaml:"allow_origins" json:"allow_origins" koanf:"allow_origins" split_words:"true"`

// UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials
// flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header.
//
// This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties)
// attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject.
//
// Optional. Default value is false.
UnsafeWildcardOriginAllowed bool `yaml:"unsafe_wildcard_origin_allowed" json:"unsafe_wildcard_origin_allowed" koanf:"unsafe_wildcard_origin_allowed" split_words:"true"`
}

func (cors *Cors) Validate() error {
for _, origin := range cors.AllowOrigins {
if origin == "*" && !cors.UnsafeWildcardOriginAllowed {
return fmt.Errorf("found wildcard '*' origin in server.public.cors.allow_origins, if this is intentional set server.public.cors.unsafe_wildcard_origin_allowed to true")
}
}

return nil
}

func (s *ServerSettings) Validate() error {
if len(strings.TrimSpace(s.Address)) == 0 {
return errors.New("field Address must not be empty")
}
if err := s.Cors.Validate(); err != nil {
return err
}
return nil
}

Expand All @@ -268,12 +279,10 @@ func (r *WebauthnSettings) Validate() error {

// RelyingParty webauthn settings for your application using hanko.
type RelyingParty struct {
Id string `yaml:"id" json:"id" koanf:"id"`
DisplayName string `yaml:"display_name" json:"display_name" koanf:"display_name" split_words:"true"`
Icon string `yaml:"icon" json:"icon" koanf:"icon"`
// Deprecated: Use Origins instead
Origin string `yaml:"origin" json:"origin" koanf:"origin"`
Origins []string `yaml:"origins" json:"origins" koanf:"origins"`
Id string `yaml:"id" json:"id" koanf:"id"`
DisplayName string `yaml:"display_name" json:"display_name" koanf:"display_name" split_words:"true"`
Icon string `yaml:"icon" json:"icon" koanf:"icon"`
Origins []string `yaml:"origins" json:"origins" koanf:"origins"`
}

// SMTP Server Settings for sending passcodes
Expand Down
4 changes: 2 additions & 2 deletions backend/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ func TestEnvironmentVariables(t *testing.T) {
err := os.Setenv("PASSCODE_SMTP_HOST", "valueFromEnvVars")
require.NoError(t, err)

err = os.Setenv("SERVER_PUBLIC_CORS_ALLOW_METHODS", "GET,PUT,POST,DELETE")
err = os.Setenv("WEBAUTHN_RELYING_PARTY_ORIGINS", "https://hanko.io,https://auth.hanko.io")
require.NoError(t, err)

configPath := "./minimal-config.yaml"
cfg, err := Load(&configPath)
require.NoError(t, err)

assert.Equal(t, "valueFromEnvVars", cfg.Passcode.Smtp.Host)
assert.True(t, reflect.DeepEqual([]string{"GET", "PUT", "POST", "DELETE"}, cfg.Server.Public.Cors.AllowMethods))
assert.True(t, reflect.DeepEqual([]string{"https://hanko.io", "https://auth.hanko.io"}, cfg.Webauthn.RelyingParty.Origins))
}
42 changes: 13 additions & 29 deletions backend/docs/Config.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ Environment variables have higher precedence than configuration via file (i.e. i
given in the file - multivalued options, like arrays, are also _not_ merged but overwritten entirely).

The schema for the configuration file is given below. To set equivalent environment variables, join keys by `_`
(underscore) and uppercase the keys, i.e. for `server.public.cors.allow_methods`
(underscore) and uppercase the keys, i.e. for `server.webauthn.relying_party.origins`
use:

```shell
export SERVER_PUBLIC_CORS_ALLOW_METHODS="GET,PUT,POST,DELETE"
export SERVER_WEBAUTHN_RELYING_PARTY_ORIGINS="https://hanko.io,android:apk-key-hash:nLSu7w..."
```


Expand All @@ -35,23 +35,19 @@ server:
# Cross Origin Resource Sharing for public endpoints.
#
cors:
## enabled ##
## allow_origins ##
#
# Sets whether cors is enabled or not.
# A list of allowed origins that may access the public endpoints.
#
# Default value: false
#
enabled: false
allow_credentials: false
allow_origins:
- "*"
allow_methods:
- ""
allow_headers:
- ""
expose_headers:
- ""
max_age: 0
- "https://hanko.io"
- "https://example.com"
## unsafe_wildcard_origin_allowed ##
#
# If allow_origins contains a wildcard '*' origin, this flag must explicitly be set to 'true'.
# Using wildcard '*' origins is insecure and potentially leads to cross-origin attacks.
#
unsafe_wildcard_origin_allowed: false
## admin ##
#
# Configuration for the admin API.
Expand Down Expand Up @@ -259,23 +255,11 @@ webauthn:
# - Acme, Inc.
#
display_name: ""
## origin ##
#
# DEPRECATED: use "origins" instead
#
# The origin for which WebAuthn credentials will be accepted by the server. Must include the protocol and can only be the effective domain,
# or a registrable domain suffix of the effective domain, as specified in the id. Except for localhost, the protocol must always be https for WebAuthn to work.
#
# Example:
# - http://localhost
# - https://example.com
# - https://subdomain.example.com
#
origin: "http://localhost"
## origins ##
#
# A list of origins for which WebAuthn credentials will be accepted by the server. Must include the protocol and can only be the effective domain,
# or a registrable domain suffix of the effective domain, as specified in the id. Except for localhost, the protocol must always be https for WebAuthn to work.
# Ip Addresses will not work.
#
# For an Android app the origin must be the base64 url encoded SHA256 fingerprint of the signing certificate.
#
Expand Down
2 changes: 1 addition & 1 deletion backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/knadh/koanf v1.5.0
github.com/labstack/echo-contrib v0.14.0
github.com/labstack/echo-jwt/v4 v4.1.0
github.com/labstack/echo/v4 v4.10.1
github.com/labstack/echo/v4 v4.10.2
github.com/lestrrat-go/jwx/v2 v2.0.9
github.com/lib/pq v1.10.7
github.com/nicksnyder/go-i18n/v2 v2.2.1
Expand Down
4 changes: 2 additions & 2 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,8 @@ github.com/labstack/echo-contrib v0.14.0 h1:tVHJjhqOcB183bzAeNDVwKgf1GWRAM6k9PvI
github.com/labstack/echo-contrib v0.14.0/go.mod h1:0tmJZUHWLU7zGvMoxZwotRxHgUqBfW37T6bHg17SgAw=
github.com/labstack/echo-jwt/v4 v4.1.0 h1:eYGBxauPkyzBM78KJbR5OSz5uhKMDkhJZhTTIuoH6Pg=
github.com/labstack/echo-jwt/v4 v4.1.0/go.mod h1:DHSSaL6cTgczdPXjf8qrTHRbrau2flcddV7CPMs2U/Y=
github.com/labstack/echo/v4 v4.10.1 h1:rB+D8In9PWjsp1OpHaqK+t04nQv/SBD1IoIcXCg0lpY=
github.com/labstack/echo/v4 v4.10.1/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4=
Expand Down
1 change: 0 additions & 1 deletion backend/handler/passcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ func (h *PasscodeHandler) Finish(c echo.Context) error {

if h.cfg.Session.EnableAuthTokenHeader {
c.Response().Header().Set("X-Auth-Token", token)
c.Response().Header().Set("Access-Control-Expose-Headers", "X-Auth-Token")
}

err = h.auditLogger.Create(c, models.AuditLogPasscodeLoginFinalSucceeded, user, nil)
Expand Down
1 change: 0 additions & 1 deletion backend/handler/password.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ func (h *PasswordHandler) Login(c echo.Context) error {

if h.cfg.Session.EnableAuthTokenHeader {
c.Response().Header().Set("X-Auth-Token", token)
c.Response().Header().Set("Access-Control-Expose-Headers", "X-Auth-Token")
}

err = h.auditLogger.Create(c, models.AuditLogPasswordLoginSucceeded, user, nil)
Expand Down
26 changes: 17 additions & 9 deletions backend/handler/public_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/labstack/echo-contrib/prometheus"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/sethvargo/go-limiter/httplimit"
"github.com/teamhanko/hanko/backend/audit_log"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/crypto/jwk"
Expand All @@ -23,16 +24,23 @@ func NewPublicRouter(cfg *config.Config, persister persistence.Persister, promet
e.Use(middleware.RequestID())
e.Use(hankoMiddleware.GetLoggerMiddleware())

if cfg.Server.Public.Cors.Enabled {
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: cfg.Server.Public.Cors.AllowOrigins,
AllowMethods: cfg.Server.Public.Cors.AllowMethods,
AllowHeaders: cfg.Server.Public.Cors.AllowHeaders,
ExposeHeaders: cfg.Server.Public.Cors.ExposeHeaders,
AllowCredentials: cfg.Server.Public.Cors.AllowCredentials,
MaxAge: cfg.Server.Public.Cors.MaxAge,
}))
exposeHeader := []string{
httplimit.HeaderRetryAfter,
httplimit.HeaderRateLimitLimit,
httplimit.HeaderRateLimitRemaining,
httplimit.HeaderRateLimitReset,
}
if cfg.Session.EnableAuthTokenHeader {
exposeHeader = append(exposeHeader, "X-Auth-Token")
}
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
UnsafeWildcardOriginWithAllowCredentials: cfg.Server.Public.Cors.UnsafeWildcardOriginAllowed,
AllowOrigins: cfg.Server.Public.Cors.AllowOrigins,
ExposeHeaders: exposeHeader,
AllowCredentials: true,
// Based on: Chromium (starting in v76) caps at 2 hours (7200 seconds).
MaxAge: 7200,
}))

if prometheus != nil {
e.Use(prometheus.HandlerFunc)
Expand Down
1 change: 1 addition & 0 deletions backend/handler/thirdparty.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func (h *ThirdPartyHandler) Callback(c echo.Context) error {
}

err = h.auditLogger.Create(c, accountLinkingResult.Type, accountLinkingResult.User, nil)

if err != nil {
return h.redirectError(c, thirdparty.ErrorServer("could not create audit log").WithCause(err), h.cfg.ThirdParty.ErrorRedirectURL)
}
Expand Down
1 change: 0 additions & 1 deletion backend/handler/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ func (h TokenHandler) Validate(c echo.Context) error {

if h.cfg.Session.EnableAuthTokenHeader {
c.Response().Header().Set("X-Auth-Token", jwtToken)
c.Response().Header().Set("Access-Control-Expose-Headers", "X-Auth-Token")
}

userID = token.UserID
Expand Down
1 change: 0 additions & 1 deletion backend/handler/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ func (h *UserHandler) Create(c echo.Context) error {

if h.cfg.Session.EnableAuthTokenHeader {
c.Response().Header().Set("X-Auth-Token", token)
c.Response().Header().Set("Access-Control-Expose-Headers", "X-Auth-Token")
}
}

Expand Down
2 changes: 0 additions & 2 deletions backend/handler/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ func NewWebauthnHandler(cfg *config.Config, persister persistence.Persister, ses
wa, err := webauthn.New(&webauthn.Config{
RPDisplayName: cfg.Webauthn.RelyingParty.DisplayName,
RPID: cfg.Webauthn.RelyingParty.Id,
RPOrigin: cfg.Webauthn.RelyingParty.Origin,
RPOrigins: cfg.Webauthn.RelyingParty.Origins,
AttestationPreference: protocol.PreferNoAttestation,
AuthenticatorSelection: protocol.AuthenticatorSelection{
Expand Down Expand Up @@ -404,7 +403,6 @@ func (h *WebauthnHandler) FinishAuthentication(c echo.Context) error {

if h.cfg.Session.EnableAuthTokenHeader {
c.Response().Header().Set("X-Auth-Token", token)
c.Response().Header().Set("Access-Control-Expose-Headers", "X-Auth-Token")
}

err = h.auditLogger.Create(c, models.AuditLogWebAuthnAuthenticationFinalSucceeded, user, nil)
Expand Down
2 changes: 1 addition & 1 deletion backend/handler/webauthn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ var defaultConfig = config.Config{
Id: "localhost",
DisplayName: "Test Relying Party",
Icon: "",
Origin: "http://localhost:8080",
Origins: []string{"http://localhost:8080"},
},
Timeout: 60000,
},
Expand Down
Loading

0 comments on commit 1def6cf

Please sign in to comment.