diff --git a/src/api.go b/src/api.go index 41f08c4..2ea57fd 100644 --- a/src/api.go +++ b/src/api.go @@ -77,21 +77,9 @@ func RegisterTLSAPIs(authRouter *auth.RouterDef) { authRouter.HandleFunc("/api/cert/delete", handleCertRemove) } -// Register the APIs for SSO and Oauth functions, WIP -func RegisterSSOAPIs(authRouter *auth.RouterDef) { - authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus) - authRouter.HandleFunc("/api/sso/enable", ssoHandler.HandleSSOEnable) - authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange) - authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL) - - authRouter.HandleFunc("/api/sso/app/register", ssoHandler.HandleRegisterApp) - //authRouter.HandleFunc("/api/sso/app/list", ssoHandler.HandleListApp) - //authRouter.HandleFunc("/api/sso/app/remove", ssoHandler.HandleRemoveApp) - - authRouter.HandleFunc("/api/sso/user/list", ssoHandler.HandleListUser) - authRouter.HandleFunc("/api/sso/user/add", ssoHandler.HandleAddUser) - authRouter.HandleFunc("/api/sso/user/edit", ssoHandler.HandleEditUser) - authRouter.HandleFunc("/api/sso/user/remove", ssoHandler.HandleRemoveUser) +// Register the APIs for Authentication handlers like Authelia and OAUTH2 +func RegisterAuthenticationHandlerAPIs(authRouter *auth.RouterDef) { + authRouter.HandleFunc("/api/sso/Authelia", autheliaRouter.HandleSetAutheliaURLAndHTTPS) } // Register the APIs for redirection rules management functions @@ -339,7 +327,7 @@ func initAPIs(targetMux *http.ServeMux) { RegisterAuthAPIs(requireAuth, targetMux) RegisterHTTPProxyAPIs(authRouter) RegisterTLSAPIs(authRouter) - //RegisterSSOAPIs(authRouter) + RegisterAuthenticationHandlerAPIs(authRouter) RegisterRedirectionAPIs(authRouter) RegisterAccessRuleAPIs(authRouter) RegisterPathRuleAPIs(authRouter) diff --git a/src/def.go b/src/def.go index 10a940e..a2a7cd4 100644 --- a/src/def.go +++ b/src/def.go @@ -16,7 +16,7 @@ import ( "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" - "imuslab.com/zoraxy/mod/auth/sso" + "imuslab.com/zoraxy/mod/auth/sso/authelia" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/dockerux" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" @@ -43,7 +43,7 @@ const ( /* Build Constants */ SYSTEM_NAME = "Zoraxy" SYSTEM_VERSION = "3.1.5" - DEVELOPMENT_BUILD = false /* Development: Set to false to use embedded web fs */ + DEVELOPMENT_BUILD = true /* Development: Set to false to use embedded web fs */ /* System Constants */ DATABASE_PATH = "sys.db" @@ -128,7 +128,9 @@ var ( staticWebServer *webserv.WebServer //Static web server for hosting simple stuffs forwardProxy *forwardproxy.Handler //HTTP Forward proxy, basically VPN for web browser loadBalancer *loadbalance.RouteManager //Global scope loadbalancer, store the state of the lb routing - ssoHandler *sso.SSOHandler //Single Sign On handler + + //Authentication Provider + autheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication //Helper modules EmailSender *email.Sender //Email sender that handle email sending diff --git a/src/mod/auth/sso/app.go b/src/mod/auth/sso/app.go deleted file mode 100644 index a1e574d..0000000 --- a/src/mod/auth/sso/app.go +++ /dev/null @@ -1,34 +0,0 @@ -package sso - -/* - app.go - - This file contains the app structure and app management - functions for the SSO module. - -*/ - -// RegisteredUpstreamApp is a structure that contains the information of an -// upstream app that is registered with the SSO server -type RegisteredUpstreamApp struct { - ID string - Secret string - Domain []string - Scopes []string - SessionDuration int //in seconds, default to 1 hour -} - -// RegisterUpstreamApp registers an upstream app with the SSO server -func (s *SSOHandler) ListRegisteredApps() []*RegisteredUpstreamApp { - apps := make([]*RegisteredUpstreamApp, 0) - for _, app := range s.Apps { - apps = append(apps, &app) - } - return apps -} - -// RegisterUpstreamApp registers an upstream app with the SSO server -func (s *SSOHandler) GetAppByID(appID string) (*RegisteredUpstreamApp, bool) { - app, ok := s.Apps[appID] - return &app, ok -} diff --git a/src/mod/auth/sso/authelia/authelia.go b/src/mod/auth/sso/authelia/authelia.go index f60cebf..075e97f 100644 --- a/src/mod/auth/sso/authelia/authelia.go +++ b/src/mod/auth/sso/authelia/authelia.go @@ -1,50 +1,99 @@ package authelia import ( + "encoding/json" "errors" "fmt" "net/http" "net/url" - "imuslab.com/zoraxy/mod/dynamicproxy" + "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/info/logger" + "imuslab.com/zoraxy/mod/utils" ) -type Options struct { - AutheliaURL string //URL of the Authelia server, e.g. authelia.example.com - UseHTTPS bool //Whether to use HTTPS for the Authelia server - Logger logger.Logger +type AutheliaRouterOptions struct { + UseHTTPS bool //If the Authelia server is using HTTPS + AutheliaURL string //The URL of the Authelia server + Logger *logger.Logger + Database *database.Database } -type AutheliaHandler struct { - options *Options +type AutheliaRouter struct { + options *AutheliaRouterOptions } -func NewAutheliaAuthenticator(options *Options) *AutheliaHandler { - return &AutheliaHandler{ +// NewAutheliaRouter creates a new AutheliaRouter object +func NewAutheliaRouter(options *AutheliaRouterOptions) *AutheliaRouter { + options.Database.NewTable("authelia") + + //Read settings from database, if exists + options.Database.Read("authelia", "autheliaURL", &options.AutheliaURL) + options.Database.Read("authelia", "useHTTPS", &options.UseHTTPS) + + return &AutheliaRouter{ options: options, } } -// HandleAutheliaAuthRouting is the handler for Authelia authentication, if the error is not nil, the request will be forwarded to the endpoint -// Do not continue processing or write to the response writer if the error is not nil -func (h *AutheliaHandler) HandleAutheliaAuthRouting(w http.ResponseWriter, r *http.Request, pe *dynamicproxy.ProxyEndpoint) error { - err := h.handleAutheliaAuth(w, r) - if err != nil { - return nil +// HandleSetAutheliaURLAndHTTPS is the internal handler for setting the Authelia URL and HTTPS +func (ar *AutheliaRouter) HandleSetAutheliaURLAndHTTPS(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + //Return the current settings + js, _ := json.Marshal(map[string]interface{}{ + "useHTTPS": ar.options.UseHTTPS, + "autheliaURL": ar.options.AutheliaURL, + }) + + utils.SendJSONResponse(w, string(js)) + return + } else if r.Method == http.MethodPost { + //Update the settings + autheliaURL, err := utils.PostPara(r, "autheliaURL") + if err != nil { + utils.SendErrorResponse(w, "autheliaURL not found") + return + } + + useHTTPS, err := utils.PostBool(r, "useHTTPS") + if err != nil { + useHTTPS = false + } + + //Write changes to runtime + ar.options.AutheliaURL = autheliaURL + ar.options.UseHTTPS = useHTTPS + + //Write changes to database + ar.options.Database.Write("authelia", "autheliaURL", autheliaURL) + ar.options.Database.Write("authelia", "useHTTPS", useHTTPS) + + utils.SendOK(w) + } else { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return } - return err + } -func (h *AutheliaHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { +// handleAutheliaAuth is the internal handler for Authelia authentication +// Set useHTTPS to true if your authelia server is using HTTPS +// Set autheliaURL to the URL of the Authelia server, e.g. authelia.example.com +func (ar *AutheliaRouter) HandleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { client := &http.Client{} + if ar.options.AutheliaURL == "" { + ar.options.Logger.PrintAndLog("Authelia", "Authelia URL not set", nil) + w.WriteHeader(500) + w.Write([]byte("500 - Internal Server Error")) + return errors.New("authelia URL not set") + } protocol := "http" - if h.options.UseHTTPS { + if ar.options.UseHTTPS { protocol = "https" } - autheliaBaseURL := protocol + "://" + h.options.AutheliaURL + autheliaBaseURL := protocol + "://" + ar.options.AutheliaURL //Remove tailing slash if any if autheliaBaseURL[len(autheliaBaseURL)-1] == '/' { autheliaBaseURL = autheliaBaseURL[:len(autheliaBaseURL)-1] @@ -53,7 +102,7 @@ func (h *AutheliaHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Requ //Make a request to Authelia to verify the request req, err := http.NewRequest("POST", autheliaBaseURL+"/api/verify", nil) if err != nil { - h.options.Logger.PrintAndLog("Authelia", "Unable to create request", err) + ar.options.Logger.PrintAndLog("Authelia", "Unable to create request", err) w.WriteHeader(401) return errors.New("unauthorized") } @@ -72,7 +121,7 @@ func (h *AutheliaHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Requ // Making the verification request resp, err := client.Do(req) if err != nil { - h.options.Logger.PrintAndLog("Authelia", "Unable to verify", err) + ar.options.Logger.PrintAndLog("Authelia", "Unable to verify", err) w.WriteHeader(401) return errors.New("unauthorized") } diff --git a/src/mod/auth/sso/handlers.go b/src/mod/auth/sso/handlers.go deleted file mode 100644 index 85104f3..0000000 --- a/src/mod/auth/sso/handlers.go +++ /dev/null @@ -1,271 +0,0 @@ -package sso - -/* - handlers.go - - This file contains the handlers for the SSO module. - If you are looking for handlers for SSO user management, - please refer to userHandlers.go. -*/ - -import ( - "encoding/json" - "net/http" - "strings" - - "github.com/gofrs/uuid" - "imuslab.com/zoraxy/mod/utils" -) - -// HandleSSOStatus handle the request to get the status of the SSO portal server -func (s *SSOHandler) HandleSSOStatus(w http.ResponseWriter, r *http.Request) { - type SSOStatus struct { - Enabled bool - SSOInterceptEnabled bool - ListeningPort int - AuthURL string - } - - status := SSOStatus{ - Enabled: s.ssoPortalServer != nil, - //SSOInterceptEnabled: s.ssoInterceptEnabled, - ListeningPort: s.Config.PortalServerPort, - AuthURL: s.Config.AuthURL, - } - - js, _ := json.Marshal(status) - utils.SendJSONResponse(w, string(js)) -} - -// Wrapper for starting and stopping the SSO portal server -// require POST request with key "enable" and value "true" or "false" -func (s *SSOHandler) HandleSSOEnable(w http.ResponseWriter, r *http.Request) { - enable, err := utils.PostBool(r, "enable") - if err != nil { - utils.SendErrorResponse(w, "invalid enable value") - return - } - - if enable { - s.HandleStartSSOPortal(w, r) - } else { - s.HandleStopSSOPortal(w, r) - } -} - -// HandleStartSSOPortal handle the request to start the SSO portal server -func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) { - if s.ssoPortalServer != nil { - //Already enabled. Do restart instead. - err := s.RestartSSOServer() - if err != nil { - utils.SendErrorResponse(w, "failed to start SSO server") - return - } - utils.SendOK(w) - return - } - - //Check if the authURL is set correctly. If not, return error - if s.Config.AuthURL == "" { - utils.SendErrorResponse(w, "auth URL not set") - return - } - - //Start the SSO portal server in go routine - go s.StartSSOPortal() - - //Write current state to database - err := s.Config.Database.Write("sso_conf", "enabled", true) - if err != nil { - utils.SendErrorResponse(w, "failed to update SSO state") - return - } - utils.SendOK(w) -} - -// HandleStopSSOPortal handle the request to stop the SSO portal server -func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) { - if s.ssoPortalServer == nil { - //Already disabled - utils.SendOK(w) - return - } - - err := s.ssoPortalServer.Close() - if err != nil { - s.Log("Failed to stop SSO portal server", err) - utils.SendErrorResponse(w, "failed to stop SSO portal server") - return - } - s.ssoPortalServer = nil - - //Write current state to database - err = s.Config.Database.Write("sso_conf", "enabled", false) - if err != nil { - utils.SendErrorResponse(w, "failed to update SSO state") - return - } - utils.SendOK(w) -} - -// HandlePortChange handle the request to change the SSO portal server port -func (s *SSOHandler) HandlePortChange(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - //Return the current port - js, _ := json.Marshal(s.Config.PortalServerPort) - utils.SendJSONResponse(w, string(js)) - return - } - - port, err := utils.PostInt(r, "port") - if err != nil { - utils.SendErrorResponse(w, "invalid port given") - return - } - - s.Config.PortalServerPort = port - - //Write to the database - err = s.Config.Database.Write("sso_conf", "port", port) - if err != nil { - utils.SendErrorResponse(w, "failed to update port") - return - } - - if s.IsRunning() { - //Restart the server if it is running - err = s.RestartSSOServer() - if err != nil { - utils.SendErrorResponse(w, "failed to restart SSO server") - return - } - } - utils.SendOK(w) -} - -// HandleSetAuthURL handle the request to change the SSO auth URL -// This is the URL that the SSO portal server will redirect to for authentication -// e.g. auth.yourdomain.com -func (s *SSOHandler) HandleSetAuthURL(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - //Return the current auth URL - js, _ := json.Marshal(s.Config.AuthURL) - utils.SendJSONResponse(w, string(js)) - return - } - - //Get the auth URL - authURL, err := utils.PostPara(r, "auth_url") - if err != nil { - utils.SendErrorResponse(w, "invalid auth URL given") - return - } - - s.Config.AuthURL = authURL - - //Write to the database - err = s.Config.Database.Write("sso_conf", "authurl", authURL) - if err != nil { - utils.SendErrorResponse(w, "failed to update auth URL") - return - } - - //Clear the cookie store and restart the server - err = s.RestartSSOServer() - if err != nil { - utils.SendErrorResponse(w, "failed to restart SSO server") - return - } - utils.SendOK(w) -} - -// HandleRegisterApp handle the request to register a new app to the SSO portal -func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) { - appName, err := utils.PostPara(r, "app_name") - if err != nil { - utils.SendErrorResponse(w, "invalid app name given") - return - } - - id, err := utils.PostPara(r, "app_id") - if err != nil { - //If id is not given, use the app name with a random UUID - newID, err := uuid.NewV4() - if err != nil { - utils.SendErrorResponse(w, "failed to generate new app ID") - return - } - id = strings.ReplaceAll(appName, " ", "") + "-" + newID.String() - } - - //Check if the given appid is already in use - if _, ok := s.Apps[id]; ok { - utils.SendErrorResponse(w, "app ID already in use") - return - } - - /* - Process the app domain - An app can have multiple domains, separated by commas - Usually the app domain is the proxy rule that points to the app - For example, if the app is hosted at app.yourdomain.com, the app domain is app.yourdomain.com - */ - appDomain, err := utils.PostPara(r, "app_domain") - if err != nil { - utils.SendErrorResponse(w, "invalid app URL given") - return - } - - appURLs := strings.Split(appDomain, ",") - //Remove padding and trailing spaces in each URL - for i := range appURLs { - appURLs[i] = strings.TrimSpace(appURLs[i]) - } - - //Create a new app entry - thisAppEntry := RegisteredUpstreamApp{ - ID: id, - Secret: "", - Domain: appURLs, - Scopes: []string{}, - SessionDuration: 3600, - } - - js, _ := json.Marshal(thisAppEntry) - - //Create a new app in the database - err = s.Config.Database.Write("sso_apps", appName, string(js)) - if err != nil { - utils.SendErrorResponse(w, "failed to create new app") - return - } - - //Also add the app to runtime config - s.Apps[appName] = thisAppEntry - - utils.SendOK(w) -} - -// HandleAppRemove handle the request to remove an app from the SSO portal -func (s *SSOHandler) HandleAppRemove(w http.ResponseWriter, r *http.Request) { - appID, err := utils.PostPara(r, "app_id") - if err != nil { - utils.SendErrorResponse(w, "invalid app ID given") - return - } - - //Check if the app actually exists - if _, ok := s.Apps[appID]; !ok { - utils.SendErrorResponse(w, "app not found") - return - } - delete(s.Apps, appID) - - //Also remove it from the database - err = s.Config.Database.Delete("sso_apps", appID) - if err != nil { - s.Log("Failed to remove app from database", err) - } - -} diff --git a/src/mod/auth/sso/oauth2.go b/src/mod/auth/sso/oauth2.go deleted file mode 100644 index d519fb6..0000000 --- a/src/mod/auth/sso/oauth2.go +++ /dev/null @@ -1,295 +0,0 @@ -package sso - -import ( - "context" - _ "embed" - "encoding/json" - "log" - "net/http" - "net/url" - "time" - - "github.com/go-oauth2/oauth2/v4/errors" - "github.com/go-oauth2/oauth2/v4/generates" - "github.com/go-oauth2/oauth2/v4/manage" - "github.com/go-oauth2/oauth2/v4/models" - "github.com/go-oauth2/oauth2/v4/server" - "github.com/go-oauth2/oauth2/v4/store" - "github.com/go-session/session" - "imuslab.com/zoraxy/mod/utils" -) - -const ( - SSO_SESSION_NAME = "ZoraxySSO" -) - -type OAuth2Server struct { - srv *server.Server //oAuth server instance - config *SSOConfig - parent *SSOHandler -} - -//go:embed static/auth.html -var authHtml []byte - -//go:embed static/login.html -var loginHtml []byte - -// NewOAuth2Server creates a new OAuth2 server instance -func NewOAuth2Server(config *SSOConfig, parent *SSOHandler) (*OAuth2Server, error) { - manager := manage.NewDefaultManager() - manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) - // token store - manager.MustTokenStorage(store.NewFileTokenStore("./conf/sso.db")) - // generate jwt access token - manager.MapAccessGenerate(generates.NewAccessGenerate()) - - //Load the information of registered app within the OAuth2 server - clientStore := store.NewClientStore() - clientStore.Set("myapp", &models.Client{ - ID: "myapp", - Secret: "verysecurepassword", - Domain: "localhost:9094", - }) - //TODO: LOAD THIS DYNAMICALLY FROM DATABASE - manager.MapClientStorage(clientStore) - - thisServer := OAuth2Server{ - config: config, - parent: parent, - } - - //Create a new oauth server - srv := server.NewServer(server.NewConfig(), manager) - srv.SetPasswordAuthorizationHandler(thisServer.PasswordAuthorizationHandler) - srv.SetUserAuthorizationHandler(thisServer.UserAuthorizeHandler) - srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { - log.Println("Internal Error:", err.Error()) - return - }) - srv.SetResponseErrorHandler(func(re *errors.Response) { - log.Println("Response Error:", re.Error.Error()) - }) - - //Set the access scope handler - srv.SetAuthorizeScopeHandler(thisServer.AuthorizationScopeHandler) - //Set the access token expiration handler based on requesting domain / hostname - srv.SetAccessTokenExpHandler(thisServer.ExpireHandler) - thisServer.srv = srv - return &thisServer, nil -} - -// Password handler, validate if the given username and password are correct -func (oas *OAuth2Server) PasswordAuthorizationHandler(ctx context.Context, clientID, username, password string) (userID string, err error) { - //TODO: LOAD THIS DYNAMICALLY FROM DATABASE - if username == "test" && password == "test" { - userID = "test" - } - return -} - -// User Authorization Handler, handle auth request from user -func (oas *OAuth2Server) UserAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) { - store, err := session.Start(r.Context(), w, r) - if err != nil { - return - } - - uid, ok := store.Get(SSO_SESSION_NAME) - if !ok { - if r.Form == nil { - r.ParseForm() - } - - store.Set("ReturnUri", r.Form) - store.Save() - - w.Header().Set("Location", "/oauth2/login") - w.WriteHeader(http.StatusFound) - return - } - - userID = uid.(string) - store.Delete(SSO_SESSION_NAME) - store.Save() - return -} - -// AccessTokenExpHandler, set the SSO session length default value -func (oas *OAuth2Server) ExpireHandler(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) { - requestHostname := r.Host - if requestHostname == "" { - //Use default value - return time.Hour, nil - } - - //Get the Registered App Config from parent - appConfig, ok := oas.parent.Apps[requestHostname] - if !ok { - //Use default value - return time.Hour, nil - } - - //Use the app's session length - return time.Second * time.Duration(appConfig.SessionDuration), nil -} - -// AuthorizationScopeHandler, handle the scope of the request -func (oas *OAuth2Server) AuthorizationScopeHandler(w http.ResponseWriter, r *http.Request) (scope string, err error) { - //Get the scope from post or GEt request - if r.Form == nil { - if err := r.ParseForm(); err != nil { - return "none", err - } - } - - //Get the hostname of the request - requestHostname := r.Host - if requestHostname == "" { - //No rule set. Use default - return "none", nil - } - - //Get the Registered App Config from parent - appConfig, ok := oas.parent.Apps[requestHostname] - if !ok { - //No rule set. Use default - return "none", nil - } - - //Check if the scope is set in the request - if v, ok := r.Form["scope"]; ok { - //Check if the requested scope is in the appConfig scope - if utils.StringInArray(appConfig.Scopes, v[0]) { - return v[0], nil - } - return "none", nil - } - - return "none", nil -} - -/* SSO Web Server Toggle Functions */ -func (oas *OAuth2Server) RegisterOauthEndpoints(primaryMux *http.ServeMux) { - primaryMux.HandleFunc("/oauth2/login", oas.loginHandler) - primaryMux.HandleFunc("/oauth2/auth", oas.authHandler) - - primaryMux.HandleFunc("/oauth2/authorize", func(w http.ResponseWriter, r *http.Request) { - store, err := session.Start(r.Context(), w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - var form url.Values - if v, ok := store.Get("ReturnUri"); ok { - form = v.(url.Values) - } - r.Form = form - - store.Delete("ReturnUri") - store.Save() - - err = oas.srv.HandleAuthorizeRequest(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - } - }) - - primaryMux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) { - err := oas.srv.HandleTokenRequest(w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - }) - - primaryMux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { - token, err := oas.srv.ValidationBearerToken(r) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - data := map[string]interface{}{ - "expires_in": int64(time.Until(token.GetAccessCreateAt().Add(token.GetAccessExpiresIn())).Seconds()), - "client_id": token.GetClientID(), - "user_id": token.GetUserID(), - } - e := json.NewEncoder(w) - e.SetIndent("", " ") - e.Encode(data) - }) -} - -func (oas *OAuth2Server) loginHandler(w http.ResponseWriter, r *http.Request) { - store, err := session.Start(r.Context(), w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if r.Method == "POST" { - if r.Form == nil { - if err := r.ParseForm(); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } - - //Load username and password from form post - username, err := utils.PostPara(r, "username") - if err != nil { - w.Write([]byte("invalid username or password")) - return - } - - password, err := utils.PostPara(r, "password") - if err != nil { - w.Write([]byte("invalid username or password")) - return - } - - //Validate the user - if !oas.parent.ValidateUsernameAndPassword(username, password) { - //Wrong password - w.Write([]byte("invalid username or password")) - return - } - - store.Set(SSO_SESSION_NAME, r.Form.Get("username")) - store.Save() - - w.Header().Set("Location", "/oauth2/auth") - w.WriteHeader(http.StatusFound) - return - } else if r.Method == "GET" { - //Check if the user is logged in - if _, ok := store.Get(SSO_SESSION_NAME); ok { - w.Header().Set("Location", "/oauth2/auth") - w.WriteHeader(http.StatusFound) - return - } - } - //User not logged in. Show login page - w.Write(loginHtml) -} - -func (oas *OAuth2Server) authHandler(w http.ResponseWriter, r *http.Request) { - store, err := session.Start(context.TODO(), w, r) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - if _, ok := store.Get(SSO_SESSION_NAME); !ok { - w.Header().Set("Location", "/oauth2/login") - w.WriteHeader(http.StatusFound) - return - } - //User logged in. Check if this user have previously authorized the app - - //TODO: Check if the user have previously authorized the app - - //User have not authorized the app. Show the authorization page - w.Write(authHtml) -} diff --git a/src/mod/auth/sso/oauth_test.go b/src/mod/auth/sso/oauth_test.go deleted file mode 100644 index 2e16460..0000000 --- a/src/mod/auth/sso/oauth_test.go +++ /dev/null @@ -1 +0,0 @@ -package sso diff --git a/src/mod/auth/sso/openid.go b/src/mod/auth/sso/openid.go deleted file mode 100644 index 533b39f..0000000 --- a/src/mod/auth/sso/openid.go +++ /dev/null @@ -1,58 +0,0 @@ -package sso - -import ( - "encoding/json" - "net/http" - "strings" -) - -type OpenIDConfiguration struct { - Issuer string `json:"issuer"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - JwksUri string `json:"jwks_uri"` - ResponseTypesSupported []string `json:"response_types_supported"` - SubjectTypesSupported []string `json:"subject_types_supported"` - IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` - ClaimsSupported []string `json:"claims_supported"` -} - -func (h *SSOHandler) HandleDiscoveryRequest(w http.ResponseWriter, r *http.Request) { - - //Prepend https:// if not present - authBaseURL := h.Config.AuthURL - if !strings.HasPrefix(authBaseURL, "http://") && !strings.HasPrefix(authBaseURL, "https://") { - authBaseURL = "https://" + authBaseURL - } - - //Handle the discovery request - discovery := OpenIDConfiguration{ - Issuer: authBaseURL, - AuthorizationEndpoint: authBaseURL + "/oauth2/authorize", - TokenEndpoint: authBaseURL + "/oauth2/token", - JwksUri: authBaseURL + "/jwks.json", - ResponseTypesSupported: []string{"code", "token"}, - SubjectTypesSupported: []string{"public"}, - IDTokenSigningAlgValuesSupported: []string{ - "RS256", - }, - ClaimsSupported: []string{ - "sub", //Subject, usually the user ID - "iss", //Issuer, usually the server URL - "aud", //Audience, usually the client ID - "exp", //Expiration Time - "iat", //Issued At - "email", //Email - "locale", //Locale - "name", //Full Name - "nickname", //Nickname - "preferred_username", //Preferred Username - "website", //Website - }, - } - - //Write the response - js, _ := json.Marshal(discovery) - w.Header().Set("Content-Type", "application/json") - w.Write(js) -} diff --git a/src/mod/auth/sso/server.go b/src/mod/auth/sso/server.go deleted file mode 100644 index 44cc2e3..0000000 --- a/src/mod/auth/sso/server.go +++ /dev/null @@ -1,132 +0,0 @@ -package sso - -import ( - "context" - "net/http" - "strconv" - "time" - - "github.com/go-oauth2/oauth2/v4/errors" - "imuslab.com/zoraxy/mod/utils" -) - -/* - server.go - - This is the web server for the SSO portal. It contains the - HTTP server and the handlers for the SSO portal. - - If you are looking for handlers that changes the settings - of the SSO portale or user management, please refer to - handlers.go. - -*/ - -func (h *SSOHandler) InitSSOPortal(portalServerPort int) { - //Create a new web server for the SSO portal - pmux := http.NewServeMux() - fs := http.FileServer(http.FS(staticFiles)) - pmux.Handle("/", fs) - - //Register API endpoint for the SSO portal - pmux.HandleFunc("/sso/login", h.HandleLogin) - - //Register API endpoint for autodiscovery - pmux.HandleFunc("/.well-known/openid-configuration", h.HandleDiscoveryRequest) - - //Register OAuth2 endpoints - h.Oauth2Server.RegisterOauthEndpoints(pmux) - h.ssoPortalMux = pmux -} - -// StartSSOPortal start the SSO portal server -// This function will block the main thread, call it in a goroutine -func (h *SSOHandler) StartSSOPortal() error { - if h.ssoPortalServer != nil { - return errors.New("SSO portal server already running") - } - h.ssoPortalServer = &http.Server{ - Addr: ":" + strconv.Itoa(h.Config.PortalServerPort), - Handler: h.ssoPortalMux, - } - err := h.ssoPortalServer.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - h.Log("Failed to start SSO portal server", err) - } - return err -} - -// StopSSOPortal stop the SSO portal server -func (h *SSOHandler) StopSSOPortal() error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - err := h.ssoPortalServer.Shutdown(ctx) - if err != nil { - h.Log("Failed to stop SSO portal server", err) - return err - } - h.ssoPortalServer = nil - return nil -} - -// StartSSOPortal start the SSO portal server -func (h *SSOHandler) RestartSSOServer() error { - if h.ssoPortalServer != nil { - err := h.StopSSOPortal() - if err != nil { - return err - } - } - go h.StartSSOPortal() - return nil -} - -func (h *SSOHandler) IsRunning() bool { - return h.ssoPortalServer != nil -} - -// HandleLogin handle the login request -func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) { - //Handle the login request - username, err := utils.PostPara(r, "username") - if err != nil { - utils.SendErrorResponse(w, "invalid username or password") - return - } - - password, err := utils.PostPara(r, "password") - if err != nil { - utils.SendErrorResponse(w, "invalid username or password") - return - } - - rememberMe, err := utils.PostBool(r, "remember_me") - if err != nil { - rememberMe = false - } - - //Check if the user exists - userEntry, err := h.GetSSOUser(username) - if err != nil { - utils.SendErrorResponse(w, "user not found") - return - } - - //Check if the password is correct - if !userEntry.VerifyPassword(password) { - utils.SendErrorResponse(w, "incorrect password") - return - } - - //Create a new session for the user - session, _ := h.cookieStore.Get(r, "Zoraxy-SSO") - session.Values["username"] = username - if rememberMe { - session.Options.MaxAge = 86400 * 15 //15 days - } else { - session.Options.MaxAge = 3600 //1 hour - } - session.Save(r, w) //Save the session - - utils.SendOK(w) -} diff --git a/src/mod/auth/sso/sso.go b/src/mod/auth/sso/sso.go deleted file mode 100644 index 4be5f35..0000000 --- a/src/mod/auth/sso/sso.go +++ /dev/null @@ -1,158 +0,0 @@ -package sso - -import ( - "embed" - "net/http" - - "github.com/gorilla/sessions" - "imuslab.com/zoraxy/mod/database" - "imuslab.com/zoraxy/mod/info/logger" -) - -/* - sso.go - - This file contains the main SSO handler and the SSO configuration - structure. It also contains the main SSO handler functions. - - SSO web interface are stored in the static folder, which is embedded - into the binary. -*/ - -//go:embed static/* -var staticFiles embed.FS //Static files for the SSO portal - -type SSOConfig struct { - SystemUUID string //System UUID, should be passed in from main scope - AuthURL string //Authentication subdomain URL, e.g. auth.example.com - PortalServerPort int //SSO portal server port - Database *database.Database //System master key-value database - Logger *logger.Logger -} - -// SSOHandler is the main SSO handler structure -type SSOHandler struct { - cookieStore *sessions.CookieStore - ssoPortalServer *http.Server - ssoPortalMux *http.ServeMux - Oauth2Server *OAuth2Server - Config *SSOConfig - Apps map[string]RegisteredUpstreamApp -} - -// Create a new Zoraxy SSO handler -func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) { - //Create a cookie store for the SSO handler - cookieStore := sessions.NewCookieStore([]byte(config.SystemUUID)) - cookieStore.Options = &sessions.Options{ - Path: "", - Domain: "", - MaxAge: 0, - Secure: false, - HttpOnly: false, - SameSite: 0, - } - - config.Database.NewTable("sso_users") //For storing user information - config.Database.NewTable("sso_conf") //For storing SSO configuration - config.Database.NewTable("sso_apps") //For storing registered apps - - //Create the SSO Handler - thisHandler := SSOHandler{ - cookieStore: cookieStore, - Config: config, - } - - //Read the app info from database - thisHandler.Apps = make(map[string]RegisteredUpstreamApp) - - //Create an oauth2 server - oauth2Server, err := NewOAuth2Server(config, &thisHandler) - if err != nil { - return nil, err - } - - //Register endpoints - thisHandler.Oauth2Server = oauth2Server - thisHandler.InitSSOPortal(config.PortalServerPort) - - return &thisHandler, nil -} - -func (h *SSOHandler) RestorePreviousRunningState() { - //Load the previous SSO state - ssoEnabled := false - ssoPort := 5488 - ssoAuthURL := "" - h.Config.Database.Read("sso_conf", "enabled", &ssoEnabled) - h.Config.Database.Read("sso_conf", "port", &ssoPort) - h.Config.Database.Read("sso_conf", "authurl", &ssoAuthURL) - - if ssoAuthURL == "" { - //Cannot enable SSO without auth URL - ssoEnabled = false - } - - h.Config.PortalServerPort = ssoPort - h.Config.AuthURL = ssoAuthURL - - if ssoEnabled { - go h.StartSSOPortal() - } -} - -// ServeForwardAuth handle the SSO request in interception mode -// Suppose to be called in dynamicproxy. -// Return true if the request is allowed to pass, false if the request is blocked and shall not be further processed -func (h *SSOHandler) ServeForwardAuth(w http.ResponseWriter, r *http.Request) bool { - //Get the current uri for appending to the auth subdomain - originalRequestURL := r.RequestURI - - redirectAuthURL := h.Config.AuthURL - if redirectAuthURL == "" || !h.IsRunning() { - //Redirect not set or auth server is offlined - w.Write([]byte("SSO auth URL not set or SSO server offline.")) - //TODO: Use better looking template if exists - return false - } - - //Check if the user have the cookie "Zoraxy-SSO" set - session, err := h.cookieStore.Get(r, "Zoraxy-SSO") - if err != nil { - //Redirect to auth subdomain - http.Redirect(w, r, redirectAuthURL+"/sso/login?m=new&t="+originalRequestURL, http.StatusFound) - return false - } - - //Check if the user is logged in - if session.Values["username"] != true { - //Redirect to auth subdomain - http.Redirect(w, r, redirectAuthURL+"/sso/login?m=expired&t="+originalRequestURL, http.StatusFound) - return false - } - - //Check if the current request subdomain is allowed - userName := session.Values["username"].(string) - user, err := h.GetSSOUser(userName) - if err != nil { - //User might have been removed from SSO. Redirect to auth subdomain - http.Redirect(w, r, redirectAuthURL, http.StatusFound) - return false - } - - //Check if the user have access to the current subdomain - if !user.Subdomains[r.Host].AllowAccess { - //User is not allowed to access the current subdomain. Sent 403 - http.Error(w, "Forbidden", http.StatusForbidden) - //TODO: Use better looking template if exists - return false - } - - //User is logged in, continue to the next handler - return true -} - -// Log a message with the SSO module tag -func (h *SSOHandler) Log(message string, err error) { - h.Config.Logger.PrintAndLog("SSO", message, err) -} diff --git a/src/mod/auth/sso/static/auth.html b/src/mod/auth/sso/static/auth.html deleted file mode 100644 index 9c74b65..0000000 --- a/src/mod/auth/sso/static/auth.html +++ /dev/null @@ -1,33 +0,0 @@ - - -
- -Single Sign-On (SSO) and authentication providers settings