From 2423d0fb3a94d7fe0748578df0444f1685890379 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 15 Dec 2024 15:52:59 +0800 Subject: [PATCH] Added experimental authelia support - Integrated #33 code snippet - Added UI for setting Authelia server address - Updated authentication provider implementation --- src/api.go | 20 +- src/def.go | 8 +- src/mod/auth/sso/app.go | 34 -- src/mod/auth/sso/authelia/authelia.go | 91 +++- src/mod/auth/sso/handlers.go | 271 ------------ src/mod/auth/sso/oauth2.go | 295 ------------- src/mod/auth/sso/oauth_test.go | 1 - src/mod/auth/sso/openid.go | 58 --- src/mod/auth/sso/server.go | 132 ------ src/mod/auth/sso/sso.go | 158 ------- src/mod/auth/sso/static/auth.html | 33 -- src/mod/auth/sso/static/index.html | 43 -- src/mod/auth/sso/static/login.html | 29 -- src/mod/auth/sso/userHandlers.go | 309 ------------- src/mod/auth/sso/users.go | 141 ------ src/mod/dynamicproxy/Server.go | 21 +- .../{basicAuth.go => authProviders.go} | 48 +- src/mod/dynamicproxy/typedef.go | 37 +- src/reverseproxy.go | 22 +- src/start.go | 23 +- src/web/components/httprp.html | 54 ++- src/web/components/sso.html | 410 +++--------------- src/web/snippet/sso_app.html | 29 -- src/web/snippet/sso_user.html | 29 -- 24 files changed, 267 insertions(+), 2029 deletions(-) delete mode 100644 src/mod/auth/sso/app.go delete mode 100644 src/mod/auth/sso/handlers.go delete mode 100644 src/mod/auth/sso/oauth2.go delete mode 100644 src/mod/auth/sso/oauth_test.go delete mode 100644 src/mod/auth/sso/openid.go delete mode 100644 src/mod/auth/sso/server.go delete mode 100644 src/mod/auth/sso/sso.go delete mode 100644 src/mod/auth/sso/static/auth.html delete mode 100644 src/mod/auth/sso/static/index.html delete mode 100644 src/mod/auth/sso/static/login.html delete mode 100644 src/mod/auth/sso/userHandlers.go delete mode 100644 src/mod/auth/sso/users.go rename src/mod/dynamicproxy/{basicAuth.go => authProviders.go} (51%) delete mode 100644 src/web/snippet/sso_app.html delete mode 100644 src/web/snippet/sso_user.html 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 @@ - - - - - Auth - - - - - - -
-
-
-

Authorize

-

The client would like to perform actions on your behalf.

-

- -

-
-
-
- - diff --git a/src/mod/auth/sso/static/index.html b/src/mod/auth/sso/static/index.html deleted file mode 100644 index b49328b..0000000 --- a/src/mod/auth/sso/static/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - Login Page - - - -
-
-
-

-
- Log in to your account -
-

-
-
-
-
- - -
-
-
-
- - -
-
-
Login
-
-
-
-
- New to us? Sign Up -
-
-
-
- - - - \ No newline at end of file diff --git a/src/mod/auth/sso/static/login.html b/src/mod/auth/sso/static/login.html deleted file mode 100644 index fd6999d..0000000 --- a/src/mod/auth/sso/static/login.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - Login - - - - - - -
-

Login In

-
-
- - -
-
- - -
- -
-
- - - \ No newline at end of file diff --git a/src/mod/auth/sso/userHandlers.go b/src/mod/auth/sso/userHandlers.go deleted file mode 100644 index be29113..0000000 --- a/src/mod/auth/sso/userHandlers.go +++ /dev/null @@ -1,309 +0,0 @@ -package sso - -/* - userHandlers.go - Handlers for SSO user management - - If you are looking for handlers that changes the settings - of the SSO portal (e.g. authURL or port), please refer to - handlers.go. -*/ - -import ( - "encoding/json" - "errors" - "net/http" - - "github.com/gofrs/uuid" - "imuslab.com/zoraxy/mod/auth" - "imuslab.com/zoraxy/mod/utils" -) - -// HandleAddUser handle the request to add a new user to the SSO system -func (s *SSOHandler) HandleAddUser(w http.ResponseWriter, r *http.Request) { - username, err := utils.PostPara(r, "username") - if err != nil { - utils.SendErrorResponse(w, "invalid username given") - return - } - - password, err := utils.PostPara(r, "password") - if err != nil { - utils.SendErrorResponse(w, "invalid password given") - return - } - - newUserId, err := uuid.NewV4() - if err != nil { - utils.SendErrorResponse(w, "failed to generate new user ID") - return - } - - //Create a new user entry - thisUserEntry := UserEntry{ - UserID: newUserId.String(), - Username: username, - PasswordHash: auth.Hash(password), - TOTPCode: "", - Enable2FA: false, - } - - js, _ := json.Marshal(thisUserEntry) - - //Create a new user in the database - err = s.Config.Database.Write("sso_users", newUserId.String(), string(js)) - if err != nil { - utils.SendErrorResponse(w, "failed to create new user") - return - } - utils.SendOK(w) -} - -// Edit user information, only accept change of username, password and enabled subdomain filed -func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) { - userID, err := utils.PostPara(r, "user_id") - if err != nil { - utils.SendErrorResponse(w, "invalid user ID given") - return - } - - if !(s.SSOUserExists(userID)) { - utils.SendErrorResponse(w, "user not found") - return - } - - //Load the user entry from database - userEntry, err := s.GetSSOUser(userID) - if err != nil { - utils.SendErrorResponse(w, "failed to load user entry") - return - } - - //Update each of the fields if it is provided - username, err := utils.PostPara(r, "username") - if err == nil { - userEntry.Username = username - } - - password, err := utils.PostPara(r, "password") - if err == nil { - userEntry.PasswordHash = auth.Hash(password) - } - - //Update the user entry in the database - js, _ := json.Marshal(userEntry) - err = s.Config.Database.Write("sso_users", userID, string(js)) - if err != nil { - utils.SendErrorResponse(w, "failed to update user entry") - return - } - utils.SendOK(w) -} - -// HandleRemoveUser remove a user from the SSO system -func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) { - userID, err := utils.PostPara(r, "user_id") - if err != nil { - utils.SendErrorResponse(w, "invalid user ID given") - return - } - - if !(s.SSOUserExists(userID)) { - utils.SendErrorResponse(w, "user not found") - return - } - - //Remove the user from the database - err = s.Config.Database.Delete("sso_users", userID) - if err != nil { - utils.SendErrorResponse(w, "failed to remove user") - return - } - utils.SendOK(w) -} - -// HandleListUser list all users in the SSO system -func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) { - ssoUsers, err := s.ListSSOUsers() - if err != nil { - utils.SendErrorResponse(w, "failed to list users") - return - } - js, _ := json.Marshal(ssoUsers) - utils.SendJSONResponse(w, string(js)) -} - -// HandleAddSubdomain add a subdomain to a user -func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request) { - userid, err := utils.PostPara(r, "user_id") - if err != nil { - utils.SendErrorResponse(w, "invalid user ID given") - return - } - - if !(s.SSOUserExists(userid)) { - utils.SendErrorResponse(w, "user not found") - return - } - - UserEntry, err := s.GetSSOUser(userid) - if err != nil { - utils.SendErrorResponse(w, "failed to load user entry") - return - } - - subdomain, err := utils.PostPara(r, "subdomain") - if err != nil { - utils.SendErrorResponse(w, "invalid subdomain given") - return - } - - allowAccess, err := utils.PostBool(r, "allow_access") - if err != nil { - utils.SendErrorResponse(w, "invalid allow access value given") - return - } - - UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{ - Subdomain: subdomain, - AllowAccess: allowAccess, - } - - err = UserEntry.Update() - if err != nil { - utils.SendErrorResponse(w, "failed to update user entry") - return - } - - utils.SendOK(w) -} - -// HandleRemoveSubdomain remove a subdomain from a user -func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Request) { - userid, err := utils.PostPara(r, "user_id") - if err != nil { - utils.SendErrorResponse(w, "invalid user ID given") - return - } - - if !(s.SSOUserExists(userid)) { - utils.SendErrorResponse(w, "user not found") - return - } - - UserEntry, err := s.GetSSOUser(userid) - if err != nil { - utils.SendErrorResponse(w, "failed to load user entry") - return - } - - subdomain, err := utils.PostPara(r, "subdomain") - if err != nil { - utils.SendErrorResponse(w, "invalid subdomain given") - return - } - - delete(UserEntry.Subdomains, subdomain) - - err = UserEntry.Update() - if err != nil { - utils.SendErrorResponse(w, "failed to update user entry") - return - } - - utils.SendOK(w) -} - -// HandleEnable2FA enable 2FA for a user -func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) { - userid, err := utils.PostPara(r, "user_id") - if err != nil { - utils.SendErrorResponse(w, "invalid user ID given") - return - } - - if !(s.SSOUserExists(userid)) { - utils.SendErrorResponse(w, "user not found") - return - } - - UserEntry, err := s.GetSSOUser(userid) - if err != nil { - utils.SendErrorResponse(w, "failed to load user entry") - return - } - - UserEntry.Enable2FA = true - provisionUri, err := UserEntry.ResetTotp(UserEntry.UserID, "Zoraxy-SSO") - if err != nil { - utils.SendErrorResponse(w, "failed to reset TOTP") - return - } - //As the ResetTotp function will update the user entry in the database, no need to call Update here - - js, _ := json.Marshal(provisionUri) - utils.SendJSONResponse(w, string(js)) -} - -// Handle Disable 2FA for a user -func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) { - userid, err := utils.PostPara(r, "user_id") - if err != nil { - utils.SendErrorResponse(w, "invalid user ID given") - return - } - - if !(s.SSOUserExists(userid)) { - utils.SendErrorResponse(w, "user not found") - return - } - - UserEntry, err := s.GetSSOUser(userid) - if err != nil { - utils.SendErrorResponse(w, "failed to load user entry") - return - } - - UserEntry.Enable2FA = false - UserEntry.TOTPCode = "" - - err = UserEntry.Update() - if err != nil { - utils.SendErrorResponse(w, "failed to update user entry") - return - } - - utils.SendOK(w) -} - -// HandleVerify2FA verify the 2FA code for a user -func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bool, error) { - userid, err := utils.PostPara(r, "user_id") - if err != nil { - return false, errors.New("invalid user ID given") - } - - if !(s.SSOUserExists(userid)) { - utils.SendErrorResponse(w, "user not found") - return false, errors.New("user not found") - } - - UserEntry, err := s.GetSSOUser(userid) - if err != nil { - utils.SendErrorResponse(w, "failed to load user entry") - return false, errors.New("failed to load user entry") - } - - totpCode, _ := utils.PostPara(r, "totp_code") - - if !UserEntry.Enable2FA { - //If 2FA is not enabled, return true - return true, nil - } - - if !UserEntry.VerifyTotp(totpCode) { - return false, nil - } - - return true, nil -} diff --git a/src/mod/auth/sso/users.go b/src/mod/auth/sso/users.go deleted file mode 100644 index 7c52d48..0000000 --- a/src/mod/auth/sso/users.go +++ /dev/null @@ -1,141 +0,0 @@ -package sso - -import ( - "encoding/json" - "time" - - "github.com/xlzd/gotp" - "imuslab.com/zoraxy/mod/auth" -) - -/* - users.go - - This file contains the user structure and user management - functions for the SSO module. - - If you are looking for handlers, please refer to handlers.go. -*/ - -type SubdomainAccessRule struct { - Subdomain string - AllowAccess bool -} - -type UserEntry struct { - UserID string `json:sub` //User ID - Username string `json:"name"` //Username - Email string `json:"email"` //Email - PasswordHash string `json:"passwordhash"` //Password hash - TOTPCode string `json:"totpcode"` //TOTP code - Enable2FA bool `json:"enable2fa"` //Enable 2FA - Subdomains map[string]*SubdomainAccessRule `json:"subdomains"` //Subdomain access rules - LastLogin int64 `json:"lastlogin"` //Last login time - LastLoginIP string `json:"lastloginip"` //Last login IP - LastLoginCountry string `json:"lastlogincountry"` //Last login country - parent *SSOHandler //Parent SSO handler -} - -type ClientResponse struct { - Sub string `json:"sub"` //User ID - Name string `json:"name"` //Username - Nickname string `json:"nickname"` //Nickname - PreferredUsername string `json:"preferred_username"` //Preferred Username - Email string `json:"email"` //Email - Locale string `json:"locale"` //Locale - Website string `json:"website"` //Website -} - -func (s *SSOHandler) SSOUserExists(userid string) bool { - //Check if the user exists in the database - var userEntry UserEntry - err := s.Config.Database.Read("sso_users", userid, &userEntry) - return err == nil -} - -func (s *SSOHandler) GetSSOUser(userid string) (UserEntry, error) { - //Load the user entry from database - var userEntry UserEntry - err := s.Config.Database.Read("sso_users", userid, &userEntry) - if err != nil { - return UserEntry{}, err - } - userEntry.parent = s - return userEntry, nil -} - -func (s *SSOHandler) ListSSOUsers() ([]*UserEntry, error) { - entries, err := s.Config.Database.ListTable("sso_users") - if err != nil { - return nil, err - } - ssoUsers := []*UserEntry{} - for _, keypairs := range entries { - group := new(UserEntry) - json.Unmarshal(keypairs[1], &group) - group.parent = s - ssoUsers = append(ssoUsers, group) - } - - return ssoUsers, nil -} - -// Validate the username and password -func (s *SSOHandler) ValidateUsernameAndPassword(username string, password string) bool { - //Validate the username and password - var userEntry UserEntry - err := s.Config.Database.Read("sso_users", username, &userEntry) - if err != nil { - return false - } - - //TODO: Remove after testing - if (username == "test") && (password == "test") { - return true - } - return userEntry.VerifyPassword(password) -} - -func (s *UserEntry) VerifyPassword(password string) bool { - return s.PasswordHash == auth.Hash(password) -} - -// Write changes in the user entry back to the database -func (u *UserEntry) Update() error { - js, _ := json.Marshal(u) - err := u.parent.Config.Database.Write("sso_users", u.UserID, string(js)) - if err != nil { - return err - } - return nil -} - -// Reset and update the TOTP code for the current user -// Return the provision uri of the new TOTP code for Google Authenticator -func (u *UserEntry) ResetTotp(accountName string, issuerName string) (string, error) { - u.TOTPCode = gotp.RandomSecret(16) - totp := gotp.NewDefaultTOTP(u.TOTPCode) - err := u.Update() - if err != nil { - return "", err - } - return totp.ProvisioningUri(accountName, issuerName), nil -} - -// Verify the TOTP code at current time -func (u *UserEntry) VerifyTotp(enteredCode string) bool { - totp := gotp.NewDefaultTOTP(u.TOTPCode) - return totp.Verify(enteredCode, time.Now().Unix()) -} - -func (u *UserEntry) GetClientResponse() ClientResponse { - return ClientResponse{ - Sub: u.UserID, - Name: u.Username, - Nickname: u.Username, - PreferredUsername: u.Username, - Email: u.Email, - Locale: "en", - Website: "", - } -} diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index f63daa5..ce26ffe 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -83,24 +83,11 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - //SSO Interception Mode - /* - if sep.AuthenticationProvider.SSOInterceptMode { - allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r) - if !allowPass { - h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307) - return - } - } - */ - //Validate basic auth - if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic { - err := h.handleBasicAuthRouting(w, r, sep) - if err != nil { - h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) - return - } + respWritten := handleAuthProviderRouting(sep, w, r, h) + if respWritten { + //Request handled by subroute + return } //Check if any virtual directory rules matches diff --git a/src/mod/dynamicproxy/basicAuth.go b/src/mod/dynamicproxy/authProviders.go similarity index 51% rename from src/mod/dynamicproxy/basicAuth.go rename to src/mod/dynamicproxy/authProviders.go index 9f71111..f50cd55 100644 --- a/src/mod/dynamicproxy/basicAuth.go +++ b/src/mod/dynamicproxy/authProviders.go @@ -9,12 +9,47 @@ import ( ) /* - BasicAuth.go + authProviders.go - This file handles the basic auth on proxy endpoints - if RequireBasicAuth is set to true + This script handle authentication providers */ +/* +Central Authentication Provider Router + +This function will route the request to the correct authentication provider +if the return value is true, do not continue to the next handler + +handleAuthProviderRouting takes in 4 parameters: +- sep: the ProxyEndpoint object +- w: the http.ResponseWriter object +- r: the http.Request object +- h: the ProxyHandler object + +and return a boolean indicate if the request is written to http.ResponseWriter +- true: the request is handled, do not write to http.ResponseWriter +- false: the request is not handled (usually means auth ok), continue to the next handler +*/ +func handleAuthProviderRouting(sep *ProxyEndpoint, w http.ResponseWriter, r *http.Request, h *ProxyHandler) bool { + if sep.AuthenticationProvider.AuthMethod == AuthMethodBasic { + err := h.handleBasicAuthRouting(w, r, sep) + if err != nil { + h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + return true + } + } else if sep.AuthenticationProvider.AuthMethod == AuthMethodAuthelia { + err := h.handleAutheliaAuth(w, r) + if err != nil { + h.Parent.Option.Logger.LogHTTPRequest(r, "host", 401) + return true + } + } + + //No authentication provider, do not need to handle + return false +} + +/* Basic Auth */ func (h *ProxyHandler) handleBasicAuthRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { err := handleBasicAuth(w, r, pe) if err != nil { @@ -64,3 +99,10 @@ func handleBasicAuth(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) return nil } + +/* Authelia */ + +// Handle authelia auth routing +func (h *ProxyHandler) handleAutheliaAuth(w http.ResponseWriter, r *http.Request) error { + return h.Parent.Option.AutheliaRouter.HandleAutheliaAuth(w, r) +} diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 4d9ce22..da6cd28 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -7,7 +7,7 @@ import ( "sync" "imuslab.com/zoraxy/mod/access" - "imuslab.com/zoraxy/mod/auth/sso" + "imuslab.com/zoraxy/mod/auth/sso/authelia" "imuslab.com/zoraxy/mod/dynamicproxy/dpcore" "imuslab.com/zoraxy/mod/dynamicproxy/loadbalance" "imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy" @@ -33,14 +33,17 @@ type ProxyHandler struct { /* Router Object Options */ type RouterOption struct { - HostUUID string //The UUID of Zoraxy, use for heading mod - HostVersion string //The version of Zoraxy, use for heading mod - Port int //Incoming port - UseTls bool //Use TLS to serve incoming requsts - ForceTLSLatest bool //Force TLS1.2 or above - NoCache bool //Force set Cache-Control: no-store - ListenOnPort80 bool //Enable port 80 http listener - ForceHttpsRedirect bool //Force redirection of http to https endpoint + /* Basic Settings */ + HostUUID string //The UUID of Zoraxy, use for heading mod + HostVersion string //The version of Zoraxy, use for heading mod + Port int //Incoming port + UseTls bool //Use TLS to serve incoming requsts + ForceTLSLatest bool //Force TLS1.2 or above + NoCache bool //Force set Cache-Control: no-store + ListenOnPort80 bool //Enable port 80 http listener + ForceHttpsRedirect bool //Force redirection of http to https endpoint + + /* Routing Service Managers */ TlsManager *tlscert.Manager //TLS manager for serving SAN certificates RedirectRuleTable *redirection.RuleTable //Redirection rules handler and table GeodbStore *geodb.Store //GeoIP resolver @@ -48,8 +51,12 @@ type RouterOption struct { StatisticCollector *statistic.Collector //Statistic collector for storing stats on incoming visitors WebDirectory string //The static web server directory containing the templates folder LoadBalancer *loadbalance.RouteManager //Load balancer that handle load balancing of proxy target - SSOHandler *sso.SSOHandler //SSO handler for handling SSO requests, interception mode only - Logger *logger.Logger //Logger for reverse proxy requets + + /* Authentication Providers */ + AutheliaRouter *authelia.AutheliaRouter //Authelia router for Authelia authentication + + /* Utilities */ + Logger *logger.Logger //Logger for reverse proxy requets } /* Router Object */ @@ -129,9 +136,15 @@ const ( ) type AuthenticationProvider struct { - AuthMethod AuthMethod //The authentication method to use + AuthMethod AuthMethod //The authentication method to use + /* Basic Auth Settings */ BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target + BasicAuthGroupIDs []string //Group IDs that are allowed to access this endpoint + + /* Authelia Settings */ + AutheliaURL string //URL of the Authelia server, leave empty to use global settings e.g. authelia.example.com + UseHTTPS bool //Whether to use HTTPS for the Authelia server } // A proxy endpoint record, a general interface for handling inbound routing diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 75211f5..3f5b3e0 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -98,8 +98,8 @@ func ReverseProxtInit() { StatisticCollector: statisticCollector, WebDirectory: *staticWebServerRoot, AccessController: accessController, + AutheliaRouter: autheliaRouter, LoadBalancer: loadBalancer, - SSOHandler: ssoHandler, Logger: SystemWideLogger, }) if err != nil { @@ -471,13 +471,17 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { } bypassGlobalTLS := (bpgtls == "true") - // Basic Auth - rba, _ := utils.PostPara(r, "bauth") - if rba == "" { - rba = "false" + // Auth Provider + authProviderTypeStr, _ := utils.PostPara(r, "authprovider") + if authProviderTypeStr == "" { + authProviderTypeStr = "0" } - requireBasicAuth := (rba == "true") + authProviderType, err := strconv.Atoi(authProviderTypeStr) + if err != nil { + utils.SendErrorResponse(w, "Invalid auth provider type") + return + } // Rate Limiting? rl, _ := utils.PostPara(r, "rate") @@ -519,8 +523,12 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, } } - if requireBasicAuth { + if authProviderType == 1 { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodBasic + } else if authProviderType == 2 { + newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodAuthelia + } else if authProviderType == 3 { + newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodOauth2 } else { newProxyEndpoint.AuthenticationProvider.AuthMethod = dynamicproxy.AuthMethodNone } diff --git a/src/start.go b/src/start.go index ec5b880..45866dc 100644 --- a/src/start.go +++ b/src/start.go @@ -12,6 +12,7 @@ import ( "imuslab.com/zoraxy/mod/access" "imuslab.com/zoraxy/mod/acme" "imuslab.com/zoraxy/mod/auth" + "imuslab.com/zoraxy/mod/auth/sso/authelia" "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/database/dbinc" "imuslab.com/zoraxy/mod/dockerux" @@ -136,21 +137,13 @@ func startupSequence() { panic(err) } - /* - //Create an SSO handler - ssoHandler, err = sso.NewSSOHandler(&sso.SSOConfig{ - SystemUUID: nodeUUID, - PortalServerPort: 5488, - AuthURL: "http://auth.localhost", - Database: sysdb, - Logger: SystemWideLogger, - }) - if err != nil { - log.Fatal(err) - } - //Restore the SSO handler to previous state before shutdown - ssoHandler.RestorePreviousRunningState() - */ + //Create authentication providers + autheliaRouter = authelia.NewAutheliaRouter(&authelia.AutheliaRouterOptions{ + UseHTTPS: false, // Automatic populate in router initiation + AutheliaURL: "", // Automatic populate in router initiation + Logger: SystemWideLogger, + Database: sysdb, + }) //Create a statistic collector statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{ diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index 61a1bfc..1c61b37 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -125,10 +125,12 @@ ${vdList} - ${subd.AuthenticationProvider.AuthMethod == 0x1?` Basic Auth`:``} - ${subd.AuthenticationProvider.AuthMethod == 0x1 && subd.RequireRateLimit?"
":""} - ${subd.AuthenticationProvider.RequireRateLimit?` Rate Limit @ ${subd.RateLimit} req/s`:``} - ${!subd.AuthenticationProvider.AuthMethod == 0x1 && !subd.RequireRateLimit?`No Special Settings`:""} + ${subd.AuthenticationProvider.AuthMethod == 0x1?` Basic Auth`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x2?` Authelia`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x3?` Oauth2`:``} + ${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"
":""} + ${subd.RequireRateLimit?` Rate Limit @ ${subd.RateLimit} req/s`:``} + ${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`No Special Settings`:""}
@@ -194,6 +196,11 @@ } let rule = accessRuleMap[thisAccessRuleID]; + if (rule == undefined){ + //Missing config or config too old + $(this).html(` Access Rule Error`); + return; + } let icon = ``; if (rule.ID == "default"){ icon = ``; @@ -269,12 +276,8 @@ `); }else if (datatype == "advanced"){ - let requireBasicAuth = payload.AuthenticationProvider.AuthMethod == 0x1; - let basicAuthCheckstate = ""; - if (requireBasicAuth){ - basicAuthCheckstate = "checked"; - } - + let authProvider = payload.AuthenticationProvider.AuthMethod; + let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck; let wsCheckstate = ""; if (skipWebSocketOriginCheck){ @@ -296,13 +299,29 @@ rateLimitDisableState = "disabled"; } - column.empty().append(`
- - + column.empty().append(` +
+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
-
-
@@ -328,6 +347,7 @@
`); + $('.authProviderPicker .ui.checkbox').checkbox(); } else if (datatype == "ratelimit"){ column.empty().append(` @@ -421,7 +441,7 @@ var epttype = "host"; let useStickySession = $(row).find(".UseStickySession")[0].checked; - let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked; + let authProviderType = $(row).find(".authProviderPicker input[type='radio']:checked").val(); let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked; let rateLimit = $(row).find(".RateLimit").val(); let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked; @@ -434,7 +454,7 @@ "rootname": uuid, "ss":useStickySession, "bpgtls": bypassGlobalTLS, - "bauth" :requireBasicAuth, + "authprovider" :authProviderType, "rate" :requireRateLimit, "ratenum" :rateLimit, }, diff --git a/src/web/components/sso.html b/src/web/components/sso.html index e3280ed..af1639a 100644 --- a/src/web/components/sso.html +++ b/src/web/components/sso.html @@ -1,381 +1,79 @@
-
-
- Work in Progress -
-

The SSO feature is currently under development.

-
+

SSO

+

Single Sign-On (SSO) and authentication providers settings

-
- \ No newline at end of file + \ No newline at end of file diff --git a/src/web/snippet/sso_app.html b/src/web/snippet/sso_app.html deleted file mode 100644 index 470fde1..0000000 --- a/src/web/snippet/sso_app.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - -
-
-
-

SSO App Management

-
-

Work in progress

-
-
- - \ No newline at end of file diff --git a/src/web/snippet/sso_user.html b/src/web/snippet/sso_user.html deleted file mode 100644 index 5cd148f..0000000 --- a/src/web/snippet/sso_user.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - -
-
-
-

SSO User Management

-
-

Work in progress

-
-
- - \ No newline at end of file