diff --git a/config/config.go b/config/config.go index 16318f82..c392e8c9 100644 --- a/config/config.go +++ b/config/config.go @@ -105,6 +105,7 @@ var ( UsernameField: "", RoleField: "", ImplicitRoles: false, + Scopes: []string{"openid", "profile", "email"}, CustomFields: []string{}, }, Security: httpd.SecurityConf{ @@ -1408,6 +1409,12 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) { isSet = true } + scopes, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__OIDC__SCOPES", idx)) + if ok { + result.Scopes = scopes + isSet = true + } + roleField, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__OIDC__ROLE_FIELD", idx)) if ok { result.RoleField = roleField diff --git a/config/config_test.go b/config/config_test.go index bdca9842..bd682ad3 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1060,6 +1060,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__REDIRECT_BASE_URL", "redirect base url") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD", "preferred_username") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__ROLE_FIELD", "sftpgo_role") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__SCOPES", "openid") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__IMPLICIT_ROLES", "1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS", "field1,field2") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED", "true") @@ -1124,6 +1125,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__REDIRECT_BASE_URL") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__ROLE_FIELD") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__SCOPES") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__IMPLICIT_ROLES") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED") @@ -1173,6 +1175,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Equal(t, 0, bindings[0].HideLoginURL) require.False(t, bindings[0].Security.Enabled) require.Equal(t, 0, bindings[0].ClientIPHeaderDepth) + require.Len(t, bindings[0].OIDC.Scopes, 3) require.Equal(t, 8000, bindings[1].Port) require.Equal(t, "127.0.0.1", bindings[1].Address) require.False(t, bindings[1].EnableHTTPS) @@ -1183,6 +1186,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Nil(t, bindings[1].TLSCipherSuites) require.Equal(t, 1, bindings[1].HideLoginURL) require.Empty(t, bindings[1].OIDC.ClientID) + require.Len(t, bindings[1].OIDC.Scopes, 3) require.False(t, bindings[1].Security.Enabled) require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name) require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName) @@ -1213,6 +1217,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Equal(t, "redirect base url", bindings[2].OIDC.RedirectBaseURL) require.Equal(t, "preferred_username", bindings[2].OIDC.UsernameField) require.Equal(t, "sftpgo_role", bindings[2].OIDC.RoleField) + require.Len(t, bindings[2].OIDC.Scopes, 1) + require.Equal(t, "openid", bindings[2].OIDC.Scopes[0]) require.True(t, bindings[2].OIDC.ImplicitRoles) require.Len(t, bindings[2].OIDC.CustomFields, 2) require.Equal(t, "field1", bindings[2].OIDC.CustomFields[0]) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 4e2af7d6..cf3aba68 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -279,6 +279,7 @@ The configuration file contains the following sections: - `client_secret`, string. Defines the application's secret. Default: blank. - `redirect_base_url`, string. Defines the base URL to redirect to after OpenID authentication. The suffix `/web/oidc/redirect` will be added to this base URL, adding also the `web_root` if configured. Default: blank. - `username_field`, string. Defines the ID token claims field to map to the SFTPGo username. Default: blank. + - `scopes`, list of strings. Request the OAuth provider to provide the scope information from an authenticated users. The `openid` scope is mandatory. Default: `"openid", "profile", "email"`. - `role_field`, string. Defines the optional ID token claims field to map to a SFTPGo role. If the defined ID token claims field is set to `admin` the authenticated user is mapped to an SFTPGo admin. You don't need to specify this field if you want to use OpenID only for the Web Client UI. Default: blank. - `implicit_roles`, boolean. If set, the `role_field` is ignored and the SFTPGo role is assumed based on the login link used. Default: `false`. - `custom_fields`, list of strings. Custom token claims fields to pass to the pre-login hook. Default: empty. diff --git a/docs/oidc.md b/docs/oidc.md index ba857d13..fb32d945 100644 --- a/docs/oidc.md +++ b/docs/oidc.md @@ -1,6 +1,7 @@ # OpenID Connect -OpenID Connect integration allows you to map your identity provider users to SFTPGo admins/users and so you can login to SFTPGo Web Client and Web Admin user interfaces using your identity provider. +OpenID Connect integration allows you to map your identity provider users to SFTPGo admins/users, +so you can login to SFTPGo Web Client and Web Admin user interfaces, using your own identity provider. SFTPGo allows to configure per-binding OpenID Connect configurations. The supported configuration parameters are documented within the `oidc` section [here](./full-configuration.md). @@ -42,6 +43,7 @@ Add the following configuration parameters to the SFTPGo configuration file (or "config_url": "http://192.168.1.12:8086/auth/realms/sftpgo", "redirect_base_url": "http://192.168.1.50:8080", "username_field": "preferred_username", + "scopes": [ "openid", "profile", "email" ], "role_field": "sftpgo_role", "implicit_roles": false, "custom_fields": [] @@ -104,8 +106,12 @@ And the following is an example ID token which allows the SFTPGo user `user1` to ``` SFTPGo users (not admins) can be created/updated after successful OpenID authentication by defining a [pre-login hook](./dynamic-user-mod.md). -You can use the `custom_fields` configuration parameter to define the token claims field names to pass to the pre-login hook, these fields are useful for implementing custom logic when creating/updating the SFTPGo user within the hook. -For example you can set the field `sftpgo_home_dir` in your identity provider and add it to the `custom_fields` in the SFTPGo configuration like this: +You can use `scopes` configuration to request additional information (claims) about authenticated users (See your provider's own documentation for more information). +By default the scopes `"openid", "profile", "email"` are retrieved. +The `custom_fields` configuration parameter can be used to define claim field names to pass to the pre-login hook, +these fields can be used e.g. for implementing custom logic when creating/updating the SFTPGo user within the hook. +For example, if you have created a scope with name `sftpgo` in your identity provider to provide a claim for `sftpgo_home_dir` , +then you can add it to the `custom_fields` in the SFTPGo configuration like this: ```json ... @@ -115,6 +121,7 @@ For example you can set the field `sftpgo_home_dir` in your identity provider an "config_url": "http://192.168.1.12:8086/auth/realms/sftpgo", "redirect_base_url": "http://192.168.1.50:8080", "username_field": "preferred_username", + "scopes": [ "openid", "profile", "email", "sftpgo" ], "role_field": "sftpgo_role", "custom_fields": ["sftpgo_home_dir"] } diff --git a/go.mod b/go.mod index 50d7ff80..7c248446 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/spf13/viper v1.12.0 github.com/stretchr/testify v1.8.0 github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 - github.com/unrolled/secure v1.11.0 + github.com/unrolled/secure v1.12.0 github.com/wagslane/go-password-validator v0.3.0 github.com/xhit/go-simple-mail/v2 v2.11.0 github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a @@ -155,7 +155,7 @@ require ( golang.org/x/tools v0.1.11 // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220714211235-042d03aeabc9 // indirect + google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9 // indirect google.golang.org/grpc v1.48.0 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/ini.v1 v1.66.6 // indirect diff --git a/go.sum b/go.sum index e6282f27..24c11960 100644 --- a/go.sum +++ b/go.sum @@ -760,8 +760,8 @@ github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjM github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/unrolled/secure v1.11.0 h1:fjkKhD/MsQnlmz/au+MmFptCFNhvf5iv04ALkdCXRCI= -github.com/unrolled/secure v1.11.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/unrolled/secure v1.12.0 h1:7k3jcgLwfjiKkhQde6VbQ3D4KDLtDBqDd/hs3PPANDY= +github.com/unrolled/secure v1.12.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= github.com/xhit/go-simple-mail/v2 v2.11.0 h1:o/056V50zfkO3Mm5tVdo9rG3ryg4ZmJ2XW5GMinHfVs= @@ -1224,8 +1224,8 @@ google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljW google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220714211235-042d03aeabc9 h1:zfXhTgBfGlIh3jMXN06W8qbhFGsh6MJNJiYEuhTddOI= -google.golang.org/genproto v0.0.0-20220714211235-042d03aeabc9/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9 h1:1aEQRgZ4Gks2SRAkLzIPpIszRazwVfjSFe1cKc+e0Jg= +google.golang.org/genproto v0.0.0-20220715211116-798f69b842b9/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/httpd/oidc.go b/httpd/oidc.go index ad3d026d..6bf506b7 100644 --- a/httpd/oidc.go +++ b/httpd/oidc.go @@ -70,6 +70,10 @@ type OIDC struct { // If set, the `RoleField` is ignored and the SFTPGo role is assumed based on // the login link used ImplicitRoles bool `json:"implicit_roles" mapstructure:"implicit_roles"` + // Scopes required by the OAuth provider to retrieve information about the authenticated user. + // The "openid" scope is required. + // Refer to your OAuth provider documentation for more information about this + Scopes []string `json:"scopes" mapstructure:"scopes"` // Custom token claims fields to pass to the pre-login hook CustomFields []string `json:"custom_fields" mapstructure:"custom_fields"` provider *oidc.Provider @@ -116,6 +120,9 @@ func (o *OIDC) initialize() error { if o.RedirectBaseURL == "" { return errors.New("oidc: redirect base URL cannot be empty") } + if !util.Contains(o.Scopes, oidc.ScopeOpenID) { + return fmt.Errorf("oidc: required scope %q is not set", oidc.ScopeOpenID) + } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() @@ -143,7 +150,7 @@ func (o *OIDC) initialize() error { ClientSecret: o.ClientSecret, Endpoint: o.provider.Endpoint(), RedirectURL: o.getRedirectURL(), - Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, + Scopes: o.Scopes, } return nil diff --git a/httpd/oidc_test.go b/httpd/oidc_test.go index 6873dfcb..b1787fd0 100644 --- a/httpd/oidc_test.go +++ b/httpd/oidc_test.go @@ -93,6 +93,11 @@ func TestOIDCInitialization(t *testing.T) { RoleField: "sftpgo_role", } err = config.initialize() + if assert.Error(t, err) { + assert.Contains(t, err.Error(), "oidc: required scope \"openid\" is not set") + } + config.Scopes = []string{oidc.ScopeOpenID} + err = config.initialize() if assert.Error(t, err) { assert.Contains(t, err.Error(), "oidc: unable to initialize provider") } @@ -1263,6 +1268,7 @@ func getTestOIDCServer() *httpdServer { UsernameField: "preferred_username", RoleField: "sftpgo_role", ImplicitRoles: false, + Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, CustomFields: nil, }, }, diff --git a/sftpgo.json b/sftpgo.json index 1fe0ca97..f78f0703 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -263,6 +263,11 @@ "client_secret": "", "config_url": "", "redirect_base_url": "", + "scopes": [ + "openid", + "profile", + "email" + ], "username_field": "", "role_field": "", "implicit_roles": false,