package session import ( "context" "encoding/json" "fmt" "time" "github.com/redis/go-redis/v9" ) const keyTemplate = "sso:pkce:%s" // PKCESession holds data required to complete the OAuth2 PKCE exchange. type PKCESession struct { CodeVerifier string `json:"code_verifier"` Nonce string `json:"nonce"` ClientAlias string `json:"client_alias"` ClientID string `json:"client_id"` RedirectURI string `json:"redirect_uri"` Scope string `json:"scope"` ReturnTo string `json:"return_to,omitempty"` CreatedAt time.Time `json:"created_at"` } // Store persists pkce sessions inside Redis using a configurable TTL. type Store struct { redis *redis.Client ttl time.Duration } func NewStore(client *redis.Client, ttl time.Duration) *Store { return &Store{redis: client, ttl: ttl} } func (s *Store) Save(ctx context.Context, state string, payload *PKCESession) error { if s.redis == nil { return fmt.Errorf("redis client is not initialised") } bytes, err := json.Marshal(payload) if err != nil { return err } return s.redis.Set(ctx, fmt.Sprintf(keyTemplate, state), bytes, s.ttl).Err() } func (s *Store) Get(ctx context.Context, state string) (*PKCESession, error) { if s.redis == nil { return nil, fmt.Errorf("redis client is not initialised") } raw, err := s.redis.Get(ctx, fmt.Sprintf(keyTemplate, state)).Result() if err != nil { if err == redis.Nil { return nil, nil } return nil, err } var payload PKCESession if err := json.Unmarshal([]byte(raw), &payload); err != nil { return nil, err } return &payload, nil } func (s *Store) Delete(ctx context.Context, state string) error { if s.redis == nil { return fmt.Errorf("redis client is not initialised") } return s.redis.Del(ctx, fmt.Sprintf(keyTemplate, state)).Err() }