package session import ( "context" "errors" "net/http" "github.com/gorilla/sessions" "github.com/stashapp/stash/pkg/logger" ) type key int const ( contextUser key = iota contextVisitedPlugins ) const ( userIDKey = "userID" visitedPluginHooksKey = "visitedPluginsHooks" ) const ( ApiKeyHeader = "ApiKey" ApiKeyParameter = "apikey" ) const ( cookieName = "session" usernameFormKey = "username" passwordFormKey = "password" ) type InvalidCredentialsError struct { Username string } func (e InvalidCredentialsError) Error() string { // don't leak the username return "invalid credentials" } var ErrUnauthorized = errors.New("unauthorized") type Store struct { sessionStore *sessions.CookieStore config SessionConfig } func NewStore(c SessionConfig) *Store { ret := &Store{ sessionStore: sessions.NewCookieStore(c.GetSessionStoreKey()), config: c, } ret.sessionStore.MaxAge(c.GetMaxSessionAge()) ret.sessionStore.Options.SameSite = http.SameSiteLaxMode return ret } func (s *Store) Login(w http.ResponseWriter, r *http.Request) error { // ignore error - we want a new session regardless newSession, _ := s.sessionStore.Get(r, cookieName) username := r.FormValue(usernameFormKey) password := r.FormValue(passwordFormKey) // authenticate the user if !s.config.ValidateCredentials(username, password) { return &InvalidCredentialsError{Username: username} } // since we only have one user, don't leak the name logger.Info("User logged in") newSession.Values[userIDKey] = username err := newSession.Save(r, w) if err != nil { return err } return nil } func (s *Store) Logout(w http.ResponseWriter, r *http.Request) error { session, err := s.sessionStore.Get(r, cookieName) if err != nil { return err } delete(session.Values, userIDKey) session.Options.MaxAge = -1 err = session.Save(r, w) if err != nil { return err } // since we only have one user, don't leak the name logger.Infof("User logged out") return nil } func (s *Store) GetSessionUserID(w http.ResponseWriter, r *http.Request) (string, error) { session, err := s.sessionStore.Get(r, cookieName) // ignore errors and treat as an empty user id, so that we handle expired // cookie if err != nil { return "", nil } if !session.IsNew { val := session.Values[userIDKey] // refresh the cookie err = session.Save(r, w) if err != nil { return "", err } ret, _ := val.(string) return ret, nil } return "", nil } func SetCurrentUserID(ctx context.Context, userID string) context.Context { return context.WithValue(ctx, contextUser, userID) } // GetCurrentUserID gets the current user id from the provided context func GetCurrentUserID(ctx context.Context) *string { userCtxVal := ctx.Value(contextUser) if userCtxVal != nil { currentUser := userCtxVal.(string) return ¤tUser } return nil } func (s *Store) Authenticate(w http.ResponseWriter, r *http.Request) (userID string, err error) { c := s.config // translate api key into current user, if present apiKey := r.Header.Get(ApiKeyHeader) // try getting the api key as a query parameter if apiKey == "" { apiKey = r.URL.Query().Get(ApiKeyParameter) } if apiKey != "" { // match against configured API and set userID to the // configured username. In future, we'll want to // get the username from the key. if c.GetAPIKey() != apiKey { return "", ErrUnauthorized } userID = c.GetUsername() } else { // handle session userID, err = s.GetSessionUserID(w, r) } if err != nil { return "", err } return }