oidc.go 23 KB

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