feat: new openid connect authentication layer
Some checks are pending
Cadoles/bouncer/pipeline/pr-develop Build started...
Some checks are pending
Cadoles/bouncer/pipeline/pr-develop Build started...
This commit is contained in:
62
internal/session/adapter/redis/adapter.go
Normal file
62
internal/session/adapter/redis/adapter.go
Normal file
@ -0,0 +1,62 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/session"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type StoreAdapter struct {
|
||||
client redis.UniversalClient
|
||||
}
|
||||
|
||||
// Del implements authn.StoreAdapter.
|
||||
func (s *StoreAdapter) Del(ctx context.Context, key string) error {
|
||||
if err := s.client.Del(ctx, key).Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get implements authn.StoreAdapter.
|
||||
func (s *StoreAdapter) Get(ctx context.Context, key string) ([]byte, error) {
|
||||
cmd := s.client.Get(ctx, key)
|
||||
|
||||
if err := cmd.Err(); err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return nil, errors.WithStack(session.ErrNotFound)
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
data, err := cmd.Bytes()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Set implements authn.StoreAdapter.
|
||||
func (s *StoreAdapter) Set(ctx context.Context, key string, data []byte, ttl time.Duration) error {
|
||||
if err := s.client.Set(ctx, key, data, ttl).Err(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewStoreAdapter(client redis.UniversalClient) *StoreAdapter {
|
||||
return &StoreAdapter{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ session.StoreAdapter = &StoreAdapter{}
|
||||
)
|
47
internal/session/options.go
Normal file
47
internal/session/options.go
Normal file
@ -0,0 +1,47 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Session sessions.Options
|
||||
KeyPrefix string
|
||||
}
|
||||
|
||||
type OptionFunc func(opts *Options)
|
||||
|
||||
func NewOptions(funcs ...OptionFunc) *Options {
|
||||
opts := &Options{
|
||||
Session: sessions.Options{
|
||||
Path: "/",
|
||||
Domain: "",
|
||||
MaxAge: int(time.Hour.Seconds()),
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
SameSite: http.SameSiteDefaultMode,
|
||||
},
|
||||
KeyPrefix: "session:",
|
||||
}
|
||||
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func WithSessionOptions(options sessions.Options) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.Session = options
|
||||
}
|
||||
}
|
||||
|
||||
func WithKeyPrefix(prefix string) OptionFunc {
|
||||
return func(opts *Options) {
|
||||
opts.KeyPrefix = prefix
|
||||
}
|
||||
}
|
182
internal/session/store.go
Normal file
182
internal/session/store.go
Normal file
@ -0,0 +1,182 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base32"
|
||||
"encoding/gob"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type StoreAdapter interface {
|
||||
Set(ctx context.Context, key string, data []byte, ttl time.Duration) error
|
||||
Del(ctx context.Context, key string) error
|
||||
Get(ctx context.Context, key string) ([]byte, error)
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
adapter StoreAdapter
|
||||
options sessions.Options
|
||||
keyPrefix string
|
||||
keyGen KeyGenFunc
|
||||
serializer SessionSerializer
|
||||
}
|
||||
|
||||
type KeyGenFunc func() (string, error)
|
||||
|
||||
func NewStore(adapter StoreAdapter, funcs ...OptionFunc) *Store {
|
||||
opts := NewOptions(funcs...)
|
||||
rs := &Store{
|
||||
options: opts.Session,
|
||||
adapter: adapter,
|
||||
keyPrefix: opts.KeyPrefix,
|
||||
keyGen: generateRandomKey,
|
||||
serializer: GobSerializer{},
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func (s *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
|
||||
return sessions.GetRegistry(r).Get(s, name)
|
||||
}
|
||||
|
||||
func (s *Store) 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 !errors.Is(err, ErrNotFound) {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (s *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
|
||||
if session.Options.MaxAge <= 0 {
|
||||
if err := s.delete(r.Context(), session); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
if session.ID == "" {
|
||||
id, err := s.keyGen()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate session id")
|
||||
}
|
||||
session.ID = id
|
||||
}
|
||||
if err := s.save(r.Context(), session); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
http.SetCookie(w, sessions.NewCookie(session.Name(), session.ID, session.Options))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) Options(opts sessions.Options) {
|
||||
s.options = opts
|
||||
}
|
||||
|
||||
func (s *Store) KeyPrefix(keyPrefix string) {
|
||||
s.keyPrefix = keyPrefix
|
||||
}
|
||||
|
||||
func (s *Store) KeyGen(f KeyGenFunc) {
|
||||
s.keyGen = f
|
||||
}
|
||||
|
||||
func (s *Store) Serializer(ss SessionSerializer) {
|
||||
s.serializer = ss
|
||||
}
|
||||
|
||||
func (s *Store) save(ctx context.Context, session *sessions.Session) error {
|
||||
b, err := s.serializer.Serialize(session)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := s.adapter.Set(ctx, s.keyPrefix+session.ID, b, time.Duration(session.Options.MaxAge)*time.Second); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// load reads session from Redis
|
||||
func (s *Store) load(ctx context.Context, session *sessions.Session) error {
|
||||
data, err := s.adapter.Get(ctx, s.keyPrefix+session.ID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return s.serializer.Deserialize(data, session)
|
||||
}
|
||||
|
||||
// delete deletes session in Redis
|
||||
func (s *Store) delete(ctx context.Context, session *sessions.Session) error {
|
||||
if err := s.adapter.Del(ctx, s.keyPrefix+session.ID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
if err := enc.Encode(s.Values); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
|
||||
}
|
||||
|
||||
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 "", errors.WithStack(err)
|
||||
}
|
||||
return strings.TrimRight(base32.StdEncoding.EncodeToString(k), "="), nil
|
||||
}
|
Reference in New Issue
Block a user