diff --git a/backend/go.mod b/backend/go.mod index b33d63e..8c671f7 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -19,6 +19,7 @@ require ( buf.build/go/protovalidate v1.1.0 // indirect cel.dev/expr v0.24.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index f9931ff..27f6b6b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -10,6 +10,8 @@ connectrpc.com/validate v0.6.0 h1:DcrgDKt2ZScrUs/d/mh9itD2yeEa0UbBBa+i0mwzx+4= connectrpc.com/validate v0.6.0/go.mod h1:ihrpI+8gVbLH1fvVWJL1I3j0CfWnF8P/90LsmluRiZs= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a h1:dIdcLbck6W67B5JFMewU5Dba1yKZA3MsT67i4No/zh0= +github.com/antonlindstrom/pgstore v0.0.0-20220421113606-e3a6e3fed12a/go.mod h1:Sdr/tmSOLEnncCuXS5TwZRxuk7deH1WXVY8cve3eVBM= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= diff --git a/backend/internal/auth/oidc.go b/backend/internal/auth/oidc.go index 516ba72..e392782 100644 --- a/backend/internal/auth/oidc.go +++ b/backend/internal/auth/oidc.go @@ -3,6 +3,7 @@ package auth import ( "context" "crypto/rand" + "database/sql" "encoding/base64" "fmt" "io" @@ -11,18 +12,20 @@ import ( "time" "echsen.club/radio/internal/config" + "github.com/antonlindstrom/pgstore" "github.com/coreos/go-oidc/v3/oidc" "github.com/gorilla/sessions" "golang.org/x/oauth2" ) type OauthService struct { - Store *sessions.CookieStore + Store sessions.Store Oauth2Config oauth2.Config Verifier *oidc.IDTokenVerifier + LogoutUrl string } -func NewOauthService(config config.AuthConfig) (*OauthService, error) { +func NewOauthService(db *sql.DB, config config.AuthConfig) (*OauthService, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -39,18 +42,30 @@ func NewOauthService(config config.AuthConfig) (*OauthService, error) { Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } - var store = sessions.NewCookieStore([]byte(config.SessionKey)) + var claims struct { + EndSessionEndpoint string `json:"end_session_endpoint"` + } + if err := provider.Claims(&claims); err != nil { + return nil, fmt.Errorf("failed to extract provider claims: %v", err) + } + + store, err := pgstore.NewPGStoreFromPool(db, []byte(config.SessionKey)) + if err != nil { + return nil, fmt.Errorf("could not create pgstore: %v", err) + } store.Options = &sessions.Options{ Path: "/", - MaxAge: 3600 * 8, - HttpOnly: true, - Secure: false, - SameSite: http.SameSiteLaxMode, + MaxAge: 3600 * 8, + HttpOnly: true, + Secure: false, // Set true in production + SameSite: http.SameSiteLaxMode, } + service := OauthService{ Store: store, Oauth2Config: oauth2Config, Verifier: provider.Verifier(&oidc.Config{ClientID: config.ClientId }), + LogoutUrl: claims.EndSessionEndpoint, } slog.Info("Created OIDC Config") @@ -153,3 +168,33 @@ func (s *OauthService) CallbackHandler(onSuccess func(w http.ResponseWriter, r * }) } } + +// func (s *OauthService) BackchannelLogoutHandler(w http.ResponseWriter, r *http.Request) { +// rawToken := r.FormValue("logout_token") +// +// token, err := s.Verifier.Verify(r.Context(), rawToken) +// if err != nil { +// http.Error(w, "Invalid logout token", http.StatusBadRequest) +// return +// } +// +// // 2. Extract the 'sid' (Session ID) or 'sub' (Subject ID) +// var claims struct { +// Sid string `json:"sid"` +// Sub string `json:"sub"` +// } +// token.Claims(&claims) +// +// pgStore, ok := s.Store.(*pgstore.PGStore) +// if !ok { +// slog.Error("Store is not a PGStore") +// return +// } +// +// // 3. Delete from DB +// // Here we need to tell the Session Store to destroy the session by its ID +// // or manually run a SQL query against the http_sessions table +// err = pgSkj +// +// w.WriteHeader(http.StatusOK) +// } diff --git a/backend/internal/radio/handler.go b/backend/internal/radio/handler.go index 44a4a0b..0c4b809 100644 --- a/backend/internal/radio/handler.go +++ b/backend/internal/radio/handler.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "connectrpc.com/connect" "connectrpc.com/validate" @@ -111,3 +112,27 @@ func (h *Handler) handleAuthSuccess(w http.ResponseWriter, r *http.Request, clai http.Redirect(w, r, "/", http.StatusFound) } + +func (h *Handler) LogoutHandler(w http.ResponseWriter, r *http.Request) { + session, _ := h.service.oauth.Store.Get(r, "auth-session") + session.Options.MaxAge = -1 + session.Save(r, w) + + if h.service.oauth.LogoutUrl == "" { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + target, err := url.Parse(h.service.oauth.LogoutUrl) + if err != nil { + http.Redirect(w, r, "/", http.StatusFound) + return + } + + q := target.Query() + q.Set("post_logout_redirect_uri", "http://localhost:8080/") + q.Set("client_id", h.service.oauth.Oauth2Config.ClientID) + target.RawQuery = q.Encode() + + http.Redirect(w, r, target.String(), http.StatusFound) +} diff --git a/backend/internal/radio/service.go b/backend/internal/radio/service.go index 478b6bf..cbcc672 100644 --- a/backend/internal/radio/service.go +++ b/backend/internal/radio/service.go @@ -14,7 +14,7 @@ type Service struct { } func NewService(db *sql.DB, config config.Config) *Service { - oauthService, err := auth.NewOauthService(config.AuthConfig) + oauthService, err := auth.NewOauthService(db, config.AuthConfig) if err != nil { slog.Error("Error while creating oidc service", "error", err) } diff --git a/backend/migrations/20260112193504_add_sessions_table.sql b/backend/migrations/20260112193504_add_sessions_table.sql new file mode 100644 index 0000000..1ab32d7 --- /dev/null +++ b/backend/migrations/20260112193504_add_sessions_table.sql @@ -0,0 +1,18 @@ +-- +goose Up +-- +goose StatementBegin +CREATE TABLE IF NOT EXISTS http_sessions ( + id SERIAL PRIMARY KEY, + key BYTEA, + data BYTEA, + created_on TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + modified_on TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + expires_on TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_http_sessions_expires_on ON http_sessions (expires_on); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE http_sessions; +-- +goose StatementEnd