diff --git a/backend/config/settings.go b/backend/config/settings.go index d780f26c..32b3fafc 100644 --- a/backend/config/settings.go +++ b/backend/config/settings.go @@ -3,7 +3,7 @@ package config type Settings struct { Application ApplicationSettings Database DatabaseSettings - RedisSession RedisSettings + Session SessionSettings RedisLimiter RedisSettings SuperUser SuperUserSettings Calendar CalendarSettings @@ -22,6 +22,7 @@ type intermediateSettings struct { Application ApplicationSettings `envPrefix:"SAC_APPLICATION_"` Database intermediateDatabaseSettings `envPrefix:"SAC_DB_"` RedisSession intermediateRedisSettings `envPrefix:"SAC_REDIS_SESSION_"` + Session intermediateSessionSettings `envPrefix:"SAC_SESSION_"` RedisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"` SuperUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"` AWS intermediateAWSSettings `envPrefix:"SAC_AWS_"` @@ -38,6 +39,16 @@ func (i *intermediateSettings) into() (*Settings, error) { return nil, err } + redisSession, err := i.RedisSession.into() + if err != nil { + return nil, err + } + + session, err := i.Session.into(*redisSession) + if err != nil { + return nil, err + } + redisLimiter, err := i.RedisLimiter.into() if err != nil { return nil, err @@ -76,6 +87,7 @@ func (i *intermediateSettings) into() (*Settings, error) { return &Settings{ Application: i.Application, Database: *database, + Session: *session, RedisLimiter: *redisLimiter, SuperUser: *superUser, Calendar: *calendar, diff --git a/backend/database/store/storer.go b/backend/database/store/storer.go index 838e121d..00978c1d 100644 --- a/backend/database/store/storer.go +++ b/backend/database/store/storer.go @@ -3,6 +3,7 @@ package store import ( "context" "fmt" + "log/slog" "runtime" "time" @@ -51,10 +52,12 @@ func NewRedisClient(settings RedisSettings) *RedisClient { } func (r *RedisClient) Get(key string) ([]byte, error) { + slog.Info("getting", "key", key) return r.client.Get(context.Background(), key).Bytes() } func (r *RedisClient) Set(key string, val []byte, exp time.Duration) error { + slog.Info("setting", "key", key, "val", string(val), "exp", exp) return r.client.Set(context.Background(), key, val, exp).Err() } diff --git a/backend/entities/auth/base/handlers.go b/backend/entities/auth/base/handlers.go index 6ecd5c81..8402fa43 100644 --- a/backend/entities/auth/base/handlers.go +++ b/backend/entities/auth/base/handlers.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" "net/http" + "net/url" "time" "github.com/GenerateNU/sac/backend/integrations/oauth/soth" @@ -101,7 +102,12 @@ func (h *Handler) ProviderCallback(c *fiber.Ctx) error { return err } - return c.Redirect(c.Cookies("redirect", "/")) + redirect, err := url.PathUnescape(c.Cookies("redirect", "/")) + if err != nil { + return err + } + + return c.Redirect(redirect) } func (h *Handler) ProviderLogout(c *fiber.Ctx) error { diff --git a/backend/fiber/memory/config.go b/backend/fiber/memory/config.go deleted file mode 100644 index 07d13edb..00000000 --- a/backend/fiber/memory/config.go +++ /dev/null @@ -1,33 +0,0 @@ -package memory - -import "time" - -// Config defines the config for storage. -type Config struct { - // Time before deleting expired keys - // - // Default is 10 * time.Second - GCInterval time.Duration -} - -// ConfigDefault is the default config -var ConfigDefault = Config{ - GCInterval: 10 * time.Second, -} - -// configDefault is a helper function to set default values -func configDefault(config ...Config) Config { - // Return default config if nothing provided - if len(config) < 1 { - return ConfigDefault - } - - // Override default config - cfg := config[0] - - // Set default values - if int(cfg.GCInterval.Seconds()) <= 0 { - cfg.GCInterval = ConfigDefault.GCInterval - } - return cfg -} diff --git a/backend/fiber/memory/memory.go b/backend/fiber/memory/memory.go deleted file mode 100644 index 8f53267e..00000000 --- a/backend/fiber/memory/memory.go +++ /dev/null @@ -1,143 +0,0 @@ -package memory - -import ( - "sync" - "sync/atomic" - "time" - - "github.com/gofiber/fiber/v2/utils" -) - -// Storage interface that is implemented by storage providers -type Storage struct { - mux sync.RWMutex - db map[string]entry - gcInterval time.Duration - done chan struct{} -} - -type entry struct { - data []byte - // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 - expiry uint32 -} - -// New creates a new memory storage -func New(config ...Config) *Storage { - // Set default config - cfg := configDefault(config...) - - // Create storage - store := &Storage{ - db: make(map[string]entry), - gcInterval: cfg.GCInterval, - done: make(chan struct{}), - } - - // Start garbage collector - utils.StartTimeStampUpdater() - go store.gc() - - return store -} - -// Get value by key -func (s *Storage) Get(key string) ([]byte, error) { - if len(key) <= 0 { - return nil, nil - } - s.mux.RLock() - v, ok := s.db[key] - s.mux.RUnlock() - if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&utils.Timestamp) { - return nil, nil - } - - return v.data, nil -} - -// Set key with value -func (s *Storage) Set(key string, val []byte, exp time.Duration) error { - // Ain't Nobody Got Time For That - if len(key) <= 0 || len(val) <= 0 { - return nil - } - - var expire uint32 - if exp != 0 { - expire = uint32(exp.Seconds()) + atomic.LoadUint32(&utils.Timestamp) - } - - e := entry{val, expire} - s.mux.Lock() - s.db[key] = e - s.mux.Unlock() - return nil -} - -// Delete key by key -func (s *Storage) Delete(key string) error { - // Ain't Nobody Got Time For That - if len(key) <= 0 { - return nil - } - s.mux.Lock() - delete(s.db, key) - s.mux.Unlock() - return nil -} - -// Reset all keys -func (s *Storage) Reset() error { - ndb := make(map[string]entry) - s.mux.Lock() - s.db = ndb - s.mux.Unlock() - return nil -} - -// Close the memory storage -func (s *Storage) Close() error { - s.done <- struct{}{} - return nil -} - -func (s *Storage) gc() { - ticker := time.NewTicker(s.gcInterval) - defer ticker.Stop() - var expired []string - - for { - select { - case <-s.done: - return - case <-ticker.C: - ts := atomic.LoadUint32(&utils.Timestamp) - expired = expired[:0] - s.mux.RLock() - for id, v := range s.db { - if v.expiry != 0 && v.expiry <= ts { - expired = append(expired, id) - } - } - s.mux.RUnlock() - s.mux.Lock() - // Double-checked locking. - // We might have replaced the item in the meantime. - for i := range expired { - v := s.db[expired[i]] - if v.expiry != 0 && v.expiry <= ts { - delete(s.db, expired[i]) - } - } - s.mux.Unlock() - } - } -} - -// Return database client -func (s *Storage) Conn() map[string]entry { - s.mux.RLock() - defer s.mux.RUnlock() - return s.db -} diff --git a/backend/fiber/session/config.go b/backend/fiber/session/config.go deleted file mode 100644 index fe74132f..00000000 --- a/backend/fiber/session/config.go +++ /dev/null @@ -1,128 +0,0 @@ -package session - -import ( - "fmt" - "strings" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/gofiber/fiber/v2/utils" -) - -// Config defines the config for middleware. -type Config struct { - // Allowed session duration - // Optional. Default value 24 * time.Hour - Expiration time.Duration - - // Storage interface to store the session data - // Optional. Default value memory.New() - Storage fiber.Storage - - // KeyLookup is a string in the form of ":" that is used - // to extract session id from the request. - // Possible values: "header:", "query:" or "cookie:" - // Optional. Default value "cookie:session_id". - KeyLookup string - - // Domain of the cookie. - // Optional. Default value "". - CookieDomain string - - // Path of the cookie. - // Optional. Default value "". - CookiePath string - - // Indicates if cookie is secure. - // Optional. Default value false. - CookieSecure bool - - // Indicates if cookie is HTTP only. - // Optional. Default value false. - CookieHTTPOnly bool - - // Value of SameSite cookie. - // Optional. Default value "Lax". - CookieSameSite string - - // Decides whether cookie should last for only the browser sesison. - // Ignores Expiration if set to true - // Optional. Default value false. - CookieSessionOnly bool - - // KeyGenerator generates the session key. - // Optional. Default value utils.UUIDv4 - KeyGenerator func() string - - // Deprecated: Please use KeyLookup - CookieName string - - // Source defines where to obtain the session id - source Source - - // The session name - sessionName string -} - -type Source string - -const ( - SourceCookie Source = "cookie" - SourceHeader Source = "header" - SourceURLQuery Source = "query" -) - -// ConfigDefault is the default config -var ConfigDefault = Config{ - Expiration: 24 * time.Hour, - KeyLookup: "cookie:session_id", - KeyGenerator: utils.UUIDv4, - source: "cookie", - sessionName: "session_id", -} - -// Helper function to set default values -func configDefault(config ...Config) Config { - // Return default config if nothing provided - if len(config) < 1 { - return ConfigDefault - } - - // Override default config - cfg := config[0] - - // Set default values - if int(cfg.Expiration.Seconds()) <= 0 { - cfg.Expiration = ConfigDefault.Expiration - } - if cfg.CookieName != "" { - log.Warn("[SESSION] CookieName is deprecated, please use KeyLookup") - cfg.KeyLookup = fmt.Sprintf("cookie:%s", cfg.CookieName) - } - if cfg.KeyLookup == "" { - cfg.KeyLookup = ConfigDefault.KeyLookup - } - if cfg.KeyGenerator == nil { - cfg.KeyGenerator = ConfigDefault.KeyGenerator - } - - selectors := strings.Split(cfg.KeyLookup, ":") - const numSelectors = 2 - if len(selectors) != numSelectors { - panic("[session] KeyLookup must in the form of :") - } - switch Source(selectors[0]) { - case SourceCookie: - cfg.source = SourceCookie - case SourceHeader: - cfg.source = SourceHeader - case SourceURLQuery: - cfg.source = SourceURLQuery - default: - panic("[session] source is not supported") - } - cfg.sessionName = selectors[1] - - return cfg -} diff --git a/backend/fiber/session/data.go b/backend/fiber/session/data.go deleted file mode 100644 index 75024f80..00000000 --- a/backend/fiber/session/data.go +++ /dev/null @@ -1,63 +0,0 @@ -package session - -import ( - "sync" -) - -// go:generate msgp -// msgp -file="data.go" -o="data_msgp.go" -tests=false -unexported -type data struct { - sync.RWMutex - Data map[string]interface{} -} - -var dataPool = sync.Pool{ - New: func() interface{} { - d := new(data) - d.Data = make(map[string]interface{}) - return d - }, -} - -func acquireData() *data { - return dataPool.Get().(*data) //nolint:forcetypeassert // We store nothing else in the pool -} - -func (d *data) Reset() { - d.Lock() - d.Data = make(map[string]interface{}) - d.Unlock() -} - -func (d *data) Get(key string) interface{} { - d.RLock() - v := d.Data[key] - d.RUnlock() - return v -} - -func (d *data) Set(key string, value interface{}) { - d.Lock() - d.Data[key] = value - d.Unlock() -} - -func (d *data) Delete(key string) { - d.Lock() - delete(d.Data, key) - d.Unlock() -} - -func (d *data) Keys() []string { - d.Lock() - keys := make([]string, 0, len(d.Data)) - for k := range d.Data { - keys = append(keys, k) - } - d.Unlock() - return keys -} - -func (d *data) Len() int { - return len(d.Data) -} diff --git a/backend/fiber/session/session.go b/backend/fiber/session/session.go deleted file mode 100644 index 7d471413..00000000 --- a/backend/fiber/session/session.go +++ /dev/null @@ -1,284 +0,0 @@ -package session - -import ( - "bytes" - "encoding/gob" - "fmt" - "sync" - "time" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/utils" - - "github.com/valyala/fasthttp" -) - -type Session struct { - id string // session id - fresh bool // if new session - ctx *fiber.Ctx // fiber context - config *Store // store configuration - data *data // key value data - byteBuffer *bytes.Buffer // byte buffer for the en- and decode - exp time.Duration // expiration of this session -} - -var sessionPool = sync.Pool{ - New: func() interface{} { - return new(Session) - }, -} - -func acquireSession() *Session { - s := sessionPool.Get().(*Session) //nolint:forcetypeassert,errcheck // We store nothing else in the pool - if s.data == nil { - s.data = acquireData() - } - if s.byteBuffer == nil { - s.byteBuffer = new(bytes.Buffer) - } - s.fresh = true - return s -} - -func releaseSession(s *Session) { - s.id = "" - s.exp = 0 - s.ctx = nil - s.config = nil - if s.data != nil { - s.data.Reset() - } - if s.byteBuffer != nil { - s.byteBuffer.Reset() - } - sessionPool.Put(s) -} - -// Fresh is true if the current session is new -func (s *Session) Fresh() bool { - return s.fresh -} - -// ID returns the session id -func (s *Session) ID() string { - return s.id -} - -// Get will return the value -func (s *Session) Get(key string) interface{} { - // Better safe than sorry - if s.data == nil { - return nil - } - return s.data.Get(key) -} - -// Set will update or create a new key value -func (s *Session) Set(key string, val interface{}) { - // Better safe than sorry - if s.data == nil { - return - } - s.data.Set(key, val) -} - -// Delete will delete the value -func (s *Session) Delete(key string) { - // Better safe than sorry - if s.data == nil { - return - } - s.data.Delete(key) -} - -// Destroy will delete the session from Storage and expire session cookie -func (s *Session) Destroy() error { - // Better safe than sorry - if s.data == nil { - return nil - } - - // Reset local data - s.data.Reset() - - // Use external Storage if exist - if err := s.config.Storage.Delete(s.id); err != nil { - return err - } - - // Expire session - s.delSession() - return nil -} - -// Regenerate generates a new session id and delete the old one from Storage -func (s *Session) Regenerate() error { - // Delete old id from storage - if err := s.config.Storage.Delete(s.id); err != nil { - return err - } - - // Generate a new session, and set session.fresh to true - s.refresh() - - return nil -} - -// Reset generates a new session id, deletes the old one from storage, and resets the associated data -func (s *Session) Reset() error { - // Reset local data - if s.data != nil { - s.data.Reset() - } - // Reset byte buffer - if s.byteBuffer != nil { - s.byteBuffer.Reset() - } - // Reset expiration - s.exp = 0 - - // Delete old id from storage - if err := s.config.Storage.Delete(s.id); err != nil { - return err - } - - // Expire session - s.delSession() - - // Generate a new session, and set session.fresh to true - s.refresh() - - return nil -} - -// refresh generates a new session, and set session.fresh to be true -func (s *Session) refresh() { - // Create a new id - s.id = s.config.KeyGenerator() - - // We assign a new id to the session, so the session must be fresh - s.fresh = true -} - -// Save will update the storage and client cookie -func (s *Session) Save() error { - // Better safe than sorry - if s.data == nil { - return nil - } - - // Check if session has your own expiration, otherwise use default value - if s.exp <= 0 { - s.exp = s.config.Expiration - } - - // Update client cookie - s.setSession() - - // Convert data to bytes - mux.Lock() - defer mux.Unlock() - encCache := gob.NewEncoder(s.byteBuffer) - err := encCache.Encode(&s.data.Data) - if err != nil { - return fmt.Errorf("failed to encode data: %w", err) - } - - // copy the data in buffer - encodedBytes := make([]byte, s.byteBuffer.Len()) - copy(encodedBytes, s.byteBuffer.Bytes()) - - // pass copied bytes with session id to provider - if err := s.config.Storage.Set(s.id, encodedBytes, s.exp); err != nil { - return err - } - - // Release session - // TODO: It's not safe to use the Session after called Save() - releaseSession(s) - - return nil -} - -// Keys will retrieve all keys in current session -func (s *Session) Keys() []string { - if s.data == nil { - return []string{} - } - return s.data.Keys() -} - -// SetExpiry sets a specific expiration for this session -func (s *Session) SetExpiry(exp time.Duration) { - s.exp = exp -} - -func (s *Session) setSession() { - fmt.Println(s.config.source) - if s.config.source == SourceHeader { - s.ctx.Request().Header.SetBytesV(s.config.sessionName, []byte(s.id)) - s.ctx.Response().Header.SetBytesV(s.config.sessionName, []byte(s.id)) - fmt.Println(s.ctx.Request().Header) - fmt.Println(s.ctx.Response().Header) - } else { - fcookie := fasthttp.AcquireCookie() - fcookie.SetKey(s.config.sessionName) - fcookie.SetValue(s.id) - fcookie.SetPath(s.config.CookiePath) - fcookie.SetDomain(s.config.CookieDomain) - // Cookies are also session cookies if they do not specify the Expires or Max-Age attribute. - // refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie - if !s.config.CookieSessionOnly { - fcookie.SetMaxAge(int(s.exp.Seconds())) - fcookie.SetExpire(time.Now().Add(s.exp)) - } - fcookie.SetSecure(s.config.CookieSecure) - fcookie.SetHTTPOnly(s.config.CookieHTTPOnly) - - switch utils.ToLower(s.config.CookieSameSite) { - case "strict": - fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) - case "none": - fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) - default: - fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) - } - s.ctx.Response().Header.SetCookie(fcookie) - fasthttp.ReleaseCookie(fcookie) - } -} - -func (s *Session) delSession() { - fmt.Println(s.config.source) - if s.config.source == SourceHeader { - s.ctx.Request().Header.Del(s.config.sessionName) - s.ctx.Response().Header.Del(s.config.sessionName) - fmt.Println(s.ctx.Request().Header) - fmt.Println(s.ctx.Response().Header) - } else { - s.ctx.Request().Header.DelCookie(s.config.sessionName) - s.ctx.Response().Header.DelCookie(s.config.sessionName) - - fcookie := fasthttp.AcquireCookie() - fcookie.SetKey(s.config.sessionName) - fcookie.SetPath(s.config.CookiePath) - fcookie.SetDomain(s.config.CookieDomain) - fcookie.SetMaxAge(-1) - fcookie.SetExpire(time.Now().Add(-1 * time.Minute)) - fcookie.SetSecure(s.config.CookieSecure) - fcookie.SetHTTPOnly(s.config.CookieHTTPOnly) - - switch utils.ToLower(s.config.CookieSameSite) { - case "strict": - fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) - case "none": - fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) - default: - fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) - } - - s.ctx.Response().Header.SetCookie(fcookie) - fasthttp.ReleaseCookie(fcookie) - } -} diff --git a/backend/fiber/session/store.go b/backend/fiber/session/store.go deleted file mode 100644 index ca101960..00000000 --- a/backend/fiber/session/store.go +++ /dev/null @@ -1,154 +0,0 @@ -package session - -import ( - "encoding/gob" - "errors" - "fmt" - "sync" - - "github.com/GenerateNU/sac/backend/fiber/memory" - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/utils" - - "github.com/valyala/fasthttp" -) - -// ErrEmptySessionID is an error that occurs when the session ID is empty. -var ErrEmptySessionID = errors.New("session id cannot be empty") - -type Store struct { - Config -} - -var mux sync.Mutex - -func New(config ...Config) *Store { - // Set default config - cfg := configDefault(config...) - - if cfg.Storage == nil { - cfg.Storage = memory.New() - } - - return &Store{ - cfg, - } -} - -// RegisterType will allow you to encode/decode custom types -// into any Storage provider -func (*Store) RegisterType(i interface{}) { - gob.Register(i) -} - -// Get will get/create a session -func (s *Store) Get(c *fiber.Ctx) (*Session, error) { - var fresh bool - loadData := true - - id := s.getSessionID(c) - - if len(id) == 0 { - fresh = true - var err error - if id, err = s.responseCookies(c); err != nil { - return nil, err - } - } - - // If no key exist, create new one - if len(id) == 0 { - loadData = false - id = s.KeyGenerator() - } - - // Create session object - sess := acquireSession() - sess.ctx = c - sess.config = s - sess.id = id - sess.fresh = fresh - - // Fetch existing data - if loadData { - raw, err := s.Storage.Get(id) - // Unmarshal if we found data - if raw != nil && err == nil { - mux.Lock() - defer mux.Unlock() - _, _ = sess.byteBuffer.Write(raw) //nolint:errcheck // This will never fail - encCache := gob.NewDecoder(sess.byteBuffer) - err := encCache.Decode(&sess.data.Data) - if err != nil { - return nil, fmt.Errorf("failed to decode session data: %w", err) - } - } else if err != nil { - return nil, err - } else { - // both raw and err is nil, which means id is not in the storage - sess.fresh = true - } - } - - return sess, nil -} - -// getSessionID will return the session id from: -// 1. cookie -// 2. http headers -// 3. query string -func (s *Store) getSessionID(c *fiber.Ctx) string { - id := c.Cookies(s.sessionName) - if len(id) > 0 { - return utils.CopyString(id) - } - - if s.source == SourceHeader { - id = string(c.Request().Header.Peek(s.sessionName)) - if len(id) > 0 { - return id - } - } - - if s.source == SourceURLQuery { - id = c.Query(s.sessionName) - if len(id) > 0 { - return utils.CopyString(id) - } - } - - return "" -} - -func (s *Store) responseCookies(c *fiber.Ctx) (string, error) { - // Get key from response cookie - cookieValue := c.Response().Header.PeekCookie(s.sessionName) - if len(cookieValue) == 0 { - return "", nil - } - - cookie := fasthttp.AcquireCookie() - defer fasthttp.ReleaseCookie(cookie) - err := cookie.ParseBytes(cookieValue) - if err != nil { - return "", err - } - - value := make([]byte, len(cookie.Value())) - copy(value, cookie.Value()) - id := string(value) - return id, nil -} - -// Reset will delete all session from the storage -func (s *Store) Reset() error { - return s.Storage.Reset() -} - -// Delete deletes a session by its id. -func (s *Store) Delete(id string) error { - if id == "" { - return ErrEmptySessionID - } - return s.Storage.Delete(id) -} diff --git a/backend/go.mod b/backend/go.mod index 58bbb3e1..04ddbcb6 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -84,7 +84,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.52.0 + github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect go.opentelemetry.io/otel v1.27.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 diff --git a/backend/integrations/oauth/soth/sothic/sothic.go b/backend/integrations/oauth/soth/sothic/sothic.go index f7f9360b..5057a1d2 100644 --- a/backend/integrations/oauth/soth/sothic/sothic.go +++ b/backend/integrations/oauth/soth/sothic/sothic.go @@ -8,21 +8,23 @@ import ( "errors" "fmt" "io" + "log/slog" "net/url" "strings" "github.com/GenerateNU/sac/backend/config" "github.com/GenerateNU/sac/backend/database/store" - "github.com/GenerateNU/sac/backend/fiber/session" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/GenerateNU/sac/backend/integrations/oauth/crypt" "github.com/GenerateNU/sac/backend/integrations/oauth/soth" ) type key int const ( - SessionName string = "_sothic_session" + SessionName string = "_sac_session" // ProviderParamKey can be used as a key in context when passing in a provider ProviderParamKey key = iota @@ -31,14 +33,23 @@ const ( // Session can/should be set by applications using gothic. The default is a cookie store. var ( SessionStore *session.Store + encrypter func(string) (string, error) + decrypter func(string) (string, error) ) // MUST be called before using the package -func Init(sessionSettings config.RedisSettings) { +func Init(sessionSettings config.SessionSettings) { config := session.Config{ - Storage: store.NewRedisClient(sessionSettings), - KeyLookup: fmt.Sprintf("header:%s", SessionName), - KeyGenerator: func() string { return SessionName }, + Storage: store.NewRedisClient(sessionSettings.Redis), + KeyLookup: fmt.Sprintf("header:%s", SessionName), + } + + encrypter = func(s string) (string, error) { + return crypt.Encrypt(s, sessionSettings.PassPhrase.Expose()) + } + + decrypter = func(s string) (string, error) { + return crypt.Decrypt(s, sessionSettings.PassPhrase.Expose()) } SessionStore = session.New(config) @@ -46,8 +57,9 @@ func Init(sessionSettings config.RedisSettings) { /* BeginAuthHandler is a convenience handler for starting the authentication process. -It expects to be able to get the name of the provider from the query parameters -as either "provider" or ":provider". + +It expects to be able to get the name of the provider from the path parameter +":provider" or as set by SetProvider. BeginAuthHandler will redirect the user to the appropriate authentication end-point for the requested provider. @@ -280,6 +292,7 @@ func SetProvider(c *fiber.Ctx, provider string) { func StoreInSession(key string, value string, c *fiber.Ctx) error { session, err := SessionStore.Get(c) if err != nil { + slog.Info("error getting session", "error", err) return err } @@ -323,7 +336,7 @@ func getSessionValue(store *session.Session, key string) (string, error) { return "", err } - return string(s), nil + return decrypter(string(s)) } func updateSessionValue(session *session.Session, key, value string) error { @@ -339,7 +352,12 @@ func updateSessionValue(session *session.Session, key, value string) error { return err } - session.Set(key, b.String()) + encrypted, err := encrypter(b.String()) + if err != nil { + return err + } + + session.Set(key, encrypted) return nil } diff --git a/backend/main.go b/backend/main.go index aea4c560..42d32a19 100644 --- a/backend/main.go +++ b/backend/main.go @@ -67,7 +67,7 @@ func main() { startBackgroundJobs(ctx, db) stores := store.ConfigureStores(config.RedisLimiter) - sothic.Init(config.RedisLimiter) + sothic.Init(config.Session) integrations := configureIntegrations(&config.Integrations) tp := telemetry.InitTracer() diff --git a/backend/middleware/auth/authorize.go b/backend/middleware/auth/authorize.go index 0515d60f..c78a3a12 100644 --- a/backend/middleware/auth/authorize.go +++ b/backend/middleware/auth/authorize.go @@ -1,43 +1,51 @@ package auth import ( + "net/url" + "slices" + "time" + + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" + "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/permission" + "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) func (m *AuthMiddlewareHandler) Authorize(requiredPermissions ...permission.Permission) fiber.Handler { return func(c *fiber.Ctx) error { - // strUser, err := sothic.GetFromSession("user", c) - // if err != nil { - // c.Cookie(&fiber.Cookie{ - // Name: "redirect", - // Value: c.OriginalURL(), - // Expires: time.Now().Add(5 * time.Minute), - // // MARK: secure should be true in prod - // // use go build tags to do this - // HTTPOnly: true, - // }) - - // return c.Redirect("/api/v1/auth/login") - // } - - // user := models.UnmarshalUser(strUser) - - // if user.Role == models.Super { - // locals.SetUser(c, user) - // return c.Next() - // } - - // userPermissions := permission.GetPermissions(user.Role) - - // for _, requiredPermission := range requiredPermissions { - // if !slices.Contains(userPermissions, requiredPermission) { - // return utilities.Forbidden() - // } - // } - - // locals.SetUser(c, user) + strUser, err := sothic.GetFromSession("user", c) + if err != nil { + c.Cookie(&fiber.Cookie{ + Name: "redirect", + Value: url.PathEscape(c.OriginalURL()), + Expires: time.Now().Add(5 * time.Minute), + // MARK: secure should be true in prod + // use go build tags to do this + HTTPOnly: true, + }) + + return c.Redirect("/api/v1/auth/login") + } + + user := models.UnmarshalUser(strUser) + + if user.Role == models.Super { + locals.SetUser(c, user) + return c.Next() + } + + userPermissions := permission.GetPermissions(user.Role) + + for _, requiredPermission := range requiredPermissions { + if !slices.Contains(userPermissions, requiredPermission) { + return utilities.Forbidden() + } + } + + locals.SetUser(c, user) return c.Next() } } diff --git a/backend/server/server.go b/backend/server/server.go index a916d37c..3896d0f9 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -33,6 +33,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/compress" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/requestid" "gorm.io/gorm" @@ -118,6 +119,8 @@ func newFiberApp(appSettings config.ApplicationSettings) *fiber.App { ErrorHandler: utilities.ErrorHandler, }) + app.Use(recover.New()) + app.Use(cors.New(cors.Config{ AllowOrigins: appSettings.ApplicationURL(), AllowCredentials: true, diff --git a/config/.env.template b/config/.env.template index b5242821..e9629296 100644 --- a/config/.env.template +++ b/config/.env.template @@ -15,6 +15,8 @@ SAC_REDIS_SESSION_HOST="127.0.0.1" SAC_REDIS_SESSION_PORT="6380" SAC_REDIS_SESSION_DB="0" +SAC_SESSION_PASSPHRASE="muneer tyler garrett brian alder sac" + SAC_REDIS_LIMITER_USERNAME="redis_limiter" SAC_REDIS_LIMITER_PASSWORD="redis_limiter!#1" SAC_REDIS_LIMITER_HOST="127.0.0.1"