Skip to content

Commit f0b9e00

Browse files
committed
add optional redis for backend storage
1 parent 3156804 commit f0b9e00

22 files changed

Lines changed: 602 additions & 127 deletions

File tree

backend/auth/handler.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func (ah *AuthHandler) HandleAuthorize(w http.ResponseWriter, r *http.Request) {
120120
ah.respondWithError(w, authReq.ClientType, "failed to generate PKCE", http.StatusInternalServerError)
121121
return
122122
}
123-
if err := ah.sessionStore.SavePKCEVerifier(authReq.SessionID, verifier); err != nil {
123+
if err := ah.sessionStore.SavePKCEVerifier(r.Context(), authReq.SessionID, verifier); err != nil {
124124
logger.Error(err, "failed to store PKCE verifier")
125125
ah.respondWithError(w, authReq.ClientType, "failed to store PKCE verifier", http.StatusInternalServerError)
126126
return
@@ -205,7 +205,7 @@ func (ah *AuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
205205
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
206206
}
207207

208-
verifier, err := ah.sessionStore.LoadAndDeletePKCEVerifier(authCode.SessionID)
208+
verifier, err := ah.sessionStore.LoadAndDeletePKCEVerifier(r.Context(), authCode.SessionID)
209209
if err != nil || verifier == "" {
210210
logger.Error(err, "PKCE verifier not found for session; cannot exchange code", "sessionID", authCode.SessionID)
211211
msg := "PKCE verifier not found. If you run multiple backend instances, use a shared session store (e.g. Redis) so the instance handling the callback can read the verifier stored at authorize time."
@@ -228,7 +228,7 @@ func (ah *AuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
228228
return
229229
}
230230
// Set session expiration and store in middleware
231-
err = ah.sessionStore.Save(sessionState)
231+
err = ah.sessionStore.Save(r.Context(), sessionState)
232232
if err != nil {
233233
logger.Error(err, "failed to save session state")
234234
ah.respondWithError(w, authCode.ClientType, "failed to save session state", http.StatusInternalServerError)

backend/auth/middleware.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func (am *AuthMiddleware) verifyState(next http.Handler) http.Handler {
182182
return
183183
}
184184

185-
if !am.isValidSession(state.SessionID) {
185+
if !am.isValidSession(r.Context(), state.SessionID) {
186186
logger.V(2).Info("Session expired or invalid", "sessionID", state.SessionID)
187187
writeErrorResponse(w, http.StatusUnauthorized, kubebindv1alpha2.ErrorCodeAuthenticationFailed, "Authentication required", "Session has expired or is invalid")
188188
return
@@ -197,8 +197,8 @@ func (am *AuthMiddleware) verifyState(next http.Handler) http.Handler {
197197
}
198198

199199
// isValidSession checks if a session ID exists and hasn't expired
200-
func (am *AuthMiddleware) isValidSession(sessionID string) bool {
201-
sessionInfo, err := am.sessionStore.Load(sessionID)
200+
func (am *AuthMiddleware) isValidSession(ctx context.Context, sessionID string) bool {
201+
sessionInfo, err := am.sessionStore.Load(ctx, sessionID)
202202
if err != nil {
203203
return false
204204
}

backend/http/handler.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,14 @@ func NewHandler(
112112
mgr *kubernetes.Manager,
113113
frontend string,
114114
tokenExpiry time.Duration,
115+
sessionStore session.Store,
115116
) (*handler, error) {
116117
// Create JWT service for CLI authentication
117118
jwtService, err := auth.NewJWTService("kube-bind-backend")
118119
if err != nil {
119120
return nil, fmt.Errorf("failed to create JWT service: %w", err)
120121
}
121122

122-
sessionStore := session.NewInMemoryStore()
123-
124123
// Create auth middleware for request authentication
125124
authMiddleware := auth.NewAuthMiddleware(jwtService, cookieSigningKey, cookieEncryptionKey, mgr, sessionStore)
126125

backend/options/options.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ import (
3333
)
3434

3535
type Options struct {
36-
Logs *logs.Options
37-
OIDC *OIDC
38-
Cookie *Cookie
39-
Serve *Serve
36+
Logs *logs.Options
37+
OIDC *OIDC
38+
Cookie *Cookie
39+
Serve *Serve
40+
Session *Session
4041

4142
ProviderKcp *providerkcp.Options
4243

@@ -81,10 +82,11 @@ type ExtraOptions struct {
8182
}
8283

8384
type completedOptions struct {
84-
Logs *logs.Options
85-
OIDC *OIDC
86-
Cookie *Cookie
87-
Serve *Serve
85+
Logs *logs.Options
86+
OIDC *OIDC
87+
Cookie *Cookie
88+
Serve *Serve
89+
Session *Session
8890

8991
// Provider specific options
9092
ProviderKcp *providerkcp.CompletedOptions
@@ -106,6 +108,7 @@ func NewOptions() *Options {
106108
OIDC: NewOIDC(),
107109
Cookie: NewCookie(),
108110
Serve: NewServe(),
111+
Session: NewSession(),
109112
ProviderKcp: providerkcp.NewOptions(),
110113

111114
ExtraOptions: ExtraOptions{
@@ -155,6 +158,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) {
155158
options.OIDC.AddFlags(fs)
156159
options.Cookie.AddFlags(fs)
157160
options.Serve.AddFlags(fs)
161+
options.Session.AddFlags(fs)
158162
options.ProviderKcp.AddFlags(fs)
159163

160164
fs.StringVar(&options.KubeConfig, "kubeconfig", options.KubeConfig, "path to a kubeconfig. Only required if out-of-cluster")
@@ -207,6 +211,9 @@ func (options *Options) Complete() (*CompletedOptions, error) {
207211
if err := options.Cookie.Complete(); err != nil {
208212
return nil, err
209213
}
214+
if err := options.Session.Complete(); err != nil {
215+
return nil, err
216+
}
210217
}
211218

212219
// normalize the scope and the isolation
@@ -243,6 +250,7 @@ func (options *Options) Complete() (*CompletedOptions, error) {
243250
OIDC: options.OIDC,
244251
Cookie: options.Cookie,
245252
Serve: options.Serve,
253+
Session: options.Session,
246254
ExtraOptions: options.ExtraOptions,
247255
},
248256
}
@@ -276,6 +284,9 @@ func (options *CompletedOptions) Validate() error {
276284
if err := options.Cookie.Validate(); err != nil {
277285
return err
278286
}
287+
if err := options.Session.Validate(); err != nil {
288+
return err
289+
}
279290
}
280291

281292
if options.ConsumerScope != string(kubebindv1alpha2.NamespacedScope) && options.ConsumerScope != string(kubebindv1alpha2.ClusterScope) {

backend/options/session.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2026 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package options
18+
19+
import (
20+
"github.com/spf13/pflag"
21+
22+
redisoptions "github.com/kube-bind/kube-bind/backend/options/session/redis"
23+
)
24+
25+
type Session struct {
26+
Redis *redisoptions.Options
27+
}
28+
29+
func NewSession() *Session {
30+
return &Session{
31+
Redis: redisoptions.NewOptions(),
32+
}
33+
}
34+
35+
func (options *Session) AddFlags(fs *pflag.FlagSet) {
36+
options.Redis.AddFlags(fs)
37+
}
38+
39+
func (options *Session) Complete() error {
40+
return options.Redis.Complete()
41+
}
42+
43+
func (options *Session) Validate() error {
44+
return options.Redis.Validate()
45+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
Copyright 2026 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package redis
18+
19+
import (
20+
"fmt"
21+
"os"
22+
23+
"github.com/spf13/pflag"
24+
)
25+
26+
type Options struct {
27+
Address string
28+
Password string
29+
}
30+
31+
func NewOptions() *Options {
32+
return &Options{}
33+
}
34+
35+
func (options *Options) AddFlags(fs *pflag.FlagSet) {
36+
fs.StringVar(&options.Address, "redis-addr", options.Address, "The redis address (e.g. localhost:6379) to connect to if storing sessions in Redis. If empty, the in-memory provider is used.")
37+
}
38+
39+
func (options *Options) Complete() error {
40+
if pwd := os.Getenv("REDIS_PASSWORD"); pwd != "" {
41+
options.Password = pwd
42+
}
43+
return nil
44+
}
45+
46+
func (options *Options) Validate() error {
47+
if options.Password != "" && options.Address == "" {
48+
return fmt.Errorf("redis-addr must be specified when using redis-password")
49+
}
50+
return nil
51+
}

backend/server.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import (
4040
kube "github.com/kube-bind/kube-bind/backend/kubernetes"
4141
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/apibindingtemplate"
4242
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/apiresourceschema"
43+
"github.com/kube-bind/kube-bind/backend/session/memory"
44+
"github.com/kube-bind/kube-bind/backend/session/redis"
4345
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
4446
)
4547

@@ -132,6 +134,16 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
132134
}
133135
}
134136

137+
sessionStore := memory.New()
138+
139+
if addr := c.Options.Session.Redis.Address; addr != "" {
140+
redisStore, err := redis.New(addr, c.Options.Session.Redis.Password)
141+
if err != nil {
142+
return nil, fmt.Errorf("error setting up Redis session store: %w", err)
143+
}
144+
sessionStore = redisStore
145+
}
146+
135147
handler, err := http.NewHandler(
136148
s,
137149
s.Config.Options.OIDC.OIDCServer,
@@ -146,6 +158,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
146158
s.Kubernetes,
147159
c.Options.Frontend,
148160
c.Options.TokenExpiry,
161+
sessionStore,
149162
)
150163
if err != nil {
151164
return nil, fmt.Errorf("error setting up HTTP Handler: %w", err)

backend/session/memory/memory.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Copyright 2026 The Kube Bind Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package memory
18+
19+
import (
20+
"context"
21+
"errors"
22+
"sync"
23+
"time"
24+
25+
"github.com/kube-bind/kube-bind/backend/session"
26+
)
27+
28+
type pkceEntry struct {
29+
verifier string
30+
expiresAt time.Time
31+
}
32+
33+
type Store struct {
34+
lock sync.RWMutex
35+
sessions map[string]*session.State
36+
pkceVerifiers map[string]pkceEntry
37+
}
38+
39+
func New() session.Store {
40+
return &Store{
41+
sessions: make(map[string]*session.State),
42+
pkceVerifiers: make(map[string]pkceEntry),
43+
}
44+
}
45+
46+
func (s *Store) Save(ctx context.Context, state *session.State) error {
47+
s.lock.Lock()
48+
defer s.lock.Unlock()
49+
s.sessions[state.SessionID] = state
50+
return nil
51+
}
52+
53+
func (s *Store) Load(ctx context.Context, sessionID string) (*session.State, error) {
54+
s.lock.RLock()
55+
defer s.lock.RUnlock()
56+
state, exists := s.sessions[sessionID]
57+
if !exists {
58+
return nil, session.ErrSessionNotFound
59+
}
60+
return state, nil
61+
}
62+
63+
func (s *Store) Delete(ctx context.Context, sessionID string) error {
64+
s.lock.Lock()
65+
defer s.lock.Unlock()
66+
delete(s.sessions, sessionID)
67+
return nil
68+
}
69+
70+
func (s *Store) SavePKCEVerifier(ctx context.Context, sessionID, verifier string) error {
71+
if sessionID == "" || verifier == "" {
72+
return errors.New("sessionID and verifier cannot be empty")
73+
}
74+
s.lock.Lock()
75+
defer s.lock.Unlock()
76+
s.pkceVerifiers[sessionID] = pkceEntry{
77+
verifier: verifier,
78+
expiresAt: time.Now().Add(session.PKCEVerifierTTL),
79+
}
80+
return nil
81+
}
82+
83+
func (s *Store) LoadAndDeletePKCEVerifier(ctx context.Context, sessionID string) (string, error) {
84+
if sessionID == "" {
85+
return "", session.ErrPKCEVerifierNotFound
86+
}
87+
s.lock.Lock()
88+
defer s.lock.Unlock()
89+
entry, ok := s.pkceVerifiers[sessionID]
90+
if !ok {
91+
return "", session.ErrPKCEVerifierNotFound
92+
}
93+
94+
delete(s.pkceVerifiers, sessionID)
95+
96+
if time.Now().After(entry.expiresAt) {
97+
return "", session.ErrPKCEVerifierNotFound
98+
}
99+
100+
return entry.verifier, nil
101+
}

0 commit comments

Comments
 (0)