sftpgo/httpd/oidcmanager.go
Nicola Murino 796ea1dde9
allow to store temporary sessions within the data provider
so we can persist password reset codes, OIDC auth sessions and tokens.
These features will also work in multi-node setups without sicky
sessions now

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2022-05-19 19:49:51 +02:00

230 lines
6.4 KiB
Go

package httpd
import (
"encoding/json"
"errors"
"sync"
"time"
"github.com/drakkan/sftpgo/v2/dataprovider"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/util"
)
var (
oidcMgr oidcManager
)
func newOIDCManager(isShared int) oidcManager {
if isShared == 1 {
logger.Info(logSender, "", "using provider OIDC manager")
return &dbOIDCManager{}
}
logger.Info(logSender, "", "using memory OIDC manager")
return &memoryOIDCManager{
pendingAuths: make(map[string]oidcPendingAuth),
tokens: make(map[string]oidcToken),
}
}
type oidcManager interface {
addPendingAuth(pendingAuth oidcPendingAuth)
removePendingAuth(state string)
getPendingAuth(state string) (oidcPendingAuth, error)
addToken(token oidcToken)
getToken(cookie string) (oidcToken, error)
removeToken(cookie string)
updateTokenUsage(token oidcToken)
cleanup()
}
type memoryOIDCManager struct {
authMutex sync.RWMutex
pendingAuths map[string]oidcPendingAuth
tokenMutex sync.RWMutex
tokens map[string]oidcToken
}
func (o *memoryOIDCManager) addPendingAuth(pendingAuth oidcPendingAuth) {
o.authMutex.Lock()
o.pendingAuths[pendingAuth.State] = pendingAuth
o.authMutex.Unlock()
}
func (o *memoryOIDCManager) removePendingAuth(state string) {
o.authMutex.Lock()
defer o.authMutex.Unlock()
delete(o.pendingAuths, state)
}
func (o *memoryOIDCManager) getPendingAuth(state string) (oidcPendingAuth, error) {
o.authMutex.RLock()
defer o.authMutex.RUnlock()
authReq, ok := o.pendingAuths[state]
if !ok {
return oidcPendingAuth{}, errors.New("oidc: no auth request found for the specified state")
}
diff := util.GetTimeAsMsSinceEpoch(time.Now()) - authReq.IssuedAt
if diff > authStateValidity {
return oidcPendingAuth{}, errors.New("oidc: auth request is too old")
}
return authReq, nil
}
func (o *memoryOIDCManager) addToken(token oidcToken) {
o.tokenMutex.Lock()
token.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now())
o.tokens[token.Cookie] = token
o.tokenMutex.Unlock()
}
func (o *memoryOIDCManager) getToken(cookie string) (oidcToken, error) {
o.tokenMutex.RLock()
defer o.tokenMutex.RUnlock()
token, ok := o.tokens[cookie]
if !ok {
return oidcToken{}, errors.New("oidc: no token found for the specified session")
}
diff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt
if diff > tokenDeleteInterval {
return oidcToken{}, errors.New("oidc: token is too old")
}
return token, nil
}
func (o *memoryOIDCManager) removeToken(cookie string) {
o.tokenMutex.Lock()
defer o.tokenMutex.Unlock()
delete(o.tokens, cookie)
}
func (o *memoryOIDCManager) updateTokenUsage(token oidcToken) {
diff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt
if diff > tokenUpdateInterval {
o.addToken(token)
}
}
func (o *memoryOIDCManager) cleanup() {
logger.Debug(logSender, "", "oidc manager cleanup")
o.cleanupAuthRequests()
o.cleanupTokens()
}
func (o *memoryOIDCManager) cleanupAuthRequests() {
o.authMutex.Lock()
defer o.authMutex.Unlock()
for k, auth := range o.pendingAuths {
diff := util.GetTimeAsMsSinceEpoch(time.Now()) - auth.IssuedAt
// remove old pending auth requests
if diff < 0 || diff > authStateValidity {
delete(o.pendingAuths, k)
}
}
}
func (o *memoryOIDCManager) cleanupTokens() {
o.tokenMutex.Lock()
defer o.tokenMutex.Unlock()
for k, token := range o.tokens {
diff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt
// remove tokens unused from more than tokenDeleteInterval
if diff > tokenDeleteInterval {
delete(o.tokens, k)
}
}
}
type dbOIDCManager struct{}
func (o *dbOIDCManager) addPendingAuth(pendingAuth oidcPendingAuth) {
session := dataprovider.Session{
Key: pendingAuth.State,
Data: pendingAuth,
Type: dataprovider.SessionTypeOIDCAuth,
Timestamp: pendingAuth.IssuedAt + authStateValidity,
}
dataprovider.AddSharedSession(session) //nolint:errcheck
}
func (o *dbOIDCManager) removePendingAuth(state string) {
dataprovider.DeleteSharedSession(state) //nolint:errcheck
}
func (o *dbOIDCManager) getPendingAuth(state string) (oidcPendingAuth, error) {
session, err := dataprovider.GetSharedSession(state)
if err != nil {
return oidcPendingAuth{}, errors.New("oidc: unable to get the auth request for the specified state")
}
if session.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now()) {
// expired
return oidcPendingAuth{}, errors.New("oidc: auth request is too old")
}
return o.decodePendingAuthData(session.Data)
}
func (o *dbOIDCManager) decodePendingAuthData(data any) (oidcPendingAuth, error) {
if val, ok := data.([]byte); ok {
authReq := oidcPendingAuth{}
err := json.Unmarshal(val, &authReq)
return authReq, err
}
logger.Error(logSender, "", "invalid oidc auth request data type %T", data)
return oidcPendingAuth{}, errors.New("oidc: invalid auth request data")
}
func (o *dbOIDCManager) addToken(token oidcToken) {
token.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now())
session := dataprovider.Session{
Key: token.Cookie,
Data: token,
Type: dataprovider.SessionTypeOIDCToken,
Timestamp: token.UsedAt + tokenDeleteInterval,
}
dataprovider.AddSharedSession(session) //nolint:errcheck
}
func (o *dbOIDCManager) removeToken(cookie string) {
dataprovider.DeleteSharedSession(cookie) //nolint:errcheck
}
func (o *dbOIDCManager) updateTokenUsage(token oidcToken) {
diff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt
if diff > tokenUpdateInterval {
o.addToken(token)
}
}
func (o *dbOIDCManager) getToken(cookie string) (oidcToken, error) {
session, err := dataprovider.GetSharedSession(cookie)
if err != nil {
return oidcToken{}, errors.New("oidc: unable to get the token for the specified session")
}
if session.Timestamp < util.GetTimeAsMsSinceEpoch(time.Now()) {
// expired
return oidcToken{}, errors.New("oidc: token is too old")
}
return o.decodeTokenData(session.Data)
}
func (o *dbOIDCManager) decodeTokenData(data any) (oidcToken, error) {
if val, ok := data.([]byte); ok {
token := oidcToken{}
err := json.Unmarshal(val, &token)
return token, err
}
logger.Error(logSender, "", "invalid oidc token data type %T", data)
return oidcToken{}, errors.New("oidc: invalid token data")
}
func (o *dbOIDCManager) cleanup() {
logger.Debug(logSender, "", "oidc manager cleanup")
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCAuth, time.Now()) //nolint:errcheck
dataprovider.CleanupSharedSessions(dataprovider.SessionTypeOIDCToken, time.Now()) //nolint:errcheck
}