/* Copyright © 2025 Wiki Cafe This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package cmd import ( "log" "net/http" "net/url" "context" "git.coopcloud.tech/wiki-cafe/member-console/internal/middleware" "github.com/coreos/go-oidc/v3/oidc" "github.com/gorilla/sessions" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/oauth2" ) var startCmd = &cobra.Command{ Use: "start", Short: "Start serving the member-console web application", Long: `The start command starts an HTTP server that serves the member-console web application from the components directory in the current directory. The server listens on port 8080 by default, unless a different port is specified using the --port flag.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { // Retrieve the port value from Viper port := viper.GetString("port") // Create a new HTTP request router httpRequestRouter := http.NewServeMux() // Add to Run function store := sessions.NewCookieStore( []byte(viper.GetString("session-secret")), ) store.Options = &sessions.Options{ HttpOnly: true, Secure: viper.GetString("env") == "production", SameSite: http.SameSiteLaxMode, MaxAge: 86400 * 7, // 1 week } // OIDC Provider provider, err := oidc.NewProvider(context.Background(), viper.GetString("issuer-url")) if err != nil { log.Fatal("Failed to initialize OIDC provider:", err) } // OAuth2 Config oauthConfig := &oauth2.Config{ ClientID: viper.GetString("client-id"), ClientSecret: viper.GetString("client-secret"), RedirectURL: viper.GetString("redirect-url"), Endpoint: provider.Endpoint(), Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } authConfig := &middleware.AuthConfig{ Store: store, OAuthConfig: oauthConfig, Verifier: provider.Verifier(&oidc.Config{ClientID: oauthConfig.ClientID}), } // Register handlers httpRequestRouter.HandleFunc("/login", middleware.LoginHandler(authConfig)) httpRequestRouter.HandleFunc("/callback", middleware.CallbackHandler(authConfig)) // Update the logout handler to include Keycloak integration httpRequestRouter.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { // 1. Get session and immediately expire it session, _ := authConfig.Store.Get(r, "auth-session") session.Values["authenticated"] = false session.Options.MaxAge = -1 // Immediate deletion session.Save(r, w) // 2. Keycloak logout parameters keycloakLogoutURL, err := url.Parse(viper.GetString("issuer-url") + "/protocol/openid-connect/logout") if err != nil { log.Printf("Error parsing logout URL: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // 3. Build logout URL with post_logout_redirect_uri and id_token_hint q := keycloakLogoutURL.Query() q.Set("post_logout_redirect_uri", viper.GetString("redirect-url")) q.Set("client_id", viper.GetString("client-id")) // Retrieve ID token from session if available if idToken, ok := session.Values["id_token"].(string); ok { q.Set("id_token_hint", idToken) } keycloakLogoutURL.RawQuery = q.Encode() // 4. Redirect to Keycloak for global session termination http.Redirect(w, r, keycloakLogoutURL.String(), http.StatusFound) }) // Update middleware stack stack := middleware.CreateStack( middleware.Logging, middleware.AuthMiddleware(authConfig), ) server := http.Server{ Addr: ":" + port, Handler: stack(httpRequestRouter), } // Serve the components directory httpRequestRouter.Handle("/", http.FileServer(http.Dir("./components"))) log.Println("Starting server on port", port) log.Fatal(server.ListenAndServe()) }, } func init() { // Register the port flag with Cobra startCmd.Flags().StringP("port", "p", "", "Port to listen on") startCmd.Flags().String("client-id", "", "OIDC Client ID") startCmd.Flags().String("client-secret", "", "OIDC Client Secret") startCmd.Flags().String("issuer-url", "", "Keycloak Issuer URL") startCmd.Flags().String("redirect-url", "", "OAuth Redirect URL") startCmd.Flags().String("session-secret", "", "Session encryption secret") // Bind the flags to Viper viper.BindPFlags(startCmd.Flags()) // Set a default value for the port if no flag or env variable is provided viper.SetDefault("port", "8080") // Add the command to the root command rootCmd.AddCommand(startCmd) }