OAuth2: add PKCE

Fixes #2134

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2025-12-11 08:32:55 +01:00
commit 21639b963c
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
4 changed files with 12 additions and 4 deletions

View file

@ -120,6 +120,6 @@ func (s *httpdServer) handleSMTPOAuth2TokenRequestPost(w http.ResponseWriter, r
sendAPIResponse(w, r, nil, "unable to create state token", http.StatusInternalServerError)
return
}
u := cfg.AuthCodeURL(stateToken, oauth2.AccessTypeOffline)
u := cfg.AuthCodeURL(stateToken, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(pendingAuth.Verifier))
sendAPIResponse(w, r, nil, u, http.StatusOK)
}

View file

@ -20,6 +20,8 @@ import (
"sync"
"time"
"golang.org/x/oauth2"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/logger"
@ -48,6 +50,7 @@ type oauth2PendingAuth struct {
ClientSecret *kms.Secret `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
IssuedAt int64 `json:"issued_at"`
Verifier string `json:"verifier"`
}
func newOAuth2PendingAuth(provider int, redirectURL, clientID string, clientSecret *kms.Secret) oauth2PendingAuth {
@ -58,6 +61,7 @@ func newOAuth2PendingAuth(provider int, redirectURL, clientID string, clientSecr
ClientSecret: clientSecret,
RedirectURL: redirectURL,
IssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),
Verifier: oauth2.GenerateVerifier(),
}
}

View file

@ -200,6 +200,7 @@ type oidcPendingAuth struct {
Nonce string `json:"nonce"`
Audience tokenAudience `json:"audience"`
IssuedAt int64 `json:"issued_at"`
Verifier string `json:"verifier"`
}
func newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {
@ -208,6 +209,7 @@ func newOIDCPendingAuth(audience tokenAudience) oidcPendingAuth {
Nonce: util.GenerateOpaqueString(),
Audience: audience,
IssuedAt: util.GetTimeAsMsSinceEpoch(time.Now()),
Verifier: oauth2.GenerateVerifier(),
}
}
@ -595,7 +597,7 @@ func (s *httpdServer) oidcLoginRedirect(w http.ResponseWriter, r *http.Request,
pendingAuth := newOIDCPendingAuth(audience)
oidcMgr.addPendingAuth(pendingAuth)
http.Redirect(w, r, s.binding.OIDC.oauth2Config.AuthCodeURL(pendingAuth.State,
oidc.Nonce(pendingAuth.Nonce)), http.StatusFound)
oidc.Nonce(pendingAuth.Nonce), oauth2.S256ChallengeOption(pendingAuth.Verifier)), http.StatusFound)
}
func (s *httpdServer) debugTokenClaims(claims map[string]any, rawIDToken string) {
@ -634,7 +636,8 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code"))
oauth2Token, err := s.binding.OIDC.oauth2Config.Exchange(ctx, r.URL.Query().Get("code"),
oauth2.VerifierOption(authReq.Verifier))
if err != nil {
logger.Debug(logSender, "", "failed to exchange oidc token: %v", err)
setFlashMessage(w, r, newFlashMessage("Failed to exchange OpenID token", util.I18nOIDCErrTokenExchange))

View file

@ -34,6 +34,7 @@ import (
"github.com/sftpgo/sdk"
sdkkms "github.com/sftpgo/sdk/kms"
"golang.org/x/oauth2"
"github.com/drakkan/sftpgo/v2/internal/acme"
"github.com/drakkan/sftpgo/v2/internal/common"
@ -4425,7 +4426,7 @@ func (s *httpdServer) handleOAuth2TokenRedirect(w http.ResponseWriter, r *http.R
cfg := oauth2Config.GetOAuth2()
cfg.RedirectURL = pendingAuth.RedirectURL
token, err := cfg.Exchange(ctx, r.URL.Query().Get("code"))
token, err := cfg.Exchange(ctx, r.URL.Query().Get("code"), oauth2.VerifierOption(pendingAuth.Verifier))
if err != nil {
s.renderMessagePage(w, r, util.I18nOAuth2ErrorTitle, http.StatusInternalServerError,
util.NewI18nError(err, util.I18nOAuth2ErrTokenExchange), "")