oidc.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package httpd
  15. import (
  16. "context"
  17. "encoding/hex"
  18. "errors"
  19. "fmt"
  20. "net/http"
  21. "net/url"
  22. "slices"
  23. "strings"
  24. "time"
  25. "github.com/coreos/go-oidc/v3/oidc"
  26. "github.com/rs/xid"
  27. "golang.org/x/oauth2"
  28. "github.com/drakkan/sftpgo/v2/internal/common"
  29. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  30. "github.com/drakkan/sftpgo/v2/internal/httpclient"
  31. "github.com/drakkan/sftpgo/v2/internal/logger"
  32. "github.com/drakkan/sftpgo/v2/internal/util"
  33. )
  34. const (
  35. oidcCookieKey = "oidc"
  36. adminRoleFieldValue = "admin"
  37. authStateValidity = 1 * 60 * 1000 // 1 minute
  38. tokenUpdateInterval = 3 * 60 * 1000 // 3 minutes
  39. tokenDeleteInterval = 2 * 3600 * 1000 // 2 hours
  40. )
  41. var (
  42. oidcTokenKey = &contextKey{"OIDC token key"}
  43. oidcGeneratedToken = &contextKey{"OIDC generated token"}
  44. )
  45. // OAuth2Config defines an interface for OAuth2 methods, so we can mock them
  46. type OAuth2Config interface {
  47. AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
  48. Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
  49. TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
  50. }
  51. // OIDCTokenVerifier defines an interface for OpenID token verifier, so we can mock them
  52. type OIDCTokenVerifier interface {
  53. Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)
  54. }
  55. // OIDC defines the OpenID Connect configuration
  56. type OIDC struct {
  57. // ClientID is the application's ID
  58. ClientID string `json:"client_id" mapstructure:"client_id"`
  59. // ClientSecret is the application's secret
  60. ClientSecret string `json:"client_secret" mapstructure:"client_secret"`
  61. ClientSecretFile string `json:"client_secret_file" mapstructure:"client_secret_file"`
  62. // ConfigURL is the identifier for the service.
  63. // SFTPGo will try to retrieve the provider configuration on startup and then
  64. // will refuse to start if it fails to connect to the specified URL
  65. ConfigURL string `json:"config_url" mapstructure:"config_url"`
  66. // RedirectBaseURL is the base URL to redirect to after OpenID authentication.
  67. // The suffix "/web/oidc/redirect" will be added to this base URL, adding also the
  68. // "web_root" if configured
  69. RedirectBaseURL string `json:"redirect_base_url" mapstructure:"redirect_base_url"`
  70. // ID token claims field to map to the SFTPGo username
  71. UsernameField string `json:"username_field" mapstructure:"username_field"`
  72. // Optional ID token claims field to map to a SFTPGo role.
  73. // If the defined ID token claims field is set to "admin" the authenticated user
  74. // is mapped to an SFTPGo admin.
  75. // You don't need to specify this field if you want to use OpenID only for the
  76. // Web Client UI
  77. RoleField string `json:"role_field" mapstructure:"role_field"`
  78. // If set, the `RoleField` is ignored and the SFTPGo role is assumed based on
  79. // the login link used
  80. ImplicitRoles bool `json:"implicit_roles" mapstructure:"implicit_roles"`
  81. // Scopes required by the OAuth provider to retrieve information about the authenticated user.
  82. // The "openid" scope is required.
  83. // Refer to your OAuth provider documentation for more information about this
  84. Scopes []string `json:"scopes" mapstructure:"scopes"`
  85. // Custom token claims fields to pass to the pre-login hook
  86. CustomFields []string `json:"custom_fields" mapstructure:"custom_fields"`
  87. // InsecureSkipSignatureCheck causes SFTPGo to skip JWT signature validation.
  88. // It's intended for special cases where providers, such as Azure, use the "none"
  89. // algorithm. Skipping the signature validation can cause security issues
  90. InsecureSkipSignatureCheck bool `json:"insecure_skip_signature_check" mapstructure:"insecure_skip_signature_check"`
  91. // Debug enables the OIDC debug mode. In debug mode, the received id_token will be logged
  92. // at the debug level
  93. Debug bool `json:"debug" mapstructure:"debug"`
  94. provider *oidc.Provider
  95. verifier OIDCTokenVerifier
  96. providerLogoutURL string
  97. oauth2Config OAuth2Config
  98. }
  99. func (o *OIDC) isEnabled() bool {
  100. return o.provider != nil
  101. }
  102. func (o *OIDC) hasRoles() bool {
  103. return o.isEnabled() && (o.RoleField != "" || o.ImplicitRoles)
  104. }
  105. func (o *OIDC) getForcedRole(audience string) string {
  106. if !o.ImplicitRoles {
  107. return ""
  108. }
  109. if audience == tokenAudienceWebAdmin {
  110. return adminRoleFieldValue
  111. }
  112. return ""
  113. }
  114. func (o *OIDC) getRedirectURL() string {
  115. url := o.RedirectBaseURL
  116. if strings.HasSuffix(o.RedirectBaseURL, "/") {
  117. url = strings.TrimSuffix(o.RedirectBaseURL, "/")
  118. }
  119. url += webOIDCRedirectPath
  120. logger.Debug(logSender, "", "oidc redirect URL: %q", url)
  121. return url
  122. }
  123. func (o *OIDC) initialize() error {
  124. if o.ConfigURL == "" {
  125. return nil
  126. }
  127. if o.UsernameField == "" {
  128. return errors.New("oidc: username field cannot be empty")
  129. }
  130. if o.RedirectBaseURL == "" {
  131. return errors.New("oidc: redirect base URL cannot be empty")
  132. }
  133. if !slices.Contains(o.Scopes, oidc.ScopeOpenID) {
  134. return fmt.Errorf("oidc: required scope %q is not set", oidc.ScopeOpenID)
  135. }
  136. if o.ClientSecretFile != "" {
  137. secret, err := util.ReadConfigFromFile(o.ClientSecretFile, configurationDir)
  138. if err != nil {
  139. return err
  140. }
  141. o.ClientSecret = secret
  142. }
  143. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  144. defer cancel()
  145. provider, err := oidc.NewProvider(ctx, o.ConfigURL)
  146. if err != nil {
  147. return fmt.Errorf("oidc: unable to initialize provider for URL %q: %w", o.ConfigURL, err)
  148. }
  149. claims := make(map[string]any)
  150. // we cannot get an error here because the response body was already parsed as JSON
  151. // on provider creation
  152. provider.Claims(&claims) //nolint:errcheck
  153. endSessionEndPoint, ok := claims["end_session_endpoint"]
  154. if ok {
  155. if val, ok := endSessionEndPoint.(string); ok {
  156. o.providerLogoutURL = val
  157. logger.Debug(logSender, "", "oidc end session endpoint %q", o.providerLogoutURL)
  158. }
  159. }
  160. o.provider = provider
  161. o.verifier = nil
  162. o.oauth2Config = &oauth2.Config{
  163. ClientID: o.ClientID,
  164. ClientSecret: o.ClientSecret,
  165. Endpoint: o.provider.Endpoint(),
  166. RedirectURL: o.getRedirectURL(),
  167. Scopes: o.Scopes,
  168. }
  169. return nil
  170. }
  171. func (o *OIDC) getVerifier(ctx context.Context) OIDCTokenVerifier {
  172. if o.verifier != nil {
  173. return o.verifier
  174. }
  175. return o.provider.VerifierContext(ctx, &oidc.Config{
  176. ClientID: o.ClientID,
  177. InsecureSkipSignatureCheck: o.InsecureSkipSignatureCheck,
  178. })
  179. }
  180. type oidcPendingAuth struct {
  181. State string `json:"state"`
  182. Nonce string `json:"nonce"`
  183. Audience tokenAudience `json:"audience"`
  184. IssuedAt int64 `json:"issued_at"`
  185. }
  186. func newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {
  187. return oidcPendingAuth{
  188. State: xid.New().String(),
  189. Nonce: hex.EncodeToString(util.GenerateRandomBytes(20)),
  190. Audience: audience,
  191. IssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),
  192. }
  193. }
  194. type oidcToken struct {
  195. AccessToken string `json:"access_token"`
  196. TokenType string `json:"token_type,omitempty"`
  197. RefreshToken string `json:"refresh_token,omitempty"`
  198. ExpiresAt int64 `json:"expires_at,omitempty"`
  199. SessionID string `json:"session_id"`
  200. IDToken string `json:"id_token"`
  201. Nonce string `json:"nonce"`
  202. Username string `json:"username"`
  203. Permissions []string `json:"permissions"`
  204. HideUserPageSections int `json:"hide_user_page_sections,omitempty"`
  205. MustSetTwoFactorAuth bool `json:"must_set_2fa,omitempty"`
  206. MustChangePassword bool `json:"must_change_password,omitempty"`
  207. RequiredTwoFactorProtocols []string `json:"required_two_factor_protocols,omitempty"`
  208. TokenRole string `json:"token_role,omitempty"` // SFTPGo role name
  209. Role any `json:"role"` // oidc user role: SFTPGo user or admin
  210. CustomFields *map[string]any `json:"custom_fields,omitempty"`
  211. Cookie string `json:"cookie"`
  212. UsedAt int64 `json:"used_at"`
  213. }
  214. func (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField string, customFields []string,
  215. forcedRole string,
  216. ) error {
  217. getClaimsFields := func() []string {
  218. keys := make([]string, 0, len(claims))
  219. for k := range claims {
  220. keys = append(keys, k)
  221. }
  222. return keys
  223. }
  224. var username string
  225. val, ok := getOIDCFieldFromClaims(claims, usernameField)
  226. if ok {
  227. username, ok = val.(string)
  228. }
  229. if !ok || username == "" {
  230. logger.Warn(logSender, "", "username field %q not found, empty or not a string, claims fields: %+v",
  231. usernameField, getClaimsFields())
  232. return errors.New("no username field")
  233. }
  234. t.Username = username
  235. if forcedRole != "" {
  236. t.Role = forcedRole
  237. } else {
  238. t.getRoleFromField(claims, roleField)
  239. }
  240. t.CustomFields = nil
  241. if len(customFields) > 0 {
  242. for _, field := range customFields {
  243. if val, ok := getOIDCFieldFromClaims(claims, field); ok {
  244. if t.CustomFields == nil {
  245. customFields := make(map[string]any)
  246. t.CustomFields = &customFields
  247. }
  248. logger.Debug(logSender, "", "custom field %q found in token claims", field)
  249. (*t.CustomFields)[field] = val
  250. } else {
  251. logger.Info(logSender, "", "custom field %q not found in token claims", field)
  252. }
  253. }
  254. }
  255. sid, ok := claims["sid"].(string)
  256. if ok {
  257. t.SessionID = sid
  258. }
  259. return nil
  260. }
  261. func (t *oidcToken) getRoleFromField(claims map[string]any, roleField string) {
  262. role, ok := getOIDCFieldFromClaims(claims, roleField)
  263. if ok {
  264. t.Role = role
  265. }
  266. }
  267. func (t *oidcToken) isAdmin() bool {
  268. switch v := t.Role.(type) {
  269. case string:
  270. return v == adminRoleFieldValue
  271. case []any:
  272. for _, s := range v {
  273. if val, ok := s.(string); ok && val == adminRoleFieldValue {
  274. return true
  275. }
  276. }
  277. return false
  278. default:
  279. return false
  280. }
  281. }
  282. func (t *oidcToken) isExpired() bool {
  283. if t.ExpiresAt == 0 {
  284. return false
  285. }
  286. return t.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())
  287. }
  288. func (t *oidcToken) refresh(ctx context.Context, config OAuth2Config, verifier OIDCTokenVerifier, r *http.Request) error {
  289. if t.RefreshToken == "" {
  290. logger.Debug(logSender, "", "refresh token not set, unable to refresh cookie %q", t.Cookie)
  291. return errors.New("refresh token not set")
  292. }
  293. oauth2Token := oauth2.Token{
  294. AccessToken: t.AccessToken,
  295. TokenType: t.TokenType,
  296. RefreshToken: t.RefreshToken,
  297. }
  298. if t.ExpiresAt > 0 {
  299. oauth2Token.Expiry = util.GetTimeFromMsecSinceEpoch(t.ExpiresAt)
  300. }
  301. newToken, err := config.TokenSource(ctx, &oauth2Token).Token()
  302. if err != nil {
  303. logger.Debug(logSender, "", "unable to refresh token for cookie %q: %v", t.Cookie, err)
  304. return err
  305. }
  306. rawIDToken, ok := newToken.Extra("id_token").(string)
  307. if !ok {
  308. logger.Debug(logSender, "", "the refreshed token has no id token, cookie %q", t.Cookie)
  309. return errors.New("the refreshed token has no id token")
  310. }
  311. t.AccessToken = newToken.AccessToken
  312. t.TokenType = newToken.TokenType
  313. t.RefreshToken = newToken.RefreshToken
  314. t.IDToken = rawIDToken
  315. if !newToken.Expiry.IsZero() {
  316. t.ExpiresAt = util.GetTimeAsMsSinceEpoch(newToken.Expiry)
  317. } else {
  318. t.ExpiresAt = 0
  319. }
  320. idToken, err := verifier.Verify(ctx, rawIDToken)
  321. if err != nil {
  322. logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %q: %v", t.Cookie, err)
  323. return err
  324. }
  325. if idToken.Nonce != "" && idToken.Nonce != t.Nonce {
  326. logger.Warn(logSender, "", "unable to verify refreshed id token for cookie %q: nonce mismatch, expected: %q, actual: %q",
  327. t.Cookie, t.Nonce, idToken.Nonce)
  328. return errors.New("the refreshed token nonce mismatch")
  329. }
  330. claims := make(map[string]any)
  331. err = idToken.Claims(&claims)
  332. if err != nil {
  333. logger.Warn(logSender, "", "unable to get refreshed id token claims for cookie %q: %v", t.Cookie, err)
  334. return err
  335. }
  336. sid, ok := claims["sid"].(string)
  337. if ok {
  338. t.SessionID = sid
  339. }
  340. err = t.refreshUser(r)
  341. if err != nil {
  342. logger.Debug(logSender, "", "unable to refresh user after token refresh for cookie %q: %v", t.Cookie, err)
  343. return err
  344. }
  345. logger.Debug(logSender, "", "oidc token refreshed for user %q, cookie %q", t.Username, t.Cookie)
  346. oidcMgr.addToken(*t)
  347. return nil
  348. }
  349. func (t *oidcToken) refreshUser(r *http.Request) error {
  350. if t.isAdmin() {
  351. admin, err := dataprovider.AdminExists(t.Username)
  352. if err != nil {
  353. return err
  354. }
  355. if err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
  356. return err
  357. }
  358. t.Permissions = admin.Permissions
  359. t.TokenRole = admin.Role
  360. t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
  361. return nil
  362. }
  363. user, err := dataprovider.GetUserWithGroupSettings(t.Username, "")
  364. if err != nil {
  365. return err
  366. }
  367. if err := user.CheckLoginConditions(); err != nil {
  368. return err
  369. }
  370. if err := checkHTTPClientUser(&user, r, xid.New().String(), true); err != nil {
  371. return err
  372. }
  373. t.Permissions = user.Filters.WebClient
  374. t.TokenRole = user.Role
  375. t.MustSetTwoFactorAuth = user.MustSetSecondFactor()
  376. t.MustChangePassword = user.MustChangePassword()
  377. t.RequiredTwoFactorProtocols = user.Filters.TwoFactorAuthProtocols
  378. return nil
  379. }
  380. func (t *oidcToken) getUser(r *http.Request) error {
  381. ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
  382. params := common.EventParams{
  383. Name: t.Username,
  384. IP: ipAddr,
  385. Protocol: common.ProtocolOIDC,
  386. Timestamp: time.Now(),
  387. Status: 1,
  388. }
  389. if t.isAdmin() {
  390. params.Event = common.IDPLoginAdmin
  391. _, admin, err := common.HandleIDPLoginEvent(params, t.CustomFields)
  392. if err != nil {
  393. return err
  394. }
  395. if admin == nil {
  396. a, err := dataprovider.AdminExists(t.Username)
  397. if err != nil {
  398. return err
  399. }
  400. admin = &a
  401. }
  402. if err := admin.CanLogin(ipAddr); err != nil {
  403. return err
  404. }
  405. t.Permissions = admin.Permissions
  406. t.TokenRole = admin.Role
  407. t.HideUserPageSections = admin.Filters.Preferences.HideUserPageSections
  408. dataprovider.UpdateAdminLastLogin(admin)
  409. common.DelayLogin(nil)
  410. return nil
  411. }
  412. params.Event = common.IDPLoginUser
  413. user, _, err := common.HandleIDPLoginEvent(params, t.CustomFields)
  414. if err != nil {
  415. return err
  416. }
  417. if user == nil {
  418. u, err := dataprovider.GetUserAfterIDPAuth(t.Username, ipAddr, common.ProtocolOIDC, t.CustomFields)
  419. if err != nil {
  420. return err
  421. }
  422. user = &u
  423. }
  424. if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolOIDC); err != nil {
  425. updateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, err)
  426. return fmt.Errorf("access denied: %w", err)
  427. }
  428. if err := user.CheckLoginConditions(); err != nil {
  429. updateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, err)
  430. return err
  431. }
  432. connectionID := fmt.Sprintf("%s_%s", common.ProtocolOIDC, xid.New().String())
  433. if err := checkHTTPClientUser(user, r, connectionID, true); err != nil {
  434. updateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, err)
  435. return err
  436. }
  437. defer user.CloseFs() //nolint:errcheck
  438. err = user.CheckFsRoot(connectionID)
  439. if err != nil {
  440. logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
  441. updateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, common.ErrInternalFailure)
  442. return err
  443. }
  444. updateLoginMetrics(user, dataprovider.LoginMethodIDP, ipAddr, nil)
  445. dataprovider.UpdateLastLogin(user)
  446. t.Permissions = user.Filters.WebClient
  447. t.TokenRole = user.Role
  448. t.MustSetTwoFactorAuth = user.MustSetSecondFactor()
  449. t.MustChangePassword = user.MustChangePassword()
  450. t.RequiredTwoFactorProtocols = user.Filters.TwoFactorAuthProtocols
  451. return nil
  452. }
  453. func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request, isAdmin bool) (oidcToken, error) {
  454. doRedirect := func() {
  455. removeOIDCCookie(w, r)
  456. if isAdmin {
  457. http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
  458. return
  459. }
  460. http.Redirect(w, r, webClientLoginPath, http.StatusFound)
  461. }
  462. cookie, err := r.Cookie(oidcCookieKey)
  463. if err != nil {
  464. logger.Debug(logSender, "", "no oidc cookie, redirecting to login page")
  465. doRedirect()
  466. return oidcToken{}, errInvalidToken
  467. }
  468. token, err := oidcMgr.getToken(cookie.Value)
  469. if err != nil {
  470. logger.Debug(logSender, "", "error getting oidc token associated with cookie %q: %v", cookie.Value, err)
  471. doRedirect()
  472. return oidcToken{}, errInvalidToken
  473. }
  474. if token.isExpired() {
  475. logger.Debug(logSender, "", "oidc token associated with cookie %q is expired", token.Cookie)
  476. ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
  477. defer cancel()
  478. if err = token.refresh(ctx, s.binding.OIDC.oauth2Config, s.binding.OIDC.getVerifier(ctx), r); err != nil {
  479. setFlashMessage(w, r, newFlashMessage("Your OpenID token is expired, please log-in again", util.I18nOIDCTokenExpired))
  480. doRedirect()
  481. return oidcToken{}, errInvalidToken
  482. }
  483. } else {
  484. oidcMgr.updateTokenUsage(token)
  485. }
  486. if isAdmin {
  487. if !token.isAdmin() {
  488. logger.Debug(logSender, "", "oidc token associated with cookie %q is not valid for admin users", token.Cookie)
  489. setFlashMessage(w, r, newFlashMessage(
  490. "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",
  491. util.I18nOIDCTokenInvalidAdmin,
  492. ))
  493. doRedirect()
  494. return oidcToken{}, errInvalidToken
  495. }
  496. return token, nil
  497. }
  498. if token.isAdmin() {
  499. logger.Debug(logSender, "", "oidc token associated with cookie %q is valid for admin users", token.Cookie)
  500. setFlashMessage(w, r, newFlashMessage(
  501. "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",
  502. util.I18nOIDCTokenInvalidUser,
  503. ))
  504. doRedirect()
  505. return oidcToken{}, errInvalidToken
  506. }
  507. return token, nil
  508. }
  509. func (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next http.Handler) http.Handler {
  510. return func(next http.Handler) http.Handler {
  511. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  512. if canSkipOIDCValidation(r) {
  513. next.ServeHTTP(w, r)
  514. return
  515. }
  516. token, err := s.validateOIDCToken(w, r, audience == tokenAudienceWebAdmin)
  517. if err != nil {
  518. return
  519. }
  520. jwtTokenClaims := jwtTokenClaims{
  521. JwtID: token.Cookie,
  522. Username: token.Username,
  523. Permissions: token.Permissions,
  524. Role: token.TokenRole,
  525. HideUserPageSections: token.HideUserPageSections,
  526. }
  527. if audience == tokenAudienceWebClient {
  528. jwtTokenClaims.MustSetTwoFactorAuth = token.MustSetTwoFactorAuth
  529. jwtTokenClaims.MustChangePassword = token.MustChangePassword
  530. jwtTokenClaims.RequiredTwoFactorProtocols = token.RequiredTwoFactorProtocols
  531. }
  532. _, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience, util.GetIPFromRemoteAddress(r.RemoteAddr))
  533. if err != nil {
  534. setFlashMessage(w, r, newFlashMessage("Unable to create cookie", util.I18nError500Message))
  535. if audience == tokenAudienceWebAdmin {
  536. http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
  537. } else {
  538. http.Redirect(w, r, webClientLoginPath, http.StatusFound)
  539. }
  540. return
  541. }
  542. ctx := context.WithValue(r.Context(), oidcTokenKey, token.Cookie)
  543. ctx = context.WithValue(ctx, oidcGeneratedToken, tokenString)
  544. next.ServeHTTP(w, r.WithContext(ctx))
  545. })
  546. }
  547. }
  548. func (s *httpdServer) handleWebAdminOIDCLogin(w http.ResponseWriter, r *http.Request) {
  549. s.oidcLoginRedirect(w, r, tokenAudienceWebAdmin)
  550. }
  551. func (s *httpdServer) handleWebClientOIDCLogin(w http.ResponseWriter, r *http.Request) {
  552. s.oidcLoginRedirect(w, r, tokenAudienceWebClient)
  553. }
  554. func (s *httpdServer) oidcLoginRedirect(w http.ResponseWriter, r *http.Request, audience tokenAudience) {
  555. pendingAuth := newOIDCPendingAuth(audience)
  556. oidcMgr.addPendingAuth(pendingAuth)
  557. http.Redirect(w, r, s.binding.OIDC.oauth2Config.AuthCodeURL(pendingAuth.State,
  558. oidc.Nonce(pendingAuth.Nonce)), http.StatusFound)
  559. }
  560. func (s *httpdServer) debugTokenClaims(claims map[string]any, rawIDToken string) {
  561. if s.binding.OIDC.Debug {
  562. if claims == nil {
  563. logger.Debug(logSender, "", "raw id token %q", rawIDToken)
  564. } else {
  565. logger.Debug(logSender, "", "raw id token %q, parsed claims %+v", rawIDToken, claims)
  566. }
  567. }
  568. }
  569. func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) {
  570. state := r.URL.Query().Get("state")
  571. authReq, err := oidcMgr.getPendingAuth(state)
  572. if err != nil {
  573. logger.Debug(logSender, "", "oidc authentication state did not match")
  574. oidcMgr.removePendingAuth(state)
  575. s.renderClientMessagePage(w, r, util.I18nInvalidAuthReqTitle, http.StatusBadRequest,
  576. util.NewI18nError(err, util.I18nInvalidAuth), "")
  577. return
  578. }
  579. oidcMgr.removePendingAuth(state)
  580. doRedirect := func() {
  581. if authReq.Audience == tokenAudienceWebAdmin {
  582. http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
  583. return
  584. }
  585. http.Redirect(w, r, webClientLoginPath, http.StatusFound)
  586. }
  587. doLogout := func(rawIDToken string) {
  588. s.logoutFromOIDCOP(rawIDToken)
  589. }
  590. ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
  591. defer cancel()
  592. oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
  593. if err != nil {
  594. logger.Debug(logSender, "", "failed to exchange oidc token: %v", err)
  595. setFlashMessage(w, r, newFlashMessage("Failed to exchange OpenID token", util.I18nOIDCErrTokenExchange))
  596. doRedirect()
  597. return
  598. }
  599. rawIDToken, ok := oauth2Token.Extra("id_token").(string)
  600. if !ok {
  601. logger.Debug(logSender, "", "no id_token field in OAuth2 OpenID token")
  602. setFlashMessage(w, r, newFlashMessage("No id_token field in OAuth2 OpenID token", util.I18nOIDCTokenInvalid))
  603. doRedirect()
  604. return
  605. }
  606. s.debugTokenClaims(nil, rawIDToken)
  607. idToken, err := s.binding.OIDC.getVerifier(ctx).Verify(ctx, rawIDToken)
  608. if err != nil {
  609. logger.Debug(logSender, "", "failed to verify oidc token: %v", err)
  610. setFlashMessage(w, r, newFlashMessage("Failed to verify OpenID token", util.I18nOIDCTokenInvalid))
  611. doRedirect()
  612. doLogout(rawIDToken)
  613. return
  614. }
  615. if idToken.Nonce != authReq.Nonce {
  616. logger.Debug(logSender, "", "oidc authentication nonce did not match")
  617. setFlashMessage(w, r, newFlashMessage("OpenID authentication nonce did not match", util.I18nOIDCTokenInvalid))
  618. doRedirect()
  619. doLogout(rawIDToken)
  620. return
  621. }
  622. claims := make(map[string]any)
  623. err = idToken.Claims(&claims)
  624. if err != nil {
  625. logger.Debug(logSender, "", "unable to get oidc token claims: %v", err)
  626. setFlashMessage(w, r, newFlashMessage("Unable to get OpenID token claims", util.I18nOIDCTokenInvalid))
  627. doRedirect()
  628. doLogout(rawIDToken)
  629. return
  630. }
  631. s.debugTokenClaims(claims, rawIDToken)
  632. token := oidcToken{
  633. AccessToken: oauth2Token.AccessToken,
  634. TokenType: oauth2Token.TokenType,
  635. RefreshToken: oauth2Token.RefreshToken,
  636. IDToken: rawIDToken,
  637. Nonce: idToken.Nonce,
  638. Cookie: xid.New().String(),
  639. }
  640. if !oauth2Token.Expiry.IsZero() {
  641. token.ExpiresAt = util.GetTimeAsMsSinceEpoch(oauth2Token.Expiry)
  642. }
  643. err = token.parseClaims(claims, s.binding.OIDC.UsernameField, s.binding.OIDC.RoleField,
  644. s.binding.OIDC.CustomFields, s.binding.OIDC.getForcedRole(authReq.Audience))
  645. if err != nil {
  646. logger.Debug(logSender, "", "unable to parse oidc token claims: %v", err)
  647. setFlashMessage(w, r, newFlashMessage(fmt.Sprintf("Unable to parse OpenID token claims: %v", err), util.I18nOIDCTokenInvalid))
  648. doRedirect()
  649. doLogout(rawIDToken)
  650. return
  651. }
  652. switch authReq.Audience {
  653. case tokenAudienceWebAdmin:
  654. if !token.isAdmin() {
  655. logger.Debug(logSender, "", "wrong oidc token role, the mapped user is not an SFTPGo admin")
  656. setFlashMessage(w, r, newFlashMessage(
  657. "Wrong OpenID role, the logged in user is not an SFTPGo admin",
  658. util.I18nOIDCTokenInvalidRoleAdmin))
  659. doRedirect()
  660. doLogout(rawIDToken)
  661. return
  662. }
  663. case tokenAudienceWebClient:
  664. if token.isAdmin() {
  665. logger.Debug(logSender, "", "wrong oidc token role, the mapped user is an SFTPGo admin")
  666. setFlashMessage(w, r, newFlashMessage(
  667. "Wrong OpenID role, the logged in user is an SFTPGo admin",
  668. util.I18nOIDCTokenInvalidRoleUser,
  669. ))
  670. doRedirect()
  671. doLogout(rawIDToken)
  672. return
  673. }
  674. }
  675. err = token.getUser(r)
  676. if err != nil {
  677. logger.Debug(logSender, "", "unable to get the sftpgo user associated with oidc token: %v", err)
  678. setFlashMessage(w, r, newFlashMessage("Unable to get the user associated with the OpenID token", util.I18nOIDCErrGetUser))
  679. doRedirect()
  680. doLogout(rawIDToken)
  681. return
  682. }
  683. loginOIDCUser(w, r, token)
  684. }
  685. func loginOIDCUser(w http.ResponseWriter, r *http.Request, token oidcToken) {
  686. oidcMgr.addToken(token)
  687. cookie := http.Cookie{
  688. Name: oidcCookieKey,
  689. Value: token.Cookie,
  690. Path: "/",
  691. HttpOnly: true,
  692. Secure: isTLS(r),
  693. SameSite: http.SameSiteLaxMode,
  694. }
  695. // we don't set a cookie expiration so we can refresh the token without setting a new cookie
  696. // the cookie will be invalidated on browser close
  697. http.SetCookie(w, &cookie)
  698. w.Header().Add("Cache-Control", `no-cache="Set-Cookie"`)
  699. if token.isAdmin() {
  700. http.Redirect(w, r, webUsersPath, http.StatusFound)
  701. return
  702. }
  703. http.Redirect(w, r, webClientFilesPath, http.StatusFound)
  704. }
  705. func (s *httpdServer) logoutOIDCUser(w http.ResponseWriter, r *http.Request) {
  706. if oidcKey, ok := r.Context().Value(oidcTokenKey).(string); ok {
  707. removeOIDCCookie(w, r)
  708. token, err := oidcMgr.getToken(oidcKey)
  709. if err == nil {
  710. s.logoutFromOIDCOP(token.IDToken)
  711. }
  712. oidcMgr.removeToken(oidcKey)
  713. }
  714. }
  715. func (s *httpdServer) logoutFromOIDCOP(idToken string) {
  716. if s.binding.OIDC.providerLogoutURL == "" {
  717. logger.Debug(logSender, "", "oidc: provider logout URL not set, unable to logout from the OP")
  718. return
  719. }
  720. go s.doOIDCFromLogout(idToken)
  721. }
  722. func (s *httpdServer) doOIDCFromLogout(idToken string) {
  723. logoutURL, err := url.Parse(s.binding.OIDC.providerLogoutURL)
  724. if err != nil {
  725. logger.Warn(logSender, "", "oidc: unable to parse logout URL: %v", err)
  726. return
  727. }
  728. query := logoutURL.Query()
  729. if idToken != "" {
  730. query.Set("id_token_hint", idToken)
  731. }
  732. logoutURL.RawQuery = query.Encode()
  733. resp, err := httpclient.RetryableGet(logoutURL.String())
  734. if err != nil {
  735. logger.Warn(logSender, "", "oidc: error calling logout URL %q: %v", logoutURL.String(), err)
  736. return
  737. }
  738. defer resp.Body.Close()
  739. logger.Debug(logSender, "", "oidc: logout url response code %v", resp.StatusCode)
  740. }
  741. func removeOIDCCookie(w http.ResponseWriter, r *http.Request) {
  742. http.SetCookie(w, &http.Cookie{
  743. Name: oidcCookieKey,
  744. Value: "",
  745. Path: "/",
  746. Expires: time.Unix(0, 0),
  747. MaxAge: -1,
  748. HttpOnly: true,
  749. Secure: isTLS(r),
  750. SameSite: http.SameSiteLaxMode,
  751. })
  752. }
  753. // canSkipOIDCValidation returns true if there is no OIDC cookie but a jwt cookie is set
  754. // and so we check if the user is logged in using a built-in user
  755. func canSkipOIDCValidation(r *http.Request) bool {
  756. _, err := r.Cookie(oidcCookieKey)
  757. if err != nil {
  758. _, err = r.Cookie(jwtCookieKey)
  759. return err == nil
  760. }
  761. return false
  762. }
  763. func isLoggedInWithOIDC(r *http.Request) bool {
  764. _, ok := r.Context().Value(oidcTokenKey).(string)
  765. return ok
  766. }
  767. func getOIDCFieldFromClaims(claims map[string]any, fieldName string) (any, bool) {
  768. if fieldName == "" {
  769. return nil, false
  770. }
  771. val, ok := claims[fieldName]
  772. if ok {
  773. return val, true
  774. }
  775. if !strings.Contains(fieldName, ".") {
  776. return nil, false
  777. }
  778. getStructValue := func(outer any, field string) (any, bool) {
  779. switch v := outer.(type) {
  780. case map[string]any:
  781. res, ok := v[field]
  782. return res, ok
  783. }
  784. return nil, false
  785. }
  786. for idx, field := range strings.Split(fieldName, ".") {
  787. if idx == 0 {
  788. val, ok = getStructValue(claims, field)
  789. } else {
  790. val, ok = getStructValue(val, field)
  791. }
  792. if !ok {
  793. return nil, false
  794. }
  795. }
  796. return val, ok
  797. }