| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731 |
- package httpd
- import (
- "context"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "strings"
- "sync"
- "time"
- "github.com/coreos/go-oidc/v3/oidc"
- "github.com/rs/xid"
- "golang.org/x/oauth2"
- "github.com/drakkan/sftpgo/v2/common"
- "github.com/drakkan/sftpgo/v2/dataprovider"
- "github.com/drakkan/sftpgo/v2/httpclient"
- "github.com/drakkan/sftpgo/v2/logger"
- "github.com/drakkan/sftpgo/v2/util"
- )
- const (
- oidcCookieKey = "oidc"
- authStateValidity = 1 * 60 * 1000 // 1 minute
- tokenUpdateInterval = 3 * 60 * 1000 // 3 minutes
- tokenDeleteInterval = 2 * 3600 * 1000 // 2 hours
- )
- var (
- oidcTokenKey = &contextKey{"OIDC token key"}
- oidcGeneratedToken = &contextKey{"OIDC generated token"}
- oidcMgr *oidcManager
- )
- func init() {
- oidcMgr = &oidcManager{
- pendingAuths: make(map[string]oidcPendingAuth),
- tokens: make(map[string]oidcToken),
- lastCleanup: time.Now(),
- }
- }
- // OAuth2Config defines an interface for OAuth2 methods, so we can mock them
- type OAuth2Config interface {
- AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
- Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
- TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
- }
- // OIDCTokenVerifier defines an interface for OpenID token verifier, so we can mock them
- type OIDCTokenVerifier interface {
- Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)
- }
- // OIDC defines the OpenID Connect configuration
- type OIDC struct {
- // ClientID is the application's ID
- ClientID string `json:"client_id" mapstructure:"client_id"`
- // ClientSecret is the application's secret
- ClientSecret string `json:"client_secret" mapstructure:"client_secret"`
- // ConfigURL is the identifier for the service.
- // SFTPGo will try to retrieve the provider configuration on startup and then
- // will refuse to start if it fails to connect to the specified URL
- ConfigURL string `json:"config_url" mapstructure:"config_url"`
- // RedirectBaseURL is the base URL to redirect to after OpenID authentication.
- // The suffix "/web/oidc/redirect" will be added to this base URL, adding also the
- // "web_root" if configured
- RedirectBaseURL string `json:"redirect_base_url" mapstructure:"redirect_base_url"`
- // ID token claims field to map to the SFTPGo username
- UsernameField string `json:"username_field" mapstructure:"username_field"`
- // Optional ID token claims field to map to a SFTPGo role.
- // If the defined ID token claims field is set to "admin" the authenticated user
- // is mapped to an SFTPGo admin.
- // You don't need to specify this field if you want to use OpenID only for the
- // Web Client UI
- RoleField string `json:"role_field" mapstructure:"role_field"`
- provider *oidc.Provider
- verifier OIDCTokenVerifier
- providerLogoutURL string
- oauth2Config OAuth2Config
- }
- func (o *OIDC) isEnabled() bool {
- return o.provider != nil
- }
- func (o *OIDC) hasRoles() bool {
- return o.isEnabled() && o.RoleField != ""
- }
- func (o *OIDC) getRedirectURL() string {
- url := o.RedirectBaseURL
- if strings.HasSuffix(o.RedirectBaseURL, "/") {
- url = strings.TrimSuffix(o.RedirectBaseURL, "/")
- }
- url += webOIDCRedirectPath
- logger.Debug(logSender, "", "oidc redirect URL: %#v", url)
- return url
- }
- func (o *OIDC) initialize() error {
- if o.ConfigURL == "" {
- return nil
- }
- if o.UsernameField == "" {
- return errors.New("oidc: username field cannot be empty")
- }
- if o.RedirectBaseURL == "" {
- return errors.New("oidc: redirect base URL cannot be empty")
- }
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
- provider, err := oidc.NewProvider(ctx, o.ConfigURL)
- if err != nil {
- return fmt.Errorf("oidc: unable to initialize provider for URL %#v: %w", o.ConfigURL, err)
- }
- claims := make(map[string]interface{})
- // we cannot get an error here because the response body was already parsed as JSON
- // on provider creation
- provider.Claims(&claims) //nolint:errcheck
- endSessionEndPoint, ok := claims["end_session_endpoint"]
- if ok {
- if val, ok := endSessionEndPoint.(string); ok {
- o.providerLogoutURL = val
- logger.Debug(logSender, "", "oidc end session endpoint %#v", o.providerLogoutURL)
- }
- }
- o.provider = provider
- o.verifier = provider.Verifier(&oidc.Config{
- ClientID: o.ClientID,
- })
- o.oauth2Config = &oauth2.Config{
- ClientID: o.ClientID,
- ClientSecret: o.ClientSecret,
- Endpoint: o.provider.Endpoint(),
- RedirectURL: o.getRedirectURL(),
- Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
- }
- return nil
- }
- type oidcPendingAuth struct {
- State string
- Nonce string
- Audience tokenAudience
- IssueAt int64
- }
- func newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {
- return oidcPendingAuth{
- State: xid.New().String(),
- Nonce: xid.New().String(),
- Audience: audience,
- IssueAt: util.GetTimeAsMsSinceEpoch(time.Now()),
- }
- }
- type oidcToken struct {
- AccessToken string `json:"access_token"`
- TokenType string `json:"token_type,omitempty"`
- RefreshToken string `json:"refresh_token,omitempty"`
- ExpiresAt int64 `json:"expires_at,omitempty"`
- SessionID string `json:"session_id"`
- IDToken string `json:"id_token"`
- Nonce string `json:"nonce"`
- Username string `json:"username"`
- Permissions []string `json:"permissions"`
- Role string `json:"role"`
- Cookie string `json:"cookie"`
- UsedAt int64 `json:"used_at"`
- }
- func (t *oidcToken) parseClaims(claims map[string]interface{}, usernameField, roleField string) error {
- getClaimsFields := func() []string {
- keys := make([]string, 0, len(claims))
- for k := range claims {
- keys = append(keys, k)
- }
- return keys
- }
- username, ok := claims[usernameField].(string)
- if !ok || username == "" {
- logger.Warn(logSender, "", "username field %#v not found, claims fields: %+v", usernameField, getClaimsFields())
- return errors.New("no username field")
- }
- t.Username = username
- if roleField != "" {
- role, ok := claims[roleField].(string)
- if ok {
- t.Role = role
- }
- }
- sid, ok := claims["sid"].(string)
- if ok {
- t.SessionID = sid
- }
- return nil
- }
- func (t *oidcToken) isAdmin() bool {
- return t.Role == "admin"
- }
- func (t *oidcToken) isExpired() bool {
- if t.ExpiresAt == 0 {
- return false
- }
- return t.ExpiresAt < util.GetTimeAsMsSinceEpoch(time.Now())
- }
- func (t *oidcToken) refresh(config OAuth2Config, verifier OIDCTokenVerifier) error {
- if t.RefreshToken == "" {
- logger.Debug(logSender, "", "refresh token not set, unable to refresh cookie %#v", t.Cookie)
- return errors.New("refresh token not set")
- }
- oauth2Token := oauth2.Token{
- AccessToken: t.AccessToken,
- TokenType: t.TokenType,
- RefreshToken: t.RefreshToken,
- }
- if t.ExpiresAt > 0 {
- oauth2Token.Expiry = util.GetTimeFromMsecSinceEpoch(t.ExpiresAt)
- }
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
- defer cancel()
- newToken, err := config.TokenSource(ctx, &oauth2Token).Token()
- if err != nil {
- logger.Debug(logSender, "", "unable to refresh token for cookie %#v: %v", t.Cookie, err)
- return err
- }
- rawIDToken, ok := newToken.Extra("id_token").(string)
- if !ok {
- logger.Debug(logSender, "", "the refreshed token has no id token, cookie %#v", t.Cookie)
- return errors.New("the refreshed token has no id token")
- }
- t.AccessToken = newToken.AccessToken
- t.TokenType = newToken.TokenType
- t.RefreshToken = newToken.RefreshToken
- t.IDToken = rawIDToken
- if !newToken.Expiry.IsZero() {
- t.ExpiresAt = util.GetTimeAsMsSinceEpoch(newToken.Expiry)
- } else {
- t.ExpiresAt = 0
- }
- idToken, err := verifier.Verify(ctx, rawIDToken)
- if err != nil {
- logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %#v: %v", t.Cookie, err)
- return err
- }
- if idToken.Nonce != t.Nonce {
- logger.Debug(logSender, "", "unable to verify refreshed id token for cookie %#v: nonce mismatch", t.Cookie)
- return errors.New("the refreshed token nonce mismatch")
- }
- claims := make(map[string]interface{})
- err = idToken.Claims(&claims)
- if err != nil {
- logger.Debug(logSender, "", "unable to get refreshed id token claims for cookie %#v: %v", t.Cookie, err)
- return err
- }
- sid, ok := claims["sid"].(string)
- if ok {
- t.SessionID = sid
- }
- logger.Debug(logSender, "", "oidc token refreshed for user %#v, cookie %#v", t.Username, t.Cookie)
- oidcMgr.addToken(*t)
- return nil
- }
- func (t *oidcToken) getUser(r *http.Request) error {
- if t.isAdmin() {
- admin, err := dataprovider.AdminExists(t.Username)
- if err != nil {
- return err
- }
- if err := admin.CanLogin(util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
- return err
- }
- t.Permissions = admin.Permissions
- dataprovider.UpdateAdminLastLogin(&admin)
- return nil
- }
- ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
- user, err := dataprovider.GetUserAfterIDPAuth(t.Username, ipAddr, common.ProtocolOIDC)
- if err != nil {
- return err
- }
- if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolOIDC); err != nil {
- updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
- return fmt.Errorf("access denied by post connect hook: %w", err)
- }
- if err := user.CheckLoginConditions(); err != nil {
- updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
- return err
- }
- connectionID := fmt.Sprintf("%v_%v", common.ProtocolOIDC, xid.New().String())
- if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
- updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
- return err
- }
- defer user.CloseFs() //nolint:errcheck
- err = user.CheckFsRoot(connectionID)
- if err != nil {
- logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
- updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, common.ErrInternalFailure)
- return err
- }
- updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, nil)
- dataprovider.UpdateLastLogin(&user)
- t.Permissions = user.Filters.WebClient
- return nil
- }
- type oidcManager struct {
- authMutex sync.RWMutex
- pendingAuths map[string]oidcPendingAuth
- tokenMutex sync.RWMutex
- tokens map[string]oidcToken
- lastCleanup time.Time
- }
- func (o *oidcManager) addPendingAuth(pendingAuth oidcPendingAuth) {
- o.authMutex.Lock()
- o.pendingAuths[pendingAuth.State] = pendingAuth
- o.authMutex.Unlock()
- o.checkCleanup()
- }
- func (o *oidcManager) removePendingAuth(key string) {
- o.authMutex.Lock()
- defer o.authMutex.Unlock()
- delete(o.pendingAuths, key)
- }
- func (o *oidcManager) getPendingAuth(state string) (oidcPendingAuth, error) {
- o.authMutex.RLock()
- defer o.authMutex.RUnlock()
- authReq, ok := o.pendingAuths[state]
- if !ok {
- return oidcPendingAuth{}, errors.New("oidc: no auth request found for the specified state")
- }
- diff := util.GetTimeAsMsSinceEpoch(time.Now()) - authReq.IssueAt
- if diff > authStateValidity {
- return oidcPendingAuth{}, errors.New("oidc: auth request is too old")
- }
- return authReq, nil
- }
- func (o *oidcManager) addToken(token oidcToken) {
- o.tokenMutex.Lock()
- token.UsedAt = util.GetTimeAsMsSinceEpoch(time.Now())
- o.tokens[token.Cookie] = token
- o.tokenMutex.Unlock()
- o.checkCleanup()
- }
- func (o *oidcManager) getToken(cookie string) (oidcToken, error) {
- o.tokenMutex.RLock()
- defer o.tokenMutex.RUnlock()
- token, ok := o.tokens[cookie]
- if !ok {
- return oidcToken{}, errors.New("oidc: no token found for the specified session")
- }
- return token, nil
- }
- func (o *oidcManager) removeToken(cookie string) {
- o.tokenMutex.Lock()
- defer o.tokenMutex.Unlock()
- delete(o.tokens, cookie)
- }
- func (o *oidcManager) updateTokenUsage(token oidcToken) {
- diff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt
- if diff > tokenUpdateInterval {
- o.addToken(token)
- }
- }
- func (o *oidcManager) checkCleanup() {
- o.authMutex.RLock()
- needCleanup := o.lastCleanup.Add(20 * time.Minute).Before(time.Now())
- o.authMutex.RUnlock()
- if needCleanup {
- o.authMutex.Lock()
- o.lastCleanup = time.Now()
- o.authMutex.Unlock()
- o.cleanupAuthRequests()
- o.cleanupTokens()
- }
- }
- func (o *oidcManager) cleanupAuthRequests() {
- o.authMutex.Lock()
- defer o.authMutex.Unlock()
- for k, auth := range o.pendingAuths {
- diff := util.GetTimeAsMsSinceEpoch(time.Now()) - auth.IssueAt
- // remove old pending auth requests
- if diff < 0 || diff > authStateValidity {
- delete(o.pendingAuths, k)
- }
- }
- }
- func (o *oidcManager) cleanupTokens() {
- o.tokenMutex.Lock()
- defer o.tokenMutex.Unlock()
- for k, token := range o.tokens {
- diff := util.GetTimeAsMsSinceEpoch(time.Now()) - token.UsedAt
- // remove tokens unused from more than 1 hour
- if diff > tokenDeleteInterval {
- delete(o.tokens, k)
- }
- }
- }
- func (s *httpdServer) validateOIDCToken(w http.ResponseWriter, r *http.Request, isAdmin bool) (oidcToken, error) {
- doRedirect := func() {
- removeOIDCCookie(w, r)
- if isAdmin {
- http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
- return
- }
- http.Redirect(w, r, webClientLoginPath, http.StatusFound)
- }
- cookie, err := r.Cookie(oidcCookieKey)
- if err != nil {
- logger.Debug(logSender, "", "no oidc cookie, redirecting to login page")
- doRedirect()
- return oidcToken{}, errInvalidToken
- }
- token, err := oidcMgr.getToken(cookie.Value)
- if err != nil {
- logger.Debug(logSender, "", "error getting oidc token associated with cookie %#v: %v", cookie.Value, err)
- doRedirect()
- return oidcToken{}, errInvalidToken
- }
- if token.isExpired() {
- logger.Debug(logSender, "", "oidc token associated with cookie %#v is expired", token.Cookie)
- if err = token.refresh(s.binding.OIDC.oauth2Config, s.binding.OIDC.verifier); err != nil {
- setFlashMessage(w, r, "Your OpenID token is expired, please log-in again")
- doRedirect()
- return oidcToken{}, errInvalidToken
- }
- } else {
- oidcMgr.updateTokenUsage(token)
- }
- if isAdmin {
- if !token.isAdmin() {
- logger.Debug(logSender, "", "oidc token associated with cookie %#v is not valid for admin users", token.Cookie)
- setFlashMessage(w, r, "Your OpenID token is not valid for the SFTPGo Web Admin UI. Please logout from your OpenID server and log-in as an SFTPGo admin")
- doRedirect()
- return oidcToken{}, errInvalidToken
- }
- return token, nil
- }
- if token.isAdmin() {
- logger.Debug(logSender, "", "oidc token associated with cookie %#v is valid for admin users", token.Cookie)
- setFlashMessage(w, r, "Your OpenID token is not valid for the SFTPGo Web Client UI. Please logout from your OpenID server and log-in as an SFTPGo user")
- doRedirect()
- return oidcToken{}, errInvalidToken
- }
- return token, nil
- }
- func (s *httpdServer) oidcTokenAuthenticator(audience tokenAudience) func(next http.Handler) http.Handler {
- return func(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if canSkipOIDCValidation(r) {
- next.ServeHTTP(w, r)
- return
- }
- token, err := s.validateOIDCToken(w, r, audience == tokenAudienceWebAdmin)
- if err != nil {
- return
- }
- jwtTokenClaims := jwtTokenClaims{
- Username: token.Username,
- Permissions: token.Permissions,
- }
- _, tokenString, err := jwtTokenClaims.createToken(s.tokenAuth, audience)
- if err != nil {
- setFlashMessage(w, r, "Unable to create cookie")
- if audience == tokenAudienceWebAdmin {
- http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
- } else {
- http.Redirect(w, r, webClientLoginPath, http.StatusFound)
- }
- return
- }
- ctx := context.WithValue(r.Context(), oidcTokenKey, token.Cookie)
- ctx = context.WithValue(ctx, oidcGeneratedToken, tokenString)
- next.ServeHTTP(w, r.WithContext(ctx))
- })
- }
- }
- func (s *httpdServer) handleWebAdminOIDCLogin(w http.ResponseWriter, r *http.Request) {
- s.oidcLoginRedirect(w, r, tokenAudienceWebAdmin)
- }
- func (s *httpdServer) handleWebClientOIDCLogin(w http.ResponseWriter, r *http.Request) {
- s.oidcLoginRedirect(w, r, tokenAudienceWebClient)
- }
- func (s *httpdServer) oidcLoginRedirect(w http.ResponseWriter, r *http.Request, audience tokenAudience) {
- pendingAuth := newOIDCPendingAuth(audience)
- oidcMgr.addPendingAuth(pendingAuth)
- http.Redirect(w, r, s.binding.OIDC.oauth2Config.AuthCodeURL(pendingAuth.State,
- oidc.Nonce(pendingAuth.Nonce)), http.StatusFound)
- }
- func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request) {
- state := r.URL.Query().Get("state")
- authReq, err := oidcMgr.getPendingAuth(state)
- if err != nil {
- logger.Debug(logSender, "", "oidc authentication state did not match")
- s.renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match",
- http.StatusBadRequest, nil, "")
- return
- }
- oidcMgr.removePendingAuth(state)
- doRedirect := func() {
- if authReq.Audience == tokenAudienceWebAdmin {
- http.Redirect(w, r, webAdminLoginPath, http.StatusFound)
- return
- }
- http.Redirect(w, r, webClientLoginPath, http.StatusFound)
- }
- doLogout := func(rawIDToken string) {
- s.logoutFromOIDCOP(rawIDToken)
- }
- ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
- defer cancel()
- oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
- if err != nil {
- logger.Debug(logSender, "", "failed to exchange oidc token: %v", err)
- setFlashMessage(w, r, "Failed to exchange OpenID token")
- doRedirect()
- return
- }
- rawIDToken, ok := oauth2Token.Extra("id_token").(string)
- if !ok {
- logger.Debug(logSender, "", "no id_token field in OAuth2 OpenID token")
- setFlashMessage(w, r, "No id_token field in OAuth2 OpenID token")
- doRedirect()
- return
- }
- idToken, err := s.binding.OIDC.verifier.Verify(ctx, rawIDToken)
- if err != nil {
- logger.Debug(logSender, "", "failed to verify oidc token: %v", err)
- setFlashMessage(w, r, "Failed to verify OpenID token")
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- if idToken.Nonce != authReq.Nonce {
- logger.Debug(logSender, "", "oidc authentication nonce did not match")
- setFlashMessage(w, r, "OpenID authentication nonce did not match")
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- claims := make(map[string]interface{})
- err = idToken.Claims(&claims)
- if err != nil {
- logger.Debug(logSender, "", "unable to get oidc token claims: %v", err)
- setFlashMessage(w, r, "Unable to get OpenID token claims")
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- token := oidcToken{
- AccessToken: oauth2Token.AccessToken,
- TokenType: oauth2Token.TokenType,
- RefreshToken: oauth2Token.RefreshToken,
- IDToken: rawIDToken,
- Nonce: idToken.Nonce,
- Cookie: xid.New().String(),
- }
- if !oauth2Token.Expiry.IsZero() {
- token.ExpiresAt = util.GetTimeAsMsSinceEpoch(oauth2Token.Expiry)
- }
- if err = token.parseClaims(claims, s.binding.OIDC.UsernameField, s.binding.OIDC.RoleField); err != nil {
- logger.Debug(logSender, "", "unable to parse oidc token claims: %v", err)
- setFlashMessage(w, r, fmt.Sprintf("Unable to parse OpenID token claims: %v", err))
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- switch authReq.Audience {
- case tokenAudienceWebAdmin:
- if !token.isAdmin() {
- logger.Debug(logSender, "", "wrong oidc token role, the mapped user is not an SFTPGo admin")
- setFlashMessage(w, r, "Wrong OpenID role, the logged in user is not an SFTPGo admin")
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- case tokenAudienceWebClient:
- if token.isAdmin() {
- logger.Debug(logSender, "", "wrong oidc token role, the mapped user is an SFTPGo admin")
- setFlashMessage(w, r, "Wrong OpenID role, the logged in user is an SFTPGo admin")
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- }
- err = token.getUser(r)
- if err != nil {
- logger.Debug(logSender, "", "unable to get the sftpgo user associated with oidc token: %v", err)
- setFlashMessage(w, r, "Unable to get the user associated with the OpenID token")
- doRedirect()
- doLogout(rawIDToken)
- return
- }
- loginOIDCUser(w, r, token)
- }
- func loginOIDCUser(w http.ResponseWriter, r *http.Request, token oidcToken) {
- oidcMgr.addToken(token)
- cookie := http.Cookie{
- Name: oidcCookieKey,
- Value: token.Cookie,
- Path: "/",
- HttpOnly: true,
- Secure: isTLS(r),
- SameSite: http.SameSiteLaxMode,
- }
- // we don't set a cookie expiration so we can refresh the token without setting a new cookie
- // the cookie will be invalidated on browser close
- http.SetCookie(w, &cookie)
- if token.isAdmin() {
- http.Redirect(w, r, webUsersPath, http.StatusFound)
- return
- }
- http.Redirect(w, r, webClientFilesPath, http.StatusFound)
- }
- func (s *httpdServer) logoutOIDCUser(w http.ResponseWriter, r *http.Request) {
- if oidcKey, ok := r.Context().Value(oidcTokenKey).(string); ok {
- removeOIDCCookie(w, r)
- token, err := oidcMgr.getToken(oidcKey)
- if err == nil {
- s.logoutFromOIDCOP(token.IDToken)
- }
- oidcMgr.removeToken(oidcKey)
- }
- }
- func (s *httpdServer) logoutFromOIDCOP(idToken string) {
- if s.binding.OIDC.providerLogoutURL == "" {
- logger.Debug(logSender, "", "oidc: provider logout URL not set, unable to logout from the OP")
- return
- }
- go s.doOIDCFromLogout(idToken)
- }
- func (s *httpdServer) doOIDCFromLogout(idToken string) {
- logoutURL, err := url.Parse(s.binding.OIDC.providerLogoutURL)
- if err != nil {
- logger.Warn(logSender, "", "oidc: unable to parse logout URL: %v", err)
- return
- }
- query := logoutURL.Query()
- if idToken != "" {
- query.Set("id_token_hint", idToken)
- }
- logoutURL.RawQuery = query.Encode()
- resp, err := httpclient.RetryableGet(logoutURL.String())
- if err != nil {
- logger.Warn(logSender, "", "oidc: error calling logout URL %#v: %v", logoutURL.String(), err)
- return
- }
- defer resp.Body.Close()
- logger.Debug(logSender, "", "oidc: logout url response code %v", resp.StatusCode)
- }
- func removeOIDCCookie(w http.ResponseWriter, r *http.Request) {
- http.SetCookie(w, &http.Cookie{
- Name: oidcCookieKey,
- Value: "",
- Path: "/",
- Expires: time.Unix(0, 0),
- MaxAge: -1,
- HttpOnly: true,
- Secure: isTLS(r),
- SameSite: http.SameSiteLaxMode,
- })
- }
- // canSkipOIDCValidation returns true if there is no OIDC cookie but a jwt cookie is set
- // and so we check if the user is logged in using a built-in user
- func canSkipOIDCValidation(r *http.Request) bool {
- _, err := r.Cookie(oidcCookieKey)
- if err != nil {
- _, err = r.Cookie(jwtCookieKey)
- return err == nil
- }
- return false
- }
- func isLoggedInWithOIDC(r *http.Request) bool {
- _, ok := r.Context().Value(oidcTokenKey).(string)
- return ok
- }
|