From 52b2b17e567d11a768734166c8b13518c8e670f5 Mon Sep 17 00:00:00 2001 From: mmerrill3 Date: Wed, 16 Nov 2022 18:24:45 -0500 Subject: [PATCH] feature: pxce for UI #9890 Signed-off-by: mmerrill3 --- cmd/argocd/commands/login.go | 18 ++-- server/server.go | 7 +- util/oidc/oidc.go | 17 +++- util/oidc/oidc_test.go | 47 +++++++--- util/oidc/pkce/pkcemanager.go | 56 ++++++++++++ util/oidc/pkce/state.go | 156 ++++++++++++++++++++++++++++++++++ 6 files changed, 273 insertions(+), 28 deletions(-) create mode 100644 util/oidc/pkce/pkcemanager.go create mode 100644 util/oidc/pkce/state.go diff --git a/cmd/argocd/commands/login.go b/cmd/argocd/commands/login.go index 92c24b787cd39..f4bbf26c4c997 100644 --- a/cmd/argocd/commands/login.go +++ b/cmd/argocd/commands/login.go @@ -2,8 +2,6 @@ package commands import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" "html" "net/http" @@ -30,6 +28,7 @@ import ( jwtutil "github.com/argoproj/argo-cd/v2/util/jwt" "github.com/argoproj/argo-cd/v2/util/localconfig" oidcutil "github.com/argoproj/argo-cd/v2/util/oidc" + "github.com/argoproj/argo-cd/v2/util/oidc/pkce" "github.com/argoproj/argo-cd/v2/util/rand" ) @@ -228,14 +227,7 @@ func oauth2Login( completionChan <- errMsg } - // PKCE implementation of https://tools.ietf.org/html/rfc7636 - codeVerifier, err := rand.StringFromCharset( - 43, - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~", - ) - errors.CheckError(err) - codeChallengeHash := sha256.Sum256([]byte(codeVerifier)) - codeChallenge := base64.RawURLEncoding.EncodeToString(codeChallengeHash[:]) + pkceCodes := pkce.GeneratePKCECodes() // Authorization redirect callback from OAuth2 auth flow. // Handles both implicit and authorization code flow @@ -276,7 +268,7 @@ func oauth2Login( handleErr(w, fmt.Sprintf("no code in request: %q", r.Form)) return } - opts := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} + opts := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", pkceCodes.CodeVerifier)} tok, err := oauth2conf.Exchange(ctx, code, opts...) if err != nil { handleErr(w, err.Error()) @@ -313,8 +305,8 @@ func oauth2Login( switch grantType { case oidcutil.GrantTypeAuthorizationCode: - opts = append(opts, oauth2.SetAuthURLParam("code_challenge", codeChallenge)) - opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", "S256")) + opts = append(opts, oauth2.SetAuthURLParam("code_challenge", pkceCodes.CodeChallenge)) + opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", pkceCodes.CodeChallengeHash)) url = oauth2conf.AuthCodeURL(stateNonce, opts...) case oidcutil.GrantTypeImplicit: url, err = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, opts...) diff --git a/server/server.go b/server/server.go index 6497646f4497a..f6139401ec792 100644 --- a/server/server.go +++ b/server/server.go @@ -115,6 +115,7 @@ import ( "github.com/argoproj/argo-cd/v2/util/notification/k8s" settings_notif "github.com/argoproj/argo-cd/v2/util/notification/settings" "github.com/argoproj/argo-cd/v2/util/oidc" + "github.com/argoproj/argo-cd/v2/util/oidc/pkce" "github.com/argoproj/argo-cd/v2/util/rbac" util_session "github.com/argoproj/argo-cd/v2/util/session" settings_util "github.com/argoproj/argo-cd/v2/util/settings" @@ -172,6 +173,7 @@ type ArgoCDServer struct { log *log.Entry sessionMgr *util_session.SessionManager settingsMgr *settings_util.SettingsManager + pkceMgr *pkce.PKCEManager enf *rbac.Enforcer projInformer cache.SharedIndexInformer projLister applisters.AppProjectNamespaceLister @@ -264,6 +266,8 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { appsetLister := appFactory.Argoproj().V1alpha1().ApplicationSets().Lister().ApplicationSets(opts.Namespace) userStateStorage := util_session.NewUserStateStorage(opts.RedisClient) + pkceStateStorage := pkce.NewPKCEStateStorage(opts.RedisClient) + pkceManager := pkce.NewPKCEManager(pkceStateStorage) sessionMgr := util_session.NewSessionManager(settingsMgr, projLister, opts.DexServerAddr, opts.DexTLSConfig, userStateStorage) enf := rbac.NewEnforcer(opts.KubeClientset, opts.Namespace, common.ArgoCDRBACConfigMapName, nil) enf.EnableEnforce(!opts.DisableAuth) @@ -307,6 +311,7 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { apiFactory: apiFactory, secretInformer: secretInformer, configMapInformer: configMapInformer, + pkceMgr: pkceManager, } } @@ -989,7 +994,7 @@ func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) { // Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex) var err error mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr, a.BaseHRef, a.DexTLSConfig)) - a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.DexServerAddr, a.DexTLSConfig, a.BaseHRef) + a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.DexServerAddr, a.DexTLSConfig, a.BaseHRef, a.pkceMgr) errorsutil.CheckError(err) mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin) mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback) diff --git a/util/oidc/oidc.go b/util/oidc/oidc.go index 4f93e200c28c9..7e3cf95b2cacf 100644 --- a/util/oidc/oidc.go +++ b/util/oidc/oidc.go @@ -1,6 +1,7 @@ package oidc import ( + "context" "encoding/hex" "encoding/json" "fmt" @@ -24,6 +25,7 @@ import ( "github.com/argoproj/argo-cd/v2/util/crypto" "github.com/argoproj/argo-cd/v2/util/dex" httputil "github.com/argoproj/argo-cd/v2/util/http" + "github.com/argoproj/argo-cd/v2/util/oidc/pkce" "github.com/argoproj/argo-cd/v2/util/rand" "github.com/argoproj/argo-cd/v2/util/settings" ) @@ -70,6 +72,8 @@ type ClientApp struct { encryptionKey []byte // provider is the OIDC provider provider Provider + // pkce manager for PKCE entries + pkceManager *pkce.PKCEManager } func GetScopesOrDefault(scopes []string) []string { @@ -81,7 +85,7 @@ func GetScopesOrDefault(scopes []string) []string { // NewClientApp will register the Argo CD client app (either via Dex or external OIDC) and return an // object which has HTTP handlers for handling the HTTP responses for login and callback -func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTlsConfig *dex.DexTLSConfig, baseHRef string) (*ClientApp, error) { +func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTlsConfig *dex.DexTLSConfig, baseHRef string, pkceManager *pkce.PKCEManager) (*ClientApp, error) { redirectURL, err := settings.RedirectURL() if err != nil { return nil, err @@ -97,6 +101,7 @@ func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTl issuerURL: settings.IssuerURL(), baseHRef: baseHRef, encryptionKey: encryptionKey, + pkceManager: pkceManager, } log.Infof("Creating client app (%s)", a.clientID) u, err := url.Parse(settings.URL) @@ -301,6 +306,12 @@ func (a *ClientApp) HandleLogin(w http.ResponseWriter, r *http.Request) { var url string switch grantType { case GrantTypeAuthorizationCode: + ctx := context.Background() + pkceCodes := pkce.GeneratePKCECodes() + pkceCodes.Nonce = stateNonce + a.pkceManager.StorePKCEEntry(ctx, pkceCodes, time.Hour) + opts = append(opts, oauth2.SetAuthURLParam("code_challenge", pkceCodes.CodeChallenge)) + opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", pkceCodes.CodeChallengeHash)) url = oauth2Config.AuthCodeURL(stateNonce, opts...) case GrantTypeImplicit: url, err = ImplicitFlowURL(oauth2Config, stateNonce, opts...) @@ -343,7 +354,9 @@ func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) { return } ctx := gooidc.ClientContext(r.Context(), a.client) - token, err := oauth2Config.Exchange(ctx, code) + codeVerifier := a.pkceManager.RetrieveVerifierCode(state) + opts := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)} + token, err := oauth2Config.Exchange(ctx, code, opts...) if err != nil { http.Error(w, fmt.Sprintf("failed to get token: %v", err), http.StatusInternalServerError) return diff --git a/util/oidc/oidc_test.go b/util/oidc/oidc_test.go index 9e5fc59ae105a..bca515370a356 100644 --- a/util/oidc/oidc_test.go +++ b/util/oidc/oidc_test.go @@ -22,8 +22,11 @@ import ( "github.com/argoproj/argo-cd/v2/util" "github.com/argoproj/argo-cd/v2/util/crypto" "github.com/argoproj/argo-cd/v2/util/dex" + "github.com/argoproj/argo-cd/v2/util/oidc/pkce" "github.com/argoproj/argo-cd/v2/util/settings" "github.com/argoproj/argo-cd/v2/util/test" + + redis_test "github.com/argoproj/argo-cd/v2/test" ) func TestInferGrantType(t *testing.T) { @@ -110,6 +113,9 @@ func TestHandleCallback(t *testing.T) { } func TestClientApp_HandleLogin(t *testing.T) { + redis, closer := redis_test.NewInMemoryRedis() + defer closer() + oidcTestServer := test.GetOIDCTestServer(t) t.Cleanup(oidcTestServer.Close) @@ -126,7 +132,7 @@ clientID: xxx clientSecret: yyy requestedScopes: ["oidc"]`, oidcTestServer.URL), } - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) req := httptest.NewRequest("GET", "https://argocd.example.com/auth/login", nil) @@ -141,7 +147,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), cdSettings.OIDCTLSInsecureSkipVerify = true - app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) w = httptest.NewRecorder() @@ -166,7 +172,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), require.NoError(t, err) cdSettings.Certificate = &cert - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) req := httptest.NewRequest("GET", "https://argocd.example.com/auth/login", nil) @@ -179,7 +185,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), t.Fatal("did not receive expected certificate verification failure error") } - app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) w = httptest.NewRecorder() @@ -192,6 +198,10 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), } func Test_Login_Flow(t *testing.T) { + + redis, closer := redis_test.NewInMemoryRedis() + defer closer() + // Show that SSO login works when no redirect URL is provided, and we fall back to the configured base href for the // Argo CD instance. @@ -211,7 +221,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), // The base href (the last argument for NewClientApp) is what HandleLogin will fall back to when no explicit // redirect URL is given. - app, err := NewClientApp(cdSettings, "", nil, "/") + app, err := NewClientApp(cdSettings, "", nil, "/", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) w := httptest.NewRecorder() @@ -238,6 +248,10 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), } func TestClientApp_HandleCallback(t *testing.T) { + + redis, closer := redis_test.NewInMemoryRedis() + defer closer() + oidcTestServer := test.GetOIDCTestServer(t) t.Cleanup(oidcTestServer.Close) @@ -254,7 +268,7 @@ clientID: xxx clientSecret: yyy requestedScopes: ["oidc"]`, oidcTestServer.URL), } - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) req := httptest.NewRequest("GET", "https://argocd.example.com/auth/callback", nil) @@ -269,7 +283,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), cdSettings.OIDCTLSInsecureSkipVerify = true - app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) w = httptest.NewRecorder() @@ -294,7 +308,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), require.NoError(t, err) cdSettings.Certificate = &cert - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) req := httptest.NewRequest("GET", "https://argocd.example.com/auth/callback", nil) @@ -307,7 +321,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), t.Fatal("did not receive expected certificate verification failure error") } - app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) w = httptest.NewRecorder() @@ -403,10 +417,13 @@ func TestIsValidRedirect(t *testing.T) { } func TestGenerateAppState(t *testing.T) { + redis, closer := redis_test.NewInMemoryRedis() + defer closer() + signature, err := util.MakeSignature(32) require.NoError(t, err) expectedReturnURL := "http://argocd.example.com/" - app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", nil, "") + app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", nil, "", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) generateResponse := httptest.NewRecorder() state, err := app.generateAppState(expectedReturnURL, generateResponse) @@ -435,6 +452,9 @@ func TestGenerateAppState(t *testing.T) { } func TestGenerateAppState_XSS(t *testing.T) { + redis, closer := redis_test.NewInMemoryRedis() + defer closer() + signature, err := util.MakeSignature(32) require.NoError(t, err) app, err := NewClientApp( @@ -443,7 +463,7 @@ func TestGenerateAppState_XSS(t *testing.T) { URL: "https://argocd.example.com", ServerSignature: signature, }, - "", nil, "", + "", nil, "", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis)), ) require.NoError(t, err) @@ -485,6 +505,9 @@ func TestGenerateAppState_XSS(t *testing.T) { } func TestGenerateAppState_NoReturnURL(t *testing.T) { + redis, closer := redis_test.NewInMemoryRedis() + defer closer() + signature, err := util.MakeSignature(32) require.NoError(t, err) cdSettings := &settings.ArgoCDSettings{ServerSignature: signature} @@ -495,7 +518,7 @@ func TestGenerateAppState_NoReturnURL(t *testing.T) { encrypted, err := crypto.Encrypt([]byte("123"), key) require.NoError(t, err) - app, err := NewClientApp(cdSettings, "", nil, "/argo-cd") + app, err := NewClientApp(cdSettings, "", nil, "/argo-cd", pkce.NewPKCEManager(pkce.NewPKCEStateStorage(redis))) require.NoError(t, err) req.AddCookie(&http.Cookie{Name: common.StateCookieName, Value: hex.EncodeToString(encrypted)}) diff --git a/util/oidc/pkce/pkcemanager.go b/util/oidc/pkce/pkcemanager.go new file mode 100644 index 0000000000000..c9572ddef417c --- /dev/null +++ b/util/oidc/pkce/pkcemanager.go @@ -0,0 +1,56 @@ +package pkce + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "time" + + "github.com/argoproj/argo-cd/v2/util/errors" + "github.com/argoproj/argo-cd/v2/util/rand" +) + +// PKCEManager handles code verifications and their associated auth tokens +type PKCEManager struct { + storage PKCEStateStorage +} + +type PKCECodes struct { + CodeVerifier string + CodeChallenge string + AuthCode string + Nonce string + CodeChallengeHash string +} + +func GeneratePKCECodes() *PKCECodes { + // PKCE implementation of https://tools.ietf.org/html/rfc7636 + codeVerifier, err := rand.StringFromCharset( + 43, + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~", + ) + errors.CheckError(err) + codeChallengeHash := sha256.Sum256([]byte(codeVerifier)) + codeChallenge := base64.RawURLEncoding.EncodeToString(codeChallengeHash[:]) + return &PKCECodes{ + CodeVerifier: codeVerifier, + CodeChallenge: codeChallenge, + CodeChallengeHash: "S256", + } +} + +// PKCEManager creates a new pkce manager +func NewPKCEManager(storage PKCEStateStorage) *PKCEManager { + s := PKCEManager{ + storage: storage, + } + return &s +} + +func (mgr *PKCEManager) StorePKCEEntry(ctx context.Context, pkceCodes *PKCECodes, expiringAt time.Duration) error { + return mgr.storage.StorePKCEEntry(ctx, pkceCodes, expiringAt) +} + +func (mgr *PKCEManager) RetrieveVerifierCode(nonce string) string { + return mgr.storage.RetrieveCodeVerifier(nonce) +} diff --git a/util/oidc/pkce/state.go b/util/oidc/pkce/state.go new file mode 100644 index 0000000000000..0f94453703276 --- /dev/null +++ b/util/oidc/pkce/state.go @@ -0,0 +1,156 @@ +package pkce + +import ( + "context" + "strings" + "sync" + "time" + + "github.com/go-redis/redis/v8" + log "github.com/sirupsen/logrus" + + util "github.com/argoproj/argo-cd/v2/util/io" +) + +const ( + pkceEntryPrefix = "pkce-auth|" + newVerifierCodeWithAuth = "new-verifier-code-with-auth" +) + +type pkceStateStorage struct { + redis *redis.Client + pkceAuthCodes map[string]string + lock sync.RWMutex + resyncDuration time.Duration +} + +var _ PKCEStateStorage = &pkceStateStorage{} + +func NewPKCEStateStorage(redis *redis.Client) *pkceStateStorage { + return &pkceStateStorage{ + pkceAuthCodes: map[string]string{}, + resyncDuration: time.Hour, + redis: redis, + } +} + +func buildRedisEntry(pkceCodes *PKCECodes) string { + return pkceEntryPrefix + buildRedisValue(pkceCodes) +} + +func buildRedisValue(pkceCodes *PKCECodes) string { + return pkceCodes.Nonce + "~" + pkceCodes.CodeVerifier +} + +func (storage *pkceStateStorage) Init(ctx context.Context) { + go storage.watchPKCEEntries(ctx) + ticker := time.NewTicker(storage.resyncDuration) + go func() { + storage.loadPKCEEntriesSafe() + for range ticker.C { + storage.loadPKCEEntriesSafe() + } + }() + go func() { + <-ctx.Done() + ticker.Stop() + }() +} + +func (storage *pkceStateStorage) watchPKCEEntries(ctx context.Context) { + pubsub := storage.redis.Subscribe(ctx, newVerifierCodeWithAuth) + defer util.Close(pubsub) + + ch := pubsub.Channel() + for { + select { + case <-ctx.Done(): + return + case val := <-ch: + storage.lock.Lock() + pkceParts := strings.Split(val.Payload, "~") + storage.pkceAuthCodes[pkceParts[0]] = strings.Join(pkceParts[1:], "") + storage.lock.Unlock() + } + } +} + +func (storage *pkceStateStorage) loadPKCEEntriesSafe() { + err := storage.loadPKCEEntries() + for err != nil { + log.Warnf("Failed to resync pkce entries. retrying again in 1 minute: %v", err) + time.Sleep(time.Minute) + err = storage.loadPKCEEntries() + } +} + +func (storage *pkceStateStorage) loadPKCEEntries() error { + storage.lock.Lock() + defer storage.lock.Unlock() + storage.pkceAuthCodes = map[string]string{} + iterator := storage.redis.Scan(context.Background(), 0, pkceEntryPrefix+"*", -1).Iterator() + for iterator.Next(context.Background()) { + parts := strings.Split(iterator.Val(), "|") + if len(parts) < 2 { + log.Warnf("Unexpected redis key prefixed with '%s'. Must have nonce and code verifier, tilde separated, after the prefix but got: '%s'.", + pkceEntryPrefix, + iterator.Val()) + continue + } + pkceParts := strings.Split(parts[1], "~") + storage.pkceAuthCodes[pkceParts[0]] = strings.Join(pkceParts[1:], "") + } + if iterator.Err() != nil { + return iterator.Err() + } + + return nil +} + +func (storage *pkceStateStorage) StorePKCEEntry(ctx context.Context, pkceCodes *PKCECodes, expiringAt time.Duration) error { + storage.lock.Lock() + storage.pkceAuthCodes[pkceCodes.Nonce] = pkceCodes.CodeVerifier + storage.lock.Unlock() + if err := storage.redis.Set(ctx, buildRedisEntry(pkceCodes), "", expiringAt).Err(); err != nil { + return err + } + return storage.redis.Publish(ctx, newVerifierCodeWithAuth, buildRedisValue(pkceCodes)).Err() +} + +func (storage *pkceStateStorage) RetrieveCodeVerifier(nonce string) string { + storage.lock.Lock() + defer storage.lock.Unlock() + if codeVerifier, ok := storage.pkceAuthCodes[nonce]; ok { + return codeVerifier + } else { + //go to redis + iterator := storage.redis.Scan(context.Background(), 0, pkceEntryPrefix+nonce+"*", 1).Iterator() + if iterator.Next(context.Background()) { + parts := strings.Split(iterator.Val(), "|") + if len(parts) < 2 { + log.Warnf("Unexpected redis key prefixed with '%s'. Must have nonce and code verifier, tilde separated, after the prefix but got: '%s'.", + pkceEntryPrefix+nonce, + iterator.Val()) + return "" + } + pkceParts := strings.Split(parts[1], "~") + codeVerifier := strings.Join(pkceParts[1:], "") + storage.pkceAuthCodes[nonce] = codeVerifier + return codeVerifier + } + + if iterator.Err() != nil { + log.Warnf("Unexpected redis error when optimistically looking in redis for nonce '%s' : %v", nonce, iterator.Err()) + } + } + log.Warnf("Did not find code verifier for nonce '%s'", nonce) + return "" +} + +type PKCEStateStorage interface { + Init(ctx context.Context) + // StorePKCEEntry stores the verifier code and authorization code combination + StorePKCEEntry(ctx context.Context, pkceEntry *PKCECodes, expiringAt time.Duration) error + // RetrieveCodeVerifier gets the verifier code in memory + RetrieveCodeVerifier(nonce string) string +}