oidc: allow to configure oauth2 scopes

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-07-16 19:25:04 +02:00
parent 371012a46e
commit 6b995db864
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
9 changed files with 49 additions and 10 deletions

View file

@ -105,6 +105,7 @@ var (
UsernameField: "", UsernameField: "",
RoleField: "", RoleField: "",
ImplicitRoles: false, ImplicitRoles: false,
Scopes: []string{"openid", "profile", "email"},
CustomFields: []string{}, CustomFields: []string{},
}, },
Security: httpd.SecurityConf{ Security: httpd.SecurityConf{
@ -1408,6 +1409,12 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) {
isSet = true 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)) roleField, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__OIDC__ROLE_FIELD", idx))
if ok { if ok {
result.RoleField = roleField result.RoleField = roleField

View file

@ -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__REDIRECT_BASE_URL", "redirect base url")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD", "preferred_username") 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__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__IMPLICIT_ROLES", "1")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS", "field1,field2") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS", "field1,field2")
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED", "true") 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__REDIRECT_BASE_URL")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__USERNAME_FIELD") 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__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__IMPLICIT_ROLES")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__OIDC__CUSTOM_FIELDS")
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED") 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.Equal(t, 0, bindings[0].HideLoginURL)
require.False(t, bindings[0].Security.Enabled) require.False(t, bindings[0].Security.Enabled)
require.Equal(t, 0, bindings[0].ClientIPHeaderDepth) 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, 8000, bindings[1].Port)
require.Equal(t, "127.0.0.1", bindings[1].Address) require.Equal(t, "127.0.0.1", bindings[1].Address)
require.False(t, bindings[1].EnableHTTPS) require.False(t, bindings[1].EnableHTTPS)
@ -1183,6 +1186,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
require.Nil(t, bindings[1].TLSCipherSuites) require.Nil(t, bindings[1].TLSCipherSuites)
require.Equal(t, 1, bindings[1].HideLoginURL) require.Equal(t, 1, bindings[1].HideLoginURL)
require.Empty(t, bindings[1].OIDC.ClientID) require.Empty(t, bindings[1].OIDC.ClientID)
require.Len(t, bindings[1].OIDC.Scopes, 3)
require.False(t, bindings[1].Security.Enabled) require.False(t, bindings[1].Security.Enabled)
require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name) require.Equal(t, "Web Admin", bindings[1].Branding.WebAdmin.Name)
require.Equal(t, "WebClient", bindings[1].Branding.WebClient.ShortName) 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, "redirect base url", bindings[2].OIDC.RedirectBaseURL)
require.Equal(t, "preferred_username", bindings[2].OIDC.UsernameField) require.Equal(t, "preferred_username", bindings[2].OIDC.UsernameField)
require.Equal(t, "sftpgo_role", bindings[2].OIDC.RoleField) 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.True(t, bindings[2].OIDC.ImplicitRoles)
require.Len(t, bindings[2].OIDC.CustomFields, 2) require.Len(t, bindings[2].OIDC.CustomFields, 2)
require.Equal(t, "field1", bindings[2].OIDC.CustomFields[0]) require.Equal(t, "field1", bindings[2].OIDC.CustomFields[0])

View file

@ -279,6 +279,7 @@ The configuration file contains the following sections:
- `client_secret`, string. Defines the application's secret. Default: blank. - `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. - `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. - `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. - `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`. - `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. - `custom_fields`, list of strings. Custom token claims fields to pass to the pre-login hook. Default: empty.

View file

@ -1,6 +1,7 @@
# OpenID Connect # 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). 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", "config_url": "http://192.168.1.12:8086/auth/realms/sftpgo",
"redirect_base_url": "http://192.168.1.50:8080", "redirect_base_url": "http://192.168.1.50:8080",
"username_field": "preferred_username", "username_field": "preferred_username",
"scopes": [ "openid", "profile", "email" ],
"role_field": "sftpgo_role", "role_field": "sftpgo_role",
"implicit_roles": false, "implicit_roles": false,
"custom_fields": [] "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). 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. You can use `scopes` configuration to request additional information (claims) about authenticated users (See your provider's own documentation for more information).
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: 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 ```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", "config_url": "http://192.168.1.12:8086/auth/realms/sftpgo",
"redirect_base_url": "http://192.168.1.50:8080", "redirect_base_url": "http://192.168.1.50:8080",
"username_field": "preferred_username", "username_field": "preferred_username",
"scopes": [ "openid", "profile", "email", "sftpgo" ],
"role_field": "sftpgo_role", "role_field": "sftpgo_role",
"custom_fields": ["sftpgo_home_dir"] "custom_fields": ["sftpgo_home_dir"]
} }

4
go.mod
View file

@ -58,7 +58,7 @@ require (
github.com/spf13/viper v1.12.0 github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 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/wagslane/go-password-validator v0.3.0
github.com/xhit/go-simple-mail/v2 v2.11.0 github.com/xhit/go-simple-mail/v2 v2.11.0
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a 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/tools v0.1.11 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/appengine v1.6.7 // 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/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/ini.v1 v1.66.6 // indirect

8
go.sum
View file

@ -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/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 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/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.12.0 h1:7k3jcgLwfjiKkhQde6VbQ3D4KDLtDBqDd/hs3PPANDY=
github.com/unrolled/secure v1.11.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= 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 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= 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= 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-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-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-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-20220715211116-798f69b842b9 h1:1aEQRgZ4Gks2SRAkLzIPpIszRazwVfjSFe1cKc+e0Jg=
google.golang.org/genproto v0.0.0-20220714211235-042d03aeabc9/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= 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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=

View file

@ -70,6 +70,10 @@ type OIDC struct {
// If set, the `RoleField` is ignored and the SFTPGo role is assumed based on // If set, the `RoleField` is ignored and the SFTPGo role is assumed based on
// the login link used // the login link used
ImplicitRoles bool `json:"implicit_roles" mapstructure:"implicit_roles"` 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 // Custom token claims fields to pass to the pre-login hook
CustomFields []string `json:"custom_fields" mapstructure:"custom_fields"` CustomFields []string `json:"custom_fields" mapstructure:"custom_fields"`
provider *oidc.Provider provider *oidc.Provider
@ -116,6 +120,9 @@ func (o *OIDC) initialize() error {
if o.RedirectBaseURL == "" { if o.RedirectBaseURL == "" {
return errors.New("oidc: redirect base URL cannot be empty") 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) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@ -143,7 +150,7 @@ func (o *OIDC) initialize() error {
ClientSecret: o.ClientSecret, ClientSecret: o.ClientSecret,
Endpoint: o.provider.Endpoint(), Endpoint: o.provider.Endpoint(),
RedirectURL: o.getRedirectURL(), RedirectURL: o.getRedirectURL(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, Scopes: o.Scopes,
} }
return nil return nil

View file

@ -93,6 +93,11 @@ func TestOIDCInitialization(t *testing.T) {
RoleField: "sftpgo_role", RoleField: "sftpgo_role",
} }
err = config.initialize() 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) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "oidc: unable to initialize provider") assert.Contains(t, err.Error(), "oidc: unable to initialize provider")
} }
@ -1263,6 +1268,7 @@ func getTestOIDCServer() *httpdServer {
UsernameField: "preferred_username", UsernameField: "preferred_username",
RoleField: "sftpgo_role", RoleField: "sftpgo_role",
ImplicitRoles: false, ImplicitRoles: false,
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
CustomFields: nil, CustomFields: nil,
}, },
}, },

View file

@ -263,6 +263,11 @@
"client_secret": "", "client_secret": "",
"config_url": "", "config_url": "",
"redirect_base_url": "", "redirect_base_url": "",
"scopes": [
"openid",
"profile",
"email"
],
"username_field": "", "username_field": "",
"role_field": "", "role_field": "",
"implicit_roles": false, "implicit_roles": false,