Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ test-e2e-contribs: $(CONTRIBS_E2E) ## Run e2e tests for external integrations
.PHONY: test-e2e-contrib-kcp
test-e2e-contrib-kcp: $(DEX_BINARY)
$(CONTRIBS_E2E):
rm -rf .kcp
mkdir .kcp
$(MAKE) run-kcp &>.kcp/kcp.log & KCP_PID=$$!; \
trap 'kill -TERM $$KCP_PID; rm -rf .kcp' TERM INT EXIT && \
Expand Down
6 changes: 3 additions & 3 deletions backend/auth/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func (ah *AuthHandler) HandleAuthorize(w http.ResponseWriter, r *http.Request) {
ah.respondWithError(w, authReq.ClientType, "failed to generate PKCE", http.StatusInternalServerError)
return
}
if err := ah.sessionStore.SavePKCEVerifier(authReq.SessionID, verifier); err != nil {
if err := ah.sessionStore.SavePKCEVerifier(r.Context(), authReq.SessionID, verifier); err != nil {
logger.Error(err, "failed to store PKCE verifier")
ah.respondWithError(w, authReq.ClientType, "failed to store PKCE verifier", http.StatusInternalServerError)
return
Expand Down Expand Up @@ -205,7 +205,7 @@ func (ah *AuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
}

verifier, err := ah.sessionStore.LoadAndDeletePKCEVerifier(authCode.SessionID)
verifier, err := ah.sessionStore.LoadAndDeletePKCEVerifier(r.Context(), authCode.SessionID)
if err != nil || verifier == "" {
logger.Error(err, "PKCE verifier not found for session; cannot exchange code", "sessionID", authCode.SessionID)
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."
Expand All @@ -228,7 +228,7 @@ func (ah *AuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
return
}
// Set session expiration and store in middleware
err = ah.sessionStore.Save(sessionState)
err = ah.sessionStore.Save(r.Context(), sessionState)
if err != nil {
logger.Error(err, "failed to save session state")
ah.respondWithError(w, authCode.ClientType, "failed to save session state", http.StatusInternalServerError)
Expand Down
6 changes: 3 additions & 3 deletions backend/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func (am *AuthMiddleware) verifyState(next http.Handler) http.Handler {
return
}

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

// isValidSession checks if a session ID exists and hasn't expired
func (am *AuthMiddleware) isValidSession(sessionID string) bool {
sessionInfo, err := am.sessionStore.Load(sessionID)
func (am *AuthMiddleware) isValidSession(ctx context.Context, sessionID string) bool {
sessionInfo, err := am.sessionStore.Load(ctx, sessionID)
if err != nil {
return false
}
Expand Down
3 changes: 1 addition & 2 deletions backend/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,14 @@ func NewHandler(
mgr *kubernetes.Manager,
frontend string,
tokenExpiry time.Duration,
sessionStore session.Store,
) (*handler, error) {
// Create JWT service for CLI authentication
jwtService, err := auth.NewJWTService("kube-bind-backend")
if err != nil {
return nil, fmt.Errorf("failed to create JWT service: %w", err)
}

sessionStore := session.NewInMemoryStore()

// Create auth middleware for request authentication
authMiddleware := auth.NewAuthMiddleware(jwtService, cookieSigningKey, cookieEncryptionKey, mgr, sessionStore)

Expand Down
27 changes: 19 additions & 8 deletions backend/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ import (
)

type Options struct {
Logs *logs.Options
OIDC *OIDC
Cookie *Cookie
Serve *Serve
Logs *logs.Options
OIDC *OIDC
Cookie *Cookie
Serve *Serve
Session *Session

ProviderKcp *providerkcp.Options

Expand Down Expand Up @@ -81,10 +82,11 @@ type ExtraOptions struct {
}

type completedOptions struct {
Logs *logs.Options
OIDC *OIDC
Cookie *Cookie
Serve *Serve
Logs *logs.Options
OIDC *OIDC
Cookie *Cookie
Serve *Serve
Session *Session

// Provider specific options
ProviderKcp *providerkcp.CompletedOptions
Expand All @@ -106,6 +108,7 @@ func NewOptions() *Options {
OIDC: NewOIDC(),
Cookie: NewCookie(),
Serve: NewServe(),
Session: NewSession(),
ProviderKcp: providerkcp.NewOptions(),

ExtraOptions: ExtraOptions{
Expand Down Expand Up @@ -155,6 +158,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) {
options.OIDC.AddFlags(fs)
options.Cookie.AddFlags(fs)
options.Serve.AddFlags(fs)
options.Session.AddFlags(fs)
options.ProviderKcp.AddFlags(fs)

fs.StringVar(&options.KubeConfig, "kubeconfig", options.KubeConfig, "path to a kubeconfig. Only required if out-of-cluster")
Expand Down Expand Up @@ -207,6 +211,9 @@ func (options *Options) Complete() (*CompletedOptions, error) {
if err := options.Cookie.Complete(); err != nil {
return nil, err
}
if err := options.Session.Complete(); err != nil {
return nil, err
}
}

// normalize the scope and the isolation
Expand Down Expand Up @@ -243,6 +250,7 @@ func (options *Options) Complete() (*CompletedOptions, error) {
OIDC: options.OIDC,
Cookie: options.Cookie,
Serve: options.Serve,
Session: options.Session,
ExtraOptions: options.ExtraOptions,
},
}
Expand Down Expand Up @@ -276,6 +284,9 @@ func (options *CompletedOptions) Validate() error {
if err := options.Cookie.Validate(); err != nil {
return err
}
if err := options.Session.Validate(); err != nil {
return err
}
}

if options.ConsumerScope != string(kubebindv1alpha2.NamespacedScope) && options.ConsumerScope != string(kubebindv1alpha2.ClusterScope) {
Expand Down
45 changes: 45 additions & 0 deletions backend/options/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
Copyright 2026 The Kube Bind Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package options

import (
"github.com/spf13/pflag"

redisoptions "github.com/kube-bind/kube-bind/backend/options/session/redis"
)

type Session struct {
Redis *redisoptions.Options
}

func NewSession() *Session {
return &Session{
Redis: redisoptions.NewOptions(),
}
}

func (options *Session) AddFlags(fs *pflag.FlagSet) {
options.Redis.AddFlags(fs)
}

func (options *Session) Complete() error {
return options.Redis.Complete()
}

func (options *Session) Validate() error {
return options.Redis.Validate()
}
51 changes: 51 additions & 0 deletions backend/options/session/redis/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2026 The Kube Bind Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package redis

import (
"fmt"
"os"

"github.com/spf13/pflag"
)

type Options struct {
Address string
Password string
}

func NewOptions() *Options {
return &Options{}
}

func (options *Options) AddFlags(fs *pflag.FlagSet) {
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.")
}

func (options *Options) Complete() error {
if pwd := os.Getenv("REDIS_PASSWORD"); pwd != "" {
options.Password = pwd
}
return nil
}

func (options *Options) Validate() error {
if options.Password != "" && options.Address == "" {
return fmt.Errorf("redis-addr must be specified when using redis-password")
}
return nil
}
13 changes: 13 additions & 0 deletions backend/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import (
kube "github.com/kube-bind/kube-bind/backend/kubernetes"
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/apibindingtemplate"
"github.com/kube-bind/kube-bind/backend/provider/kcp/controllers/apiresourceschema"
"github.com/kube-bind/kube-bind/backend/session/memory"
"github.com/kube-bind/kube-bind/backend/session/redis"
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
)

Expand Down Expand Up @@ -139,6 +141,16 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
}
}

sessionStore := memory.New()

if addr := c.Options.Session.Redis.Address; addr != "" {
redisStore, err := redis.New(addr, c.Options.Session.Redis.Password)
if err != nil {
return nil, fmt.Errorf("error setting up Redis session store: %w", err)
}
sessionStore = redisStore
}

handler, err := http.NewHandler(
s,
s.Config.Options.OIDC.OIDCServer,
Expand All @@ -153,6 +165,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
s.Kubernetes,
c.Options.Frontend,
c.Options.TokenExpiry,
sessionStore,
)
if err != nil {
return nil, fmt.Errorf("error setting up HTTP Handler: %w", err)
Expand Down
101 changes: 101 additions & 0 deletions backend/session/memory/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
Copyright 2026 The Kube Bind Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package memory

import (
"context"
"errors"
"sync"
"time"

"github.com/kube-bind/kube-bind/backend/session"
)

type pkceEntry struct {
verifier string
expiresAt time.Time
}

type Store struct {
lock sync.RWMutex
sessions map[string]*session.State
Comment thread
olamilekan000 marked this conversation as resolved.
pkceVerifiers map[string]pkceEntry
}

func New() session.Store {
return &Store{
sessions: make(map[string]*session.State),
pkceVerifiers: make(map[string]pkceEntry),
}
}

func (s *Store) Save(ctx context.Context, state *session.State) error {
s.lock.Lock()
defer s.lock.Unlock()
s.sessions[state.SessionID] = state
return nil
}

func (s *Store) Load(ctx context.Context, sessionID string) (*session.State, error) {
s.lock.RLock()
defer s.lock.RUnlock()
state, exists := s.sessions[sessionID]
if !exists {
return nil, session.ErrSessionNotFound
}
return state, nil
}

func (s *Store) Delete(ctx context.Context, sessionID string) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.sessions, sessionID)
return nil
}

func (s *Store) SavePKCEVerifier(ctx context.Context, sessionID, verifier string) error {
if sessionID == "" || verifier == "" {
return errors.New("sessionID and verifier cannot be empty")
}
s.lock.Lock()
defer s.lock.Unlock()
s.pkceVerifiers[sessionID] = pkceEntry{
verifier: verifier,
expiresAt: time.Now().Add(session.PKCEVerifierTTL),
}
return nil
}

func (s *Store) LoadAndDeletePKCEVerifier(ctx context.Context, sessionID string) (string, error) {
if sessionID == "" {
return "", session.ErrPKCEVerifierNotFound
}
s.lock.Lock()
defer s.lock.Unlock()
entry, ok := s.pkceVerifiers[sessionID]
if !ok {
return "", session.ErrPKCEVerifierNotFound
}

delete(s.pkceVerifiers, sessionID)

if time.Now().After(entry.expiresAt) {
return "", session.ErrPKCEVerifierNotFound
}

return entry.verifier, nil
}
Loading
Loading