| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772 | 
							- // Copyright (C) 2019-2022  Nicola Murino
 
- //
 
- // This program is free software: you can redistribute it and/or modify
 
- // it under the terms of the GNU Affero General Public License as published
 
- // by the Free Software Foundation, version 3.
 
- //
 
- // This program is distributed in the hope that it will be useful,
 
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
 
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 
- // GNU Affero General Public License for more details.
 
- //
 
- // You should have received a copy of the GNU Affero General Public License
 
- // along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
- package httpd
 
- import (
 
- 	"context"
 
- 	"errors"
 
- 	"fmt"
 
- 	"net/http"
 
- 	"net/url"
 
- 	"strings"
 
- 	"time"
 
- 	"github.com/coreos/go-oidc/v3/oidc"
 
- 	"github.com/rs/xid"
 
- 	"golang.org/x/oauth2"
 
- 	"github.com/drakkan/sftpgo/v2/internal/common"
 
- 	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
 
- 	"github.com/drakkan/sftpgo/v2/internal/httpclient"
 
- 	"github.com/drakkan/sftpgo/v2/internal/logger"
 
- 	"github.com/drakkan/sftpgo/v2/internal/util"
 
- )
 
- const (
 
- 	oidcCookieKey       = "oidc"
 
- 	adminRoleFieldValue = "admin"
 
- 	authStateValidity   = 1 * 60 * 1000   // 1 minute
 
- 	tokenUpdateInterval = 3 * 60 * 1000   // 3 minutes
 
- 	tokenDeleteInterval = 2 * 3600 * 1000 // 2 hours
 
- )
 
- var (
 
- 	oidcTokenKey       = &contextKey{"OIDC token key"}
 
- 	oidcGeneratedToken = &contextKey{"OIDC generated token"}
 
- )
 
- // OAuth2Config defines an interface for OAuth2 methods, so we can mock them
 
- type OAuth2Config interface {
 
- 	AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
 
- 	Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
 
- 	TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
 
- }
 
- // OIDCTokenVerifier defines an interface for OpenID token verifier, so we can mock them
 
- type OIDCTokenVerifier interface {
 
- 	Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)
 
- }
 
- // OIDC defines the OpenID Connect configuration
 
- type OIDC struct {
 
- 	// ClientID is the application's ID
 
- 	ClientID string `json:"client_id" mapstructure:"client_id"`
 
- 	// ClientSecret is the application's secret
 
- 	ClientSecret string `json:"client_secret" mapstructure:"client_secret"`
 
- 	// ConfigURL is the identifier for the service.
 
- 	// SFTPGo will try to retrieve the provider configuration on startup and then
 
- 	// will refuse to start if it fails to connect to the specified URL
 
- 	ConfigURL string `json:"config_url" mapstructure:"config_url"`
 
- 	// RedirectBaseURL is 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
 
- 	RedirectBaseURL string `json:"redirect_base_url" mapstructure:"redirect_base_url"`
 
- 	// ID token claims field to map to the SFTPGo username
 
- 	UsernameField string `json:"username_field" mapstructure:"username_field"`
 
- 	// 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
 
- 	RoleField string `json:"role_field" mapstructure:"role_field"`
 
- 	// If set, the `RoleField` is ignored and the SFTPGo role is assumed based on
 
- 	// the login link used
 
- 	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
 
- 	CustomFields []string `json:"custom_fields" mapstructure:"custom_fields"`
 
- 	// InsecureSkipSignatureCheck causes SFTPGo to skip JWT signature validation.
 
- 	// It's intended for special cases where providers, such as Azure, use the "none"
 
- 	// algorithm. Skipping the signature validation can cause security issues
 
- 	InsecureSkipSignatureCheck bool `json:"insecure_skip_signature_check" mapstructure:"insecure_skip_signature_check"`
 
- 	// 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
 
- 	oauth2Config      OAuth2Config
 
- }
 
- func (o *OIDC) isEnabled() bool {
 
- 	return o.provider != nil
 
- }
 
- func (o *OIDC) hasRoles() bool {
 
- 	return o.isEnabled() && (o.RoleField != "" || o.ImplicitRoles)
 
- }
 
- func (o *OIDC) getForcedRole(audience string) string {
 
- 	if !o.ImplicitRoles {
 
- 		return ""
 
- 	}
 
- 	if audience == tokenAudienceWebAdmin {
 
- 		return adminRoleFieldValue
 
- 	}
 
- 	return ""
 
- }
 
- func (o *OIDC) getRedirectURL() string {
 
- 	url := o.RedirectBaseURL
 
- 	if strings.HasSuffix(o.RedirectBaseURL, "/") {
 
- 		url = strings.TrimSuffix(o.RedirectBaseURL, "/")
 
- 	}
 
- 	url += webOIDCRedirectPath
 
- 	logger.Debug(logSender, "", "oidc redirect URL: %#v", url)
 
- 	return url
 
- }
 
- func (o *OIDC) initialize() error {
 
- 	if o.ConfigURL == "" {
 
- 		return nil
 
- 	}
 
- 	if o.UsernameField == "" {
 
- 		return errors.New("oidc: username field cannot be empty")
 
- 	}
 
- 	if o.RedirectBaseURL == "" {
 
- 		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)
 
- 	defer cancel()
 
- 	provider, err := oidc.NewProvider(ctx, o.ConfigURL)
 
- 	if err != nil {
 
- 		return fmt.Errorf("oidc: unable to initialize provider for URL %#v: %w", o.ConfigURL, err)
 
- 	}
 
- 	claims := make(map[string]any)
 
- 	// we cannot get an error here because the response body was already parsed as JSON
 
- 	// on provider creation
 
- 	provider.Claims(&claims) //nolint:errcheck
 
- 	endSessionEndPoint, ok := claims["end_session_endpoint"]
 
- 	if ok {
 
- 		if val, ok := endSessionEndPoint.(string); ok {
 
- 			o.providerLogoutURL = val
 
- 			logger.Debug(logSender, "", "oidc end session endpoint %#v", o.providerLogoutURL)
 
- 		}
 
- 	}
 
- 	o.provider = provider
 
- 	o.verifier = provider.Verifier(&oidc.Config{
 
- 		ClientID:                   o.ClientID,
 
- 		InsecureSkipSignatureCheck: o.InsecureSkipSignatureCheck,
 
- 	})
 
- 	o.oauth2Config = &oauth2.Config{
 
- 		ClientID:     o.ClientID,
 
- 		ClientSecret: o.ClientSecret,
 
- 		Endpoint:     o.provider.Endpoint(),
 
- 		RedirectURL:  o.getRedirectURL(),
 
- 		Scopes:       o.Scopes,
 
- 	}
 
- 	return nil
 
- }
 
- type oidcPendingAuth struct {
 
- 	State    string        `json:"state"`
 
- 	Nonce    string        `json:"nonce"`
 
- 	Audience tokenAudience `json:"audience"`
 
- 	IssuedAt int64         `json:"issued_at"`
 
- }
 
- func newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {
 
- 	return oidcPendingAuth{
 
- 		State:    xid.New().String(),
 
- 		Nonce:    xid.New().String(),
 
- 		Audience: audience,
 
- 		IssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),
 
- 	}
 
- }
 
- type oidcToken struct {
 
- 	AccessToken          string          `json:"access_token"`
 
- 	TokenType            string          `json:"token_type,omitempty"`
 
- 	RefreshToken         string          `json:"refresh_token,omitempty"`
 
- 	ExpiresAt            int64           `json:"expires_at,omitempty"`
 
- 	SessionID            string          `json:"session_id"`
 
- 	IDToken              string          `json:"id_token"`
 
- 	Nonce                string          `json:"nonce"`
 
- 	Username             string          `json:"username"`
 
- 	Permissions          []string        `json:"permissions"`
 
- 	HideUserPageSections int             `json:"hide_user_page_sections,omitempty"`
 
- 	TokenRole            string          `json:"token_role,omitempty"` // SFTPGo role name
 
- 	Role                 any             `json:"role"`                 // oidc user role: SFTPGo user or admin
 
- 	CustomFields         *map[string]any `json:"custom_fields,omitempty"`
 
- 	Cookie               string          `json:"cookie"`
 
- 	UsedAt               int64           `json:"used_at"`
 
- }
 
- func (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField string, customFields []string,
 
- 	forcedRole string,
 
- ) error {
 
- 	getClaimsFields := func() []string {
 
- 		keys := make([]string, 0, len(claims))
 
- 		for k := range claims {
 
- 			keys = append(keys, k)
 
- 		}
 
- 		return keys
 
- 	}
 
- 	username, ok := claims[usernameField].(string)
 
- 	if !ok || username == "" {
 
- 		logger.Warn(logSender, "", "username field %#v not found, claims fields: %+v", usernameField, getClaimsFields())
 
- 		return errors.New("no username field")
 
- 	}
 
- 	t.Username = username
 
- 	if forcedRole != "" {
 
- 		t.Role = forcedRole
 
- 	} else {
 
- 		t.getRoleFromField(claims, roleField)
 
- 	}
 
- 	t.CustomFields = nil
 
- 	if len(customFields) > 0 {
 
- 		for _, field := range customFields {
 
- 			if val, ok := claims[field]; ok {
 
- 				if t.CustomFields == nil {
 
- 					customFields := make(map[string]any)
 
- 					t.CustomFields = &customFields
 
- 				}
 
- 				logger.Debug(logSender, "", "custom field %#v found in token claims", field)
 
- 				(*t.CustomFields)[field] = val
 
- 			} else {
 
- 				logger.Info(logSender, "", "custom field %#v not found in token claims", field)
 
- 			}
 
- 		}
 
- 	}
 
- 	sid, ok := claims["sid"].(string)
 
- 	if ok {
 
- 		t.SessionID = sid
 
- 	}
 
- 	return nil
 
- }
 
- func (t *oidcToken) getRoleFromField(claims map[string]any, roleField string) {
 
- 	if roleField != "" {
 
- 		role, ok := claims[roleField]
 
- 		if ok {
 
- 			t.Role = role
 
- 			return
 
- 		}
 
- 		if !strings.Contains(roleField, ".") {
 
- 			return
 
- 		}
 
- 		getStructValue := func(outer any, field string) (any, bool) {
 
- 			switch val := outer.(type) {
 
- 			case map[string]any:
 
- 				res, ok := val[field]
 
- 				return res, ok
 
- 			}
 
- 			return nil, false
 
- 		}
 
- 		for idx, field := range strings.Split(roleField, ".") {
 
- 			if idx == 0 {
 
- 				role, ok = getStructValue(claims, field)
 
- 			} else {
 
- 				role, ok = getStructValue(role, field)
 
- 			}
 
- 			if !ok {
 
- 				return
 
- 			}
 
- 		}
 
- 		t.Role = role
 
- 	}
 
- }
 
- func (t *oidcToken) isAdmin() bool {
 
- 	switch v := t.Role.(type) {
 
- 	case string:
 
- 		return v == adminRoleFieldValue
 
- 	case []any:
 
- 		for _, s := range v {
 
- 			if val, ok := s.(string); ok && val == adminRoleFieldValue {
 
- 				return true
 
- 			}
 
- 		}
 
- 		return false
 
- 	default:
 
- 		return false
 
- 	}
 
- }
 
- func (t *oidcToken) isExpired() bool {
 
- 	if t.ExpiresAt == 0 {
 
- 		return false
 
- 	}
 
- 	return t.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())
 
- }
 
- 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")
 
- 	}
 
- 	oauth2Token := oauth2.Token{
 
- 		AccessToken:  t.AccessToken,
 
- 		TokenType:    t.TokenType,
 
- 		RefreshToken: t.RefreshToken,
 
- 	}
 
- 	if t.ExpiresAt > 0 {
 
- 		oauth2Token.Expiry = util.GetTimeFromMsecSinceEpoch(t.ExpiresAt)
 
- 	}
 
- 	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
 
- 	defer cancel()
 
- 	newToken, err := config.TokenSource(ctx, &oauth2Token).Token()
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "unable to refresh token for cookie %#v: %v", t.Cookie, err)
 
- 		return err
 
- 	}
 
- 	rawIDToken, ok := newToken.Extra("id_token").(string)
 
- 	if !ok {
 
- 		logger.Debug(logSender, "", "the refreshed token has no id token, cookie %#v", t.Cookie)
 
- 		return errors.New("the refreshed token has no id token")
 
- 	}
 
- 	t.AccessToken = newToken.AccessToken
 
- 	t.TokenType = newToken.TokenType
 
- 	t.RefreshToken = newToken.RefreshToken
 
- 	t.IDToken = rawIDToken
 
- 	if !newToken.Expiry.IsZero() {
 
- 		t.ExpiresAt = util.GetTimeAsMsSinceEpoch(newToken.Expiry)
 
- 	} else {
 
- 		t.ExpiresAt = 0
 
- 	}
 
- 	idToken, err := verifier.Verify(ctx, rawIDToken)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %#v: %v", t.Cookie, err)
 
- 		return err
 
- 	}
 
- 	if idToken.Nonce != t.Nonce {
 
- 		logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %#v: nonce mismatch", t.Cookie)
 
- 		return errors.New("the refreshed token nonce mismatch")
 
- 	}
 
- 	claims := make(map[string]any)
 
- 	err = idToken.Claims(&claims)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "unable to get refreshed id token claims for cookie %#v: %v", t.Cookie, err)
 
- 		return err
 
- 	}
 
- 	sid, ok := claims["sid"].(string)
 
- 	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.TokenRole = admin.Role
 
- 		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
 
- 	t.TokenRole = user.Role
 
- 	return nil
 
- }
 
- func (t *oidcToken) getUser(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.TokenRole = admin.Role
 
- 		t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
 
- 		dataprovider.UpdateAdminLastLogin(&admin)
 
- 		return nil
 
- 	}
 
- 	ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
 
- 	user, err := dataprovider.GetUserAfterIDPAuth(t.Username, ipAddr, common.ProtocolOIDC, t.CustomFields)
 
- 	if err != nil {
 
- 		return err
 
- 	}
 
- 	if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolOIDC); err != nil {
 
- 		updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
 
- 		return fmt.Errorf("access denied by post connect hook: %w", err)
 
- 	}
 
- 	if err := user.CheckLoginConditions(); err != nil {
 
- 		updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
 
- 		return err
 
- 	}
 
- 	connectionID := fmt.Sprintf("%v_%v", common.ProtocolOIDC, xid.New().String())
 
- 	if err := checkHTTPClientUser(&user, r, connectionID, true); err != nil {
 
- 		updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
 
- 		return err
 
- 	}
 
- 	defer user.CloseFs() //nolint:errcheck
 
- 	err = user.CheckFsRoot(connectionID)
 
- 	if err != nil {
 
- 		logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
 
- 		updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, common.ErrInternalFailure)
 
- 		return err
 
- 	}
 
- 	updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, nil)
 
- 	dataprovider.UpdateLastLogin(&user)
 
- 	t.Permissions = user.Filters.WebClient
 
- 	t.TokenRole = user.Role
 
- 	return nil
 
- }
 
- func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request, isAdmin bool) (oidcToken, error) {
 
- 	doRedirect := func() {
 
- 		removeOIDCCookie(w, r)
 
- 		if isAdmin {
 
- 			http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
 
- 			return
 
- 		}
 
- 		http.Redirect(w, r, webClientLoginPath, http.StatusFound)
 
- 	}
 
- 	cookie, err := r.Cookie(oidcCookieKey)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "no oidc cookie, redirecting to login page")
 
- 		doRedirect()
 
- 		return oidcToken{}, errInvalidToken
 
- 	}
 
- 	token, err := oidcMgr.getToken(cookie.Value)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "error getting oidc token associated with cookie %#v: %v", cookie.Value, err)
 
- 		doRedirect()
 
- 		return oidcToken{}, errInvalidToken
 
- 	}
 
- 	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, r); err != nil {
 
- 			setFlashMessage(w, r, "Your OpenID token is expired, please log-in again")
 
- 			doRedirect()
 
- 			return oidcToken{}, errInvalidToken
 
- 		}
 
- 	} else {
 
- 		oidcMgr.updateTokenUsage(token)
 
- 	}
 
- 	if isAdmin {
 
- 		if !token.isAdmin() {
 
- 			logger.Debug(logSender, "", "oidc token associated with cookie %#v is not valid for admin users", token.Cookie)
 
- 			setFlashMessage(w, r, "Your OpenID token is not valid for the SFTPGo Web Admin UI. Please logout from your OpenID server and log-in as an SFTPGo admin")
 
- 			doRedirect()
 
- 			return oidcToken{}, errInvalidToken
 
- 		}
 
- 		return token, nil
 
- 	}
 
- 	if token.isAdmin() {
 
- 		logger.Debug(logSender, "", "oidc token associated with cookie %#v is valid for admin users", token.Cookie)
 
- 		setFlashMessage(w, r, "Your OpenID token is not valid for the SFTPGo Web Client UI. Please logout from your OpenID server and log-in as an SFTPGo user")
 
- 		doRedirect()
 
- 		return oidcToken{}, errInvalidToken
 
- 	}
 
- 	return token, nil
 
- }
 
- func (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next http.Handler) http.Handler {
 
- 	return func(next http.Handler) http.Handler {
 
- 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 
- 			if canSkipOIDCValidation(r) {
 
- 				next.ServeHTTP(w, r)
 
- 				return
 
- 			}
 
- 			token, err := s.validateOIDCToken(w, r, audience == tokenAudienceWebAdmin)
 
- 			if err != nil {
 
- 				return
 
- 			}
 
- 			jwtTokenClaims := jwtTokenClaims{
 
- 				Username:             token.Username,
 
- 				Permissions:          token.Permissions,
 
- 				Role:                 token.TokenRole,
 
- 				HideUserPageSections: token.HideUserPageSections,
 
- 			}
 
- 			_, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience, util.GetIPFromRemoteAddress(r.RemoteAddr))
 
- 			if err != nil {
 
- 				setFlashMessage(w, r, "Unable to create cookie")
 
- 				if audience == tokenAudienceWebAdmin {
 
- 					http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
 
- 				} else {
 
- 					http.Redirect(w, r, webClientLoginPath, http.StatusFound)
 
- 				}
 
- 				return
 
- 			}
 
- 			ctx := context.WithValue(r.Context(), oidcTokenKey, token.Cookie)
 
- 			ctx = context.WithValue(ctx, oidcGeneratedToken, tokenString)
 
- 			next.ServeHTTP(w, r.WithContext(ctx))
 
- 		})
 
- 	}
 
- }
 
- func (s *httpdServer) handleWebAdminOIDCLogin(w http.ResponseWriter, r *http.Request) {
 
- 	s.oidcLoginRedirect(w, r, tokenAudienceWebAdmin)
 
- }
 
- func (s *httpdServer) handleWebClientOIDCLogin(w http.ResponseWriter, r *http.Request) {
 
- 	s.oidcLoginRedirect(w, r, tokenAudienceWebClient)
 
- }
 
- func (s *httpdServer) oidcLoginRedirect(w http.ResponseWriter, r *http.Request, audience tokenAudience) {
 
- 	pendingAuth := newOIDCPendingAuth(audience)
 
- 	oidcMgr.addPendingAuth(pendingAuth)
 
- 	http.Redirect(w, r, s.binding.OIDC.oauth2Config.AuthCodeURL(pendingAuth.State,
 
- 		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)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "oidc authentication state did not match")
 
- 		s.renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match",
 
- 			http.StatusBadRequest, nil, "")
 
- 		return
 
- 	}
 
- 	oidcMgr.removePendingAuth(state)
 
- 	doRedirect := func() {
 
- 		if authReq.Audience == tokenAudienceWebAdmin {
 
- 			http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
 
- 			return
 
- 		}
 
- 		http.Redirect(w, r, webClientLoginPath, http.StatusFound)
 
- 	}
 
- 	doLogout := func(rawIDToken string) {
 
- 		s.logoutFromOIDCOP(rawIDToken)
 
- 	}
 
- 	ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
 
- 	defer cancel()
 
- 	oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "failed to exchange oidc token: %v", err)
 
- 		setFlashMessage(w, r, "Failed to exchange OpenID token")
 
- 		doRedirect()
 
- 		return
 
- 	}
 
- 	rawIDToken, ok := oauth2Token.Extra("id_token").(string)
 
- 	if !ok {
 
- 		logger.Debug(logSender, "", "no id_token field in OAuth2 OpenID token")
 
- 		setFlashMessage(w, r, "No id_token field in OAuth2 OpenID token")
 
- 		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)
 
- 		setFlashMessage(w, r, "Failed to verify OpenID token")
 
- 		doRedirect()
 
- 		doLogout(rawIDToken)
 
- 		return
 
- 	}
 
- 	if idToken.Nonce != authReq.Nonce {
 
- 		logger.Debug(logSender, "", "oidc authentication nonce did not match")
 
- 		setFlashMessage(w, r, "OpenID authentication nonce did not match")
 
- 		doRedirect()
 
- 		doLogout(rawIDToken)
 
- 		return
 
- 	}
 
- 	claims := make(map[string]any)
 
- 	err = idToken.Claims(&claims)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "unable to get oidc token claims: %v", err)
 
- 		setFlashMessage(w, r, "Unable to get OpenID token claims")
 
- 		doRedirect()
 
- 		doLogout(rawIDToken)
 
- 		return
 
- 	}
 
- 	s.debugTokenClaims(claims, rawIDToken)
 
- 	token := oidcToken{
 
- 		AccessToken:  oauth2Token.AccessToken,
 
- 		TokenType:    oauth2Token.TokenType,
 
- 		RefreshToken: oauth2Token.RefreshToken,
 
- 		IDToken:      rawIDToken,
 
- 		Nonce:        idToken.Nonce,
 
- 		Cookie:       xid.New().String(),
 
- 	}
 
- 	if !oauth2Token.Expiry.IsZero() {
 
- 		token.ExpiresAt = util.GetTimeAsMsSinceEpoch(oauth2Token.Expiry)
 
- 	}
 
- 	err = token.parseClaims(claims, s.binding.OIDC.UsernameField, s.binding.OIDC.RoleField,
 
- 		s.binding.OIDC.CustomFields, s.binding.OIDC.getForcedRole(authReq.Audience))
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "unable to parse oidc token claims: %v", err)
 
- 		setFlashMessage(w, r, fmt.Sprintf("Unable to parse OpenID token claims: %v", err))
 
- 		doRedirect()
 
- 		doLogout(rawIDToken)
 
- 		return
 
- 	}
 
- 	switch authReq.Audience {
 
- 	case tokenAudienceWebAdmin:
 
- 		if !token.isAdmin() {
 
- 			logger.Debug(logSender, "", "wrong oidc token role, the mapped user is not an SFTPGo admin")
 
- 			setFlashMessage(w, r, "Wrong OpenID role, the logged in user is not an SFTPGo admin")
 
- 			doRedirect()
 
- 			doLogout(rawIDToken)
 
- 			return
 
- 		}
 
- 	case tokenAudienceWebClient:
 
- 		if token.isAdmin() {
 
- 			logger.Debug(logSender, "", "wrong oidc token role, the mapped user is an SFTPGo admin")
 
- 			setFlashMessage(w, r, "Wrong OpenID role, the logged in user is an SFTPGo admin")
 
- 			doRedirect()
 
- 			doLogout(rawIDToken)
 
- 			return
 
- 		}
 
- 	}
 
- 	err = token.getUser(r)
 
- 	if err != nil {
 
- 		logger.Debug(logSender, "", "unable to get the sftpgo user associated with oidc token: %v", err)
 
- 		setFlashMessage(w, r, "Unable to get the user associated with the OpenID token")
 
- 		doRedirect()
 
- 		doLogout(rawIDToken)
 
- 		return
 
- 	}
 
- 	loginOIDCUser(w, r, token)
 
- }
 
- func loginOIDCUser(w http.ResponseWriter, r *http.Request, token oidcToken) {
 
- 	oidcMgr.addToken(token)
 
- 	cookie := http.Cookie{
 
- 		Name:     oidcCookieKey,
 
- 		Value:    token.Cookie,
 
- 		Path:     "/",
 
- 		HttpOnly: true,
 
- 		Secure:   isTLS(r),
 
- 		SameSite: http.SameSiteLaxMode,
 
- 	}
 
- 	// we don't set a cookie expiration so we can refresh the token without setting a new cookie
 
- 	// the cookie will be invalidated on browser close
 
- 	http.SetCookie(w, &cookie)
 
- 	if token.isAdmin() {
 
- 		http.Redirect(w, r, webUsersPath, http.StatusFound)
 
- 		return
 
- 	}
 
- 	http.Redirect(w, r, webClientFilesPath, http.StatusFound)
 
- }
 
- func (s *httpdServer) logoutOIDCUser(w http.ResponseWriter, r *http.Request) {
 
- 	if oidcKey, ok := r.Context().Value(oidcTokenKey).(string); ok {
 
- 		removeOIDCCookie(w, r)
 
- 		token, err := oidcMgr.getToken(oidcKey)
 
- 		if err == nil {
 
- 			s.logoutFromOIDCOP(token.IDToken)
 
- 		}
 
- 		oidcMgr.removeToken(oidcKey)
 
- 	}
 
- }
 
- func (s *httpdServer) logoutFromOIDCOP(idToken string) {
 
- 	if s.binding.OIDC.providerLogoutURL == "" {
 
- 		logger.Debug(logSender, "", "oidc: provider logout URL not set, unable to logout from the OP")
 
- 		return
 
- 	}
 
- 	go s.doOIDCFromLogout(idToken)
 
- }
 
- func (s *httpdServer) doOIDCFromLogout(idToken string) {
 
- 	logoutURL, err := url.Parse(s.binding.OIDC.providerLogoutURL)
 
- 	if err != nil {
 
- 		logger.Warn(logSender, "", "oidc: unable to parse logout URL: %v", err)
 
- 		return
 
- 	}
 
- 	query := logoutURL.Query()
 
- 	if idToken != "" {
 
- 		query.Set("id_token_hint", idToken)
 
- 	}
 
- 	logoutURL.RawQuery = query.Encode()
 
- 	resp, err := httpclient.RetryableGet(logoutURL.String())
 
- 	if err != nil {
 
- 		logger.Warn(logSender, "", "oidc: error calling logout URL %#v: %v", logoutURL.String(), err)
 
- 		return
 
- 	}
 
- 	defer resp.Body.Close()
 
- 	logger.Debug(logSender, "", "oidc: logout url response code %v", resp.StatusCode)
 
- }
 
- func removeOIDCCookie(w http.ResponseWriter, r *http.Request) {
 
- 	http.SetCookie(w, &http.Cookie{
 
- 		Name:     oidcCookieKey,
 
- 		Value:    "",
 
- 		Path:     "/",
 
- 		Expires:  time.Unix(0, 0),
 
- 		MaxAge:   -1,
 
- 		HttpOnly: true,
 
- 		Secure:   isTLS(r),
 
- 		SameSite: http.SameSiteLaxMode,
 
- 	})
 
- }
 
- // canSkipOIDCValidation returns true if there is no OIDC cookie but a jwt cookie is set
 
- // and so we check if the user is logged in using a built-in user
 
- func canSkipOIDCValidation(r *http.Request) bool {
 
- 	_, err := r.Cookie(oidcCookieKey)
 
- 	if err != nil {
 
- 		_, err = r.Cookie(jwtCookieKey)
 
- 		return err == nil
 
- 	}
 
- 	return false
 
- }
 
- func isLoggedInWithOIDC(r *http.Request) bool {
 
- 	_, ok := r.Context().Value(oidcTokenKey).(string)
 
- 	return ok
 
- }
 
 
  |