From e6bfbcd48954ed20a4afa8f555edcade6e601c90 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Fri, 22 Jul 2022 11:11:35 +0200 Subject: [PATCH] OIDC: allow to debug the received id_token Signed-off-by: Nicola Murino --- config/config.go | 7 +++++++ config/config_test.go | 5 +++++ docs/full-configuration.md | 1 + go.mod | 2 +- go.sum | 4 ++-- httpd/oidc.go | 17 ++++++++++++++++- httpd/oidc_test.go | 1 + sftpgo.json | 3 ++- 8 files changed, 35 insertions(+), 5 deletions(-) diff --git a/config/config.go b/config/config.go index 6928f4ca..8c1dc835 100644 --- a/config/config.go +++ b/config/config.go @@ -122,6 +122,7 @@ var ( ImplicitRoles: false, Scopes: []string{"openid", "profile", "email"}, CustomFields: []string{}, + Debug: false, }, Security: httpd.SecurityConf{ Enabled: false, @@ -1437,6 +1438,12 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) { isSet = true } + debug, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__OIDC__DEBUG", idx)) + if ok { + result.Debug = debug + isSet = true + } + return result, isSet } diff --git a/config/config_test.go b/config/config_test.go index 510a2bd5..83170978 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1055,6 +1055,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { 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__OIDC__DEBUG", "1") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED", "true") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS", "*.example.com,*.example.net") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX", "1") @@ -1121,6 +1122,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { 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__OIDC__DEBUG") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ENABLED") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__ALLOWED_HOSTS_ARE_REGEX") @@ -1170,6 +1172,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.False(t, bindings[0].Security.Enabled) require.Equal(t, 0, bindings[0].ClientIPHeaderDepth) require.Len(t, bindings[0].OIDC.Scopes, 3) + require.False(t, bindings[0].OIDC.Debug) require.Equal(t, 8000, bindings[1].Port) require.Equal(t, "127.0.0.1", bindings[1].Address) require.False(t, bindings[1].EnableHTTPS) @@ -1182,6 +1185,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { 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].OIDC.Debug) 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) @@ -1219,6 +1223,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.Len(t, bindings[2].OIDC.CustomFields, 2) require.Equal(t, "field1", bindings[2].OIDC.CustomFields[0]) require.Equal(t, "field2", bindings[2].OIDC.CustomFields[1]) + require.True(t, bindings[2].OIDC.Debug) require.True(t, bindings[2].Security.Enabled) require.Len(t, bindings[2].Security.AllowedHosts, 2) require.Equal(t, "*.example.com", bindings[2].Security.AllowedHosts[0]) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index ec723da5..0609a214 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -280,6 +280,7 @@ The configuration file contains the following sections: - `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. + - `debug`, boolean. If set, the received id tokens will be logged at debug level. Default: `false`. - `security`, struct. Defines security headers to add to HTTP responses and allows to restrict allowed hosts. The following parameters are supported: - `enabled`, boolean. Set to `true` to enable security configurations. Default: `false`. - `allowed_hosts`, list of strings. Fully qualified domain names that are allowed. An empty list allows any and all host names. Default: empty. diff --git a/go.mod b/go.mod index 0aec5869..20be9322 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/net v0.0.0-20220708220712-1185a9018129 golang.org/x/oauth2 v0.0.0-20220718184931-c8730f7fcb92 - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 + golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49 golang.org/x/time v0.0.0-20220609170525-579cf78fd858 google.golang.org/api v0.88.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 diff --git a/go.sum b/go.sum index 7304313c..37eea406 100644 --- a/go.sum +++ b/go.sum @@ -970,8 +970,8 @@ golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49 h1:TMjZDarEwf621XDryfitp/8awEhiZNiwgphKlTMGRIg= +golang.org/x/sys v0.0.0-20220721230656-c6bc011c0c49/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/httpd/oidc.go b/httpd/oidc.go index b40576d9..89a86f18 100644 --- a/httpd/oidc.go +++ b/httpd/oidc.go @@ -89,7 +89,10 @@ type OIDC struct { // 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"` + CustomFields []string `json:"custom_fields" mapstructure:"custom_fields"` + // Debug enables the OIDC debug mode. In debug mode, the received id_token will be logged + // at the debug level + Debug bool `json:"debug" mapstructure:"debug"` provider *oidc.Provider verifier OIDCTokenVerifier providerLogoutURL string @@ -477,6 +480,16 @@ func (s *httpdServer) oidcLoginRedirect(w http.ResponseWriter, r *http.Request, oidc.Nonce(pendingAuth.Nonce)), http.StatusFound) } +func (s *httpdServer) debugTokenClaims(claims map[string]any, rawIDToken string) { + if s.binding.OIDC.Debug { + if claims == nil { + logger.Debug(logSender, "", "raw id token %q", rawIDToken) + } else { + logger.Debug(logSender, "", "raw id token %q, parsed claims %+v", rawIDToken, claims) + } + } +} + func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) { state := r.URL.Query().Get("state") authReq, err := oidcMgr.getPendingAuth(state) @@ -516,6 +529,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) doRedirect() return } + s.debugTokenClaims(nil, rawIDToken) idToken, err := s.binding.OIDC.verifier.Verify(ctx, rawIDToken) if err != nil { logger.Debug(logSender, "", "failed to verify oidc token: %v", err) @@ -541,6 +555,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) doLogout(rawIDToken) return } + s.debugTokenClaims(claims, rawIDToken) token := oidcToken{ AccessToken: oauth2Token.AccessToken, TokenType: oauth2Token.TokenType, diff --git a/httpd/oidc_test.go b/httpd/oidc_test.go index 02d960ac..dfd48327 100644 --- a/httpd/oidc_test.go +++ b/httpd/oidc_test.go @@ -1380,6 +1380,7 @@ func getTestOIDCServer() *httpdServer { ImplicitRoles: false, Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, CustomFields: nil, + Debug: true, }, }, enableWebAdmin: true, diff --git a/sftpgo.json b/sftpgo.json index de1f8136..a78e7fbf 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -266,7 +266,8 @@ "username_field": "", "role_field": "", "implicit_roles": false, - "custom_fields": [] + "custom_fields": [], + "debug": false }, "security": { "enabled": false,