root: replace boj/redistore with vendored version of rbcervilla/redisstore (#6988)
* root: replace boj/redistore with vendored version of rbcervilla/redisstore Signed-off-by: Jens Langhammer <jens@goauthentik.io> * setup env for go tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -280,7 +280,7 @@ func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
|
||||
"id_token_hint": []string{cc.RawToken},
|
||||
}
|
||||
redirect += "?" + uv.Encode()
|
||||
err = a.Logout(cc.Sub)
|
||||
err = a.Logout(r.Context(), cc.Sub)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to logout of other sessions")
|
||||
}
|
||||
|
@ -1,23 +1,23 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/outpost/proxyv2/codecs"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
"gopkg.in/boj/redistore.v1"
|
||||
"goauthentik.io/internal/outpost/proxyv2/redisstore"
|
||||
)
|
||||
|
||||
const RedisKeyPrefix = "authentik_proxy_session_"
|
||||
@ -30,20 +30,26 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||
maxAge = int(*t) + 1
|
||||
}
|
||||
if a.isEmbedded {
|
||||
rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), config.Get().Redis.Password, strconv.Itoa(config.Get().Redis.DB))
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port),
|
||||
// Username: config.Get().Redis.Password,
|
||||
Password: config.Get().Redis.Password,
|
||||
DB: config.Get().Redis.DB,
|
||||
})
|
||||
|
||||
// New default RedisStore
|
||||
rs, err := redisstore.NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rs.Codecs = codecs.CodecsFromPairs(maxAge, []byte(*p.CookieSecret))
|
||||
rs.SetMaxLength(math.MaxInt)
|
||||
rs.SetKeyPrefix(RedisKeyPrefix)
|
||||
|
||||
rs.Options.HttpOnly = true
|
||||
if strings.ToLower(externalHost.Scheme) == "https" {
|
||||
rs.Options.Secure = true
|
||||
}
|
||||
rs.Options.Domain = *p.CookieDomain
|
||||
rs.Options.SameSite = http.SameSiteLaxMode
|
||||
rs.KeyPrefix(RedisKeyPrefix)
|
||||
rs.Options(sessions.Options{
|
||||
HttpOnly: strings.ToLower(externalHost.Scheme) == "https",
|
||||
Domain: *p.CookieDomain,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
|
||||
a.log.Trace("using redis session backend")
|
||||
return rs
|
||||
}
|
||||
@ -80,7 +86,7 @@ func (a *Application) getAllCodecs() []securecookie.Codec {
|
||||
return cs
|
||||
}
|
||||
|
||||
func (a *Application) Logout(sub string) error {
|
||||
func (a *Application) Logout(ctx context.Context, sub string) error {
|
||||
if _, ok := a.sessions.(*sessions.FilesystemStore); ok {
|
||||
files, err := os.ReadDir(os.TempDir())
|
||||
if err != nil {
|
||||
@ -120,31 +126,22 @@ func (a *Application) Logout(sub string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
if rs, ok := a.sessions.(*redistore.RediStore); ok {
|
||||
pool := rs.Pool.Get()
|
||||
defer pool.Close()
|
||||
rep, err := pool.Do("KEYS", fmt.Sprintf("%s*", RedisKeyPrefix))
|
||||
if rs, ok := a.sessions.(*redisstore.RedisStore); ok {
|
||||
client := rs.Client()
|
||||
defer client.Close()
|
||||
keys, err := client.Keys(ctx, fmt.Sprintf("%s*", RedisKeyPrefix)).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keys, err := redis.Strings(rep, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serializer := redistore.GobSerializer{}
|
||||
serializer := redisstore.GobSerializer{}
|
||||
for _, key := range keys {
|
||||
v, err := pool.Do("GET", key)
|
||||
v, err := client.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to get value")
|
||||
continue
|
||||
}
|
||||
b, err := redis.Bytes(v, err)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to load value")
|
||||
continue
|
||||
}
|
||||
s := sessions.Session{}
|
||||
err = serializer.Deserialize(b, &s)
|
||||
err = serializer.Deserialize([]byte(v), &s)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to deserialize")
|
||||
continue
|
||||
@ -156,7 +153,7 @@ func (a *Application) Logout(sub string) error {
|
||||
claims := c.(Claims)
|
||||
if claims.Sub == sub {
|
||||
a.log.WithField("key", key).Trace("deleting session")
|
||||
_, err := pool.Do("DEL", key)
|
||||
_, err := client.Del(ctx, key).Result()
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to delete key")
|
||||
continue
|
||||
|
21
internal/outpost/proxyv2/redisstore/LICENSE
Normal file
21
internal/outpost/proxyv2/redisstore/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Ruben Cervilla
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
200
internal/outpost/proxyv2/redisstore/redisstore.go
Normal file
200
internal/outpost/proxyv2/redisstore/redisstore.go
Normal file
@ -0,0 +1,200 @@
|
||||
package redisstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisStore stores gorilla sessions in Redis
|
||||
type RedisStore struct {
|
||||
// client to connect to redis
|
||||
client redis.UniversalClient
|
||||
// default options to use when a new session is created
|
||||
options sessions.Options
|
||||
// key prefix with which the session will be stored
|
||||
keyPrefix string
|
||||
// key generator
|
||||
keyGen KeyGenFunc
|
||||
// session serializer
|
||||
serializer SessionSerializer
|
||||
}
|
||||
|
||||
// KeyGenFunc defines a function used by store to generate a key
|
||||
type KeyGenFunc func() (string, error)
|
||||
|
||||
// NewRedisStore returns a new RedisStore with default configuration
|
||||
func NewRedisStore(ctx context.Context, client redis.UniversalClient) (*RedisStore, error) {
|
||||
rs := &RedisStore{
|
||||
options: sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 86400 * 30,
|
||||
},
|
||||
client: client,
|
||||
keyPrefix: "session:",
|
||||
keyGen: generateRandomKey,
|
||||
serializer: GobSerializer{},
|
||||
}
|
||||
|
||||
return rs, rs.client.Ping(ctx).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) Client() redis.UniversalClient {
|
||||
return s.client
|
||||
}
|
||||
|
||||
// Get returns a session for the given name after adding it to the registry.
|
||||
func (s *RedisStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||
return sessions.GetRegistry(r).Get(s, name)
|
||||
}
|
||||
|
||||
// New returns a session for the given name without adding it to the registry.
|
||||
func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
||||
session := sessions.NewSession(s, name)
|
||||
opts := s.options
|
||||
session.Options = &opts
|
||||
session.IsNew = true
|
||||
|
||||
c, err := r.Cookie(name)
|
||||
if err != nil {
|
||||
return session, nil
|
||||
}
|
||||
session.ID = c.Value
|
||||
|
||||
err = s.load(r.Context(), session)
|
||||
if err == nil {
|
||||
session.IsNew = false
|
||||
} else if err == redis.Nil {
|
||||
err = nil // no data stored
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
|
||||
// Save adds a single session to the response.
|
||||
//
|
||||
// If the Options.MaxAge of the session is <= 0 then the session file will be
|
||||
// deleted from the store. With this process it enforces the properly
|
||||
// session cookie handling so no need to trust in the cookie management in the
|
||||
// web browser.
|
||||
func (s *RedisStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||
// Delete if max-age is <= 0
|
||||
if session.Options.MaxAge <= 0 {
|
||||
if err := s.delete(r.Context(), session); err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.ID == "" {
|
||||
id, err := s.keyGen()
|
||||
if err != nil {
|
||||
return errors.New("redisstore: failed to generate session id")
|
||||
}
|
||||
session.ID = id
|
||||
}
|
||||
if err := s.save(r.Context(), session); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), session.ID, session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Options set options to use when a new session is created
|
||||
func (s *RedisStore) Options(opts sessions.Options) {
|
||||
s.options = opts
|
||||
}
|
||||
|
||||
// KeyPrefix sets the key prefix to store session in Redis
|
||||
func (s *RedisStore) KeyPrefix(keyPrefix string) {
|
||||
s.keyPrefix = keyPrefix
|
||||
}
|
||||
|
||||
// KeyGen sets the key generator function
|
||||
func (s *RedisStore) KeyGen(f KeyGenFunc) {
|
||||
s.keyGen = f
|
||||
}
|
||||
|
||||
// Serializer sets the session serializer to store session
|
||||
func (s *RedisStore) Serializer(ss SessionSerializer) {
|
||||
s.serializer = ss
|
||||
}
|
||||
|
||||
// Close closes the Redis store
|
||||
func (s *RedisStore) Close() error {
|
||||
return s.client.Close()
|
||||
}
|
||||
|
||||
// save writes session in Redis
|
||||
func (s *RedisStore) save(ctx context.Context, session *sessions.Session) error {
|
||||
b, err := s.serializer.Serialize(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.Set(ctx, s.keyPrefix+session.ID, b, time.Duration(session.Options.MaxAge)*time.Second).Err()
|
||||
}
|
||||
|
||||
// load reads session from Redis
|
||||
func (s *RedisStore) load(ctx context.Context, session *sessions.Session) error {
|
||||
cmd := s.client.Get(ctx, s.keyPrefix+session.ID)
|
||||
if cmd.Err() != nil {
|
||||
return cmd.Err()
|
||||
}
|
||||
|
||||
b, err := cmd.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.serializer.Deserialize(b, session)
|
||||
}
|
||||
|
||||
// delete deletes session in Redis
|
||||
func (s *RedisStore) delete(ctx context.Context, session *sessions.Session) error {
|
||||
return s.client.Del(ctx, s.keyPrefix+session.ID).Err()
|
||||
}
|
||||
|
||||
// SessionSerializer provides an interface for serialize/deserialize a session
|
||||
type SessionSerializer interface {
|
||||
Serialize(s *sessions.Session) ([]byte, error)
|
||||
Deserialize(b []byte, s *sessions.Session) error
|
||||
}
|
||||
|
||||
// Gob serializer
|
||||
type GobSerializer struct{}
|
||||
|
||||
func (gs GobSerializer) Serialize(s *sessions.Session) ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := gob.NewEncoder(buf)
|
||||
err := enc.Encode(s.Values)
|
||||
if err == nil {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (gs GobSerializer) Deserialize(d []byte, s *sessions.Session) error {
|
||||
dec := gob.NewDecoder(bytes.NewBuffer(d))
|
||||
return dec.Decode(&s.Values)
|
||||
}
|
||||
|
||||
// generateRandomKey returns a new random key
|
||||
func generateRandomKey() (string, error) {
|
||||
k := make([]byte, 64)
|
||||
if _, err := io.ReadFull(rand.Reader, k); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(base32.StdEncoding.EncodeToString(k), "="), nil
|
||||
}
|
158
internal/outpost/proxyv2/redisstore/redisstore_test.go
Normal file
158
internal/outpost/proxyv2/redisstore/redisstore_test.go
Normal file
@ -0,0 +1,158 @@
|
||||
package redisstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const (
|
||||
redisAddr = "localhost:6379"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://www.example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create request", err)
|
||||
}
|
||||
|
||||
session, err := store.New(req, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create session", err)
|
||||
}
|
||||
if session.IsNew == false {
|
||||
t.Fatal("session is not new")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
opts := sessions.Options{
|
||||
Path: "/path",
|
||||
MaxAge: 99999,
|
||||
}
|
||||
store.Options(opts)
|
||||
|
||||
req, err := http.NewRequest("GET", "http://www.example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create request", err)
|
||||
}
|
||||
|
||||
session, err := store.New(req, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create store", err)
|
||||
}
|
||||
if session.Options.Path != opts.Path || session.Options.MaxAge != opts.MaxAge {
|
||||
t.Fatal("failed to set options")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://www.example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create request", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
session, err := store.New(req, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create session", err)
|
||||
}
|
||||
|
||||
session.Values["key"] = "value"
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
t.Fatal("failed to save: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://www.example.com", nil)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create request", err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
session, err := store.New(req, "hello")
|
||||
if err != nil {
|
||||
t.Fatal("failed to create session", err)
|
||||
}
|
||||
|
||||
session.Values["key"] = "value"
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
t.Fatal("failed to save session: ", err)
|
||||
}
|
||||
|
||||
session.Options.MaxAge = -1
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
t.Fatal("failed to delete session: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddr,
|
||||
})
|
||||
|
||||
cmd := client.Ping(context.Background())
|
||||
err := cmd.Err()
|
||||
if err != nil {
|
||||
t.Fatal("connection is not opened")
|
||||
}
|
||||
|
||||
store, err := NewRedisStore(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatal("failed to create redis store", err)
|
||||
}
|
||||
|
||||
err = store.Close()
|
||||
if err != nil {
|
||||
t.Fatal("failed to close")
|
||||
}
|
||||
|
||||
cmd = client.Ping(context.Background())
|
||||
if cmd.Err() == nil {
|
||||
t.Fatal("connection is properly closed")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user