mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 00:50:31 +00:00
oidc: update user after token refresh
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
bd294bb3cf
commit
6c7b3ac5bb
4 changed files with 155 additions and 19 deletions
7
go.mod
7
go.mod
|
@ -20,7 +20,7 @@ require (
|
|||
github.com/cockroachdb/cockroach-go/v2 v2.2.16
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.19.1
|
||||
github.com/fclairamb/ftpserverlib v0.19.2-0.20220922051837-cde05ddf9fe6
|
||||
github.com/fclairamb/go-log v0.4.1
|
||||
github.com/go-acme/lego/v4 v4.8.0
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20220512131524-9e71a0d4b3d6
|
||||
|
@ -70,7 +70,7 @@ require (
|
|||
golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1
|
||||
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
|
||||
golang.org/x/time v0.0.0-20220920022843-2ce7c2934d45
|
||||
google.golang.org/api v0.96.0
|
||||
google.golang.org/api v0.97.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
|
@ -156,7 +156,7 @@ require (
|
|||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 // indirect
|
||||
google.golang.org/grpc v1.49.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
|
@ -166,7 +166,6 @@ require (
|
|||
)
|
||||
|
||||
replace (
|
||||
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20220917142547-394d5e183aeb
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895
|
||||
|
|
12
go.sum
12
go.sum
|
@ -268,8 +268,6 @@ github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b h1:kCNBtUFKfhiUaE1Z
|
|||
github.com/drakkan/crypto v0.0.0-20220831070132-e3c36f2ab82b/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20220917142547-394d5e183aeb h1:sd63fxu7eKejDU0fBiGvVejeEEXNcGv6SVmXrFsDaKM=
|
||||
github.com/drakkan/ftpserverlib v0.0.0-20220917142547-394d5e183aeb/go.mod h1:Nwsxl2ZzyPiSCgB1rZGnEscTenwkxPhCn1D+Hm/k9JA=
|
||||
github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895 h1:YZkDIISo8YO7PAOX85GYxGCayjBqAutIAjL+XsdEgkc=
|
||||
github.com/drakkan/net v0.0.0-20220913160159-a08dc61b7895/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
|
||||
|
@ -286,6 +284,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
|
|||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fclairamb/ftpserverlib v0.19.2-0.20220922051837-cde05ddf9fe6 h1:WdhM0yDKdtSD+cqWHAMLMTwvUvmzy36eI3Ow8emZmn0=
|
||||
github.com/fclairamb/ftpserverlib v0.19.2-0.20220922051837-cde05ddf9fe6/go.mod h1:7pR5Ckeygw3T006z1ND6HYSbJz+fTvkFAXlF6snW4yI=
|
||||
github.com/fclairamb/go-log v0.4.1 h1:rLtdSG9x2pK41AIAnE8WYpl05xBJfw1ZyYxZaXFcBsM=
|
||||
github.com/fclairamb/go-log v0.4.1/go.mod h1:sw1KvnkZ4wKCYkvy4SL3qVZcJSWFP8Ure4pM3z+KNn4=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
|
@ -1123,8 +1123,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
|
|||
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
|
||||
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
|
||||
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
|
||||
google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM=
|
||||
google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/api v0.97.0 h1:x/vEL1XDF/2V4xzdNgFPaKHluRESo2aTsL7QzHnBtGQ=
|
||||
google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -1229,8 +1229,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP
|
|||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/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-20220920201722-2b89144ce006 h1:mmbq5q8M1t7dhkLw320YK4PsOXm6jdnUAkErImaIqOg=
|
||||
google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737 h1:K1zaaMdYBXRyX+cwFnxj7M6zwDyumLQMZ5xqwGvjreQ=
|
||||
google.golang.org/genproto v0.0.0-20220921223823-23cae91e6737/go.mod h1:2r/26NEF3bFmT3eC3aZreahSal0C3Shl8Gi6vyDYqOQ=
|
||||
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=
|
||||
|
|
|
@ -308,7 +308,7 @@ func (t *oidcToken) isExpired() bool {
|
|||
return t.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())
|
||||
}
|
||||
|
||||
func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier) error {
|
||||
func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier, r *http.Request) error {
|
||||
if t.RefreshToken == "" {
|
||||
logger.Debug(logSender, "", "refresh token not set, unable to refresh cookie %#v", t.Cookie)
|
||||
return errors.New("refresh token not set")
|
||||
|
@ -363,12 +363,44 @@ func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier) err
|
|||
if ok {
|
||||
t.SessionID = sid
|
||||
}
|
||||
err = t.refreshUser(r)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "unable to refresh user after token refresh for cookie %#v: %v", t.Cookie, err)
|
||||
return err
|
||||
}
|
||||
logger.Debug(logSender, "", "oidc token refreshed for user %#v, cookie %#v", t.Username, t.Cookie)
|
||||
oidcMgr.addToken(*t)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *oidcToken) refreshUser(r *http.Request) error {
|
||||
if t.isAdmin() {
|
||||
admin, err := dataprovider.AdminExists(t.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Permissions = admin.Permissions
|
||||
t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
|
||||
return nil
|
||||
}
|
||||
user, err := dataprovider.GetUserWithGroupSettings(t.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := user.CheckLoginConditions(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkHTTPClientUser(&user, r, xid.New().String(), true); err != nil {
|
||||
return err
|
||||
}
|
||||
t.Permissions = user.Filters.WebClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *oidcToken) getUser(r *http.Request) error {
|
||||
if t.isAdmin() {
|
||||
admin, err := dataprovider.AdminExists(t.Username)
|
||||
|
@ -438,7 +470,7 @@ func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request,
|
|||
}
|
||||
if token.isExpired() {
|
||||
logger.Debug(logSender, "", "oidc token associated with cookie %#v is expired", token.Cookie)
|
||||
if err = token.refresh(s.binding.OIDC.oauth2Config, s.binding.OIDC.verifier); err != nil {
|
||||
if err = token.refresh(s.binding.OIDC.oauth2Config, s.binding.OIDC.verifier, r); err != nil {
|
||||
setFlashMessage(w, r, "Your OpenID token is expired, please log-in again")
|
||||
doRedirect()
|
||||
return oidcToken{}, errInvalidToken
|
||||
|
|
|
@ -528,12 +528,16 @@ func TestOIDCLoginLogout(t *testing.T) {
|
|||
func TestOIDCRefreshToken(t *testing.T) {
|
||||
oidcMgr, ok := oidcMgr.(*memoryOIDCManager)
|
||||
require.True(t, ok)
|
||||
r, err := http.NewRequest(http.MethodGet, webUsersPath, nil)
|
||||
assert.NoError(t, err)
|
||||
token := oidcToken{
|
||||
Cookie: xid.New().String(),
|
||||
AccessToken: xid.New().String(),
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(-1 * time.Minute)),
|
||||
Nonce: xid.New().String(),
|
||||
Role: adminRoleFieldValue,
|
||||
Username: defaultAdminUsername,
|
||||
}
|
||||
config := mockOAuth2Config{
|
||||
tokenSource: &mockTokenSource{
|
||||
|
@ -543,12 +547,12 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
verifier := mockOIDCVerifier{
|
||||
err: common.ErrGenericFailure,
|
||||
}
|
||||
err := token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "refresh token not set")
|
||||
}
|
||||
token.RefreshToken = xid.New().String()
|
||||
err = token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
assert.ErrorIs(t, err, common.ErrGenericFailure)
|
||||
|
||||
newToken := &oauth2.Token{
|
||||
|
@ -564,7 +568,7 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
verifier = mockOIDCVerifier{
|
||||
token: &oidc.IDToken{},
|
||||
}
|
||||
err = token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "the refreshed token has no id token")
|
||||
}
|
||||
|
@ -580,7 +584,7 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
verifier = mockOIDCVerifier{
|
||||
err: common.ErrGenericFailure,
|
||||
}
|
||||
err = token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
assert.ErrorIs(t, err, common.ErrGenericFailure)
|
||||
|
||||
newToken = newToken.WithExtra(map[string]any{
|
||||
|
@ -595,7 +599,7 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
verifier = mockOIDCVerifier{
|
||||
token: &oidc.IDToken{},
|
||||
}
|
||||
err = token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "the refreshed token nonce mismatch")
|
||||
}
|
||||
|
@ -604,7 +608,7 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
Nonce: token.Nonce,
|
||||
},
|
||||
}
|
||||
err = token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "oidc: claims not set")
|
||||
}
|
||||
|
@ -615,13 +619,114 @@ func TestOIDCRefreshToken(t *testing.T) {
|
|||
verifier = mockOIDCVerifier{
|
||||
token: idToken,
|
||||
}
|
||||
err = token.refresh(&config, &verifier)
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, token.Permissions, 1)
|
||||
token.Role = nil
|
||||
// user does not exist
|
||||
err = token.refresh(&config, &verifier, r)
|
||||
assert.Error(t, err)
|
||||
require.Len(t, oidcMgr.tokens, 1)
|
||||
oidcMgr.removeToken(token.Cookie)
|
||||
require.Len(t, oidcMgr.tokens, 0)
|
||||
}
|
||||
|
||||
func TestOIDCRefreshUser(t *testing.T) {
|
||||
token := oidcToken{
|
||||
Cookie: xid.New().String(),
|
||||
AccessToken: xid.New().String(),
|
||||
TokenType: "Bearer",
|
||||
ExpiresAt: util.GetTimeAsMsSinceEpoch(time.Now().Add(1 * time.Minute)),
|
||||
Nonce: xid.New().String(),
|
||||
Role: adminRoleFieldValue,
|
||||
Username: "missing username",
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, webUsersPath, nil)
|
||||
assert.NoError(t, err)
|
||||
err = token.refreshUser(r)
|
||||
assert.Error(t, err)
|
||||
admin := dataprovider.Admin{
|
||||
Username: "test_oidc_admin_refresh",
|
||||
Password: "p",
|
||||
Permissions: []string{dataprovider.PermAdminAny},
|
||||
Status: 0,
|
||||
Filters: dataprovider.AdminFilters{
|
||||
Preferences: dataprovider.AdminPreferences{
|
||||
HideUserPageSections: 1 + 2 + 4,
|
||||
},
|
||||
},
|
||||
}
|
||||
err = dataprovider.AddAdmin(&admin, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
token.Username = admin.Username
|
||||
err = token.refreshUser(r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "is disabled")
|
||||
}
|
||||
|
||||
admin.Status = 1
|
||||
err = dataprovider.UpdateAdmin(&admin, "", "")
|
||||
assert.NoError(t, err)
|
||||
err = token.refreshUser(r)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, admin.Permissions, token.Permissions)
|
||||
assert.Equal(t, admin.Filters.Preferences.HideUserPageSections, token.HideUserPageSections)
|
||||
|
||||
err = dataprovider.DeleteAdmin(admin.Username, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
username := "test_oidc_user_refresh_token"
|
||||
user := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: username,
|
||||
Password: "p",
|
||||
HomeDir: filepath.Join(os.TempDir(), username),
|
||||
Status: 0,
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
Filters: dataprovider.UserFilters{
|
||||
BaseUserFilters: sdk.BaseUserFilters{
|
||||
DeniedProtocols: []string{common.ProtocolHTTP},
|
||||
WebClient: []string{sdk.WebClientSharesDisabled, sdk.WebClientWriteDisabled},
|
||||
},
|
||||
},
|
||||
}
|
||||
err = dataprovider.AddUser(&user, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
r, err = http.NewRequest(http.MethodGet, webClientFilesPath, nil)
|
||||
assert.NoError(t, err)
|
||||
token.Role = nil
|
||||
token.Username = username
|
||||
assert.False(t, token.isAdmin())
|
||||
err = token.refreshUser(r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "is disabled")
|
||||
}
|
||||
user, err = dataprovider.UserExists(username)
|
||||
assert.NoError(t, err)
|
||||
user.Status = 1
|
||||
err = dataprovider.UpdateUser(&user, "", "")
|
||||
assert.NoError(t, err)
|
||||
err = token.refreshUser(r)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "protocol HTTP is not allowed")
|
||||
}
|
||||
|
||||
user.Filters.DeniedProtocols = []string{common.ProtocolFTP}
|
||||
err = dataprovider.UpdateUser(&user, "", "")
|
||||
assert.NoError(t, err)
|
||||
err = token.refreshUser(r)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.Filters.WebClient, token.Permissions)
|
||||
|
||||
err = dataprovider.DeleteUser(username, "", "")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateOIDCToken(t *testing.T) {
|
||||
oidcMgr, ok := oidcMgr.(*memoryOIDCManager)
|
||||
require.True(t, ok)
|
||||
|
|
Loading…
Reference in a new issue