oidc: update user after token refresh

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-09-22 08:30:22 +02:00
parent bd294bb3cf
commit 6c7b3ac5bb
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
4 changed files with 155 additions and 19 deletions

7
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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

View file

@ -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)