oidc.go 25 KB

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