Browse Source

Added sso intercept

Toby Chui 6 months ago
parent
commit
3cb5e771b0

+ 39 - 0
mod/auth/sso/handlers.go

@@ -168,6 +168,18 @@ func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
 		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")
@@ -197,5 +209,32 @@ func (s *SSOHandler) HandleRegisterApp(w http.ResponseWriter, r *http.Request) {
 		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)
+	}
+
+}

+ 5 - 1
mod/auth/sso/server.go

@@ -72,6 +72,10 @@ func (h *SSOHandler) RestartSSOServer() error {
 	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
@@ -93,7 +97,7 @@ func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
 	}
 
 	//Check if the user exists
-	userEntry, err := h.SSO_GetUser(username)
+	userEntry, err := h.GetSSOUser(username)
 	if err != nil {
 		utils.SendErrorResponse(w, "user not found")
 		return

+ 23 - 12
mod/auth/sso/sso.go

@@ -52,11 +52,10 @@ func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
 		HttpOnly: false,
 		SameSite: 0,
 	}
-	//Create a table for the new sso user management system
-	err := config.Database.NewTable("sso_users")
-	if err != nil {
-		return nil, err
-	}
+
+	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{
@@ -64,6 +63,9 @@ func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
 		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 {
@@ -78,6 +80,7 @@ func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
 }
 
 func (h *SSOHandler) RestorePreviousRunningState() {
+	//Load the previous SSO state
 	ssoEnabled := false
 	ssoPort := 5488
 	ssoAuthURL := ""
@@ -98,34 +101,42 @@ func (h *SSOHandler) RestorePreviousRunningState() {
 	}
 }
 
-// ServeForwardAuth handle the SSO request by forwarding auth to the authelia server
-// return false if the request is not authorized and shall not be proceed
-// Note that only accounts that have SSO enabled will be handled by this handler
+// 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, h.Config.AuthURL+"?m=new&t="+originalRequestURL, http.StatusFound)
+		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, h.Config.AuthURL+"?m=expired&t="+originalRequestURL, http.StatusFound)
+		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.SSO_GetUser(userName)
+	user, err := h.GetSSOUser(userName)
 	if err != nil {
 		//User might have been removed from SSO. Redirect to auth subdomain
-		http.Redirect(w, r, h.Config.AuthURL, http.StatusFound)
+		http.Redirect(w, r, redirectAuthURL, http.StatusFound)
 		return false
 	}
 

+ 17 - 22
mod/auth/sso/userHandlers.go

@@ -67,13 +67,13 @@ func (s *SSOHandler) HandleEditUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if !(s.SSO_UserExists(userID)) {
+	if !(s.SSOUserExists(userID)) {
 		utils.SendErrorResponse(w, "user not found")
 		return
 	}
 
 	//Load the user entry from database
-	userEntry, err := s.SSO_GetUser(userID)
+	userEntry, err := s.GetSSOUser(userID)
 	if err != nil {
 		utils.SendErrorResponse(w, "failed to load user entry")
 		return
@@ -108,7 +108,7 @@ func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if !(s.SSO_UserExists(userID)) {
+	if !(s.SSOUserExists(userID)) {
 		utils.SendErrorResponse(w, "user not found")
 		return
 	}
@@ -124,23 +124,16 @@ func (s *SSOHandler) HandleRemoveUser(w http.ResponseWriter, r *http.Request) {
 
 // HandleListUser list all users in the SSO system
 func (s *SSOHandler) HandleListUser(w http.ResponseWriter, r *http.Request) {
-	entries, err := s.Config.Database.ListTable("sso_users")
+	ssoUsers, err := s.ListSSOUsers()
 	if err != nil {
 		utils.SendErrorResponse(w, "failed to list users")
 		return
 	}
-	ssoUsers := map[string]*UserEntry{}
-	for _, keypairs := range entries {
-		userid := string(keypairs[0])
-		group := new(UserEntry)
-		json.Unmarshal(keypairs[1], &group)
-		ssoUsers[userid] = group
-	}
-
 	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 {
@@ -148,12 +141,12 @@ func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	if !(s.SSO_UserExists(userid)) {
+	if !(s.SSOUserExists(userid)) {
 		utils.SendErrorResponse(w, "user not found")
 		return
 	}
 
-	UserEntry, err := s.SSO_GetUser(userid)
+	UserEntry, err := s.GetSSOUser(userid)
 	if err != nil {
 		utils.SendErrorResponse(w, "failed to load user entry")
 		return
@@ -185,6 +178,7 @@ func (s *SSOHandler) HandleAddSubdomain(w http.ResponseWriter, r *http.Request)
 	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 {
@@ -192,12 +186,12 @@ func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	if !(s.SSO_UserExists(userid)) {
+	if !(s.SSOUserExists(userid)) {
 		utils.SendErrorResponse(w, "user not found")
 		return
 	}
 
-	UserEntry, err := s.SSO_GetUser(userid)
+	UserEntry, err := s.GetSSOUser(userid)
 	if err != nil {
 		utils.SendErrorResponse(w, "failed to load user entry")
 		return
@@ -220,6 +214,7 @@ func (s *SSOHandler) HandleRemoveSubdomain(w http.ResponseWriter, r *http.Reques
 	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 {
@@ -227,12 +222,12 @@ func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if !(s.SSO_UserExists(userid)) {
+	if !(s.SSOUserExists(userid)) {
 		utils.SendErrorResponse(w, "user not found")
 		return
 	}
 
-	UserEntry, err := s.SSO_GetUser(userid)
+	UserEntry, err := s.GetSSOUser(userid)
 	if err != nil {
 		utils.SendErrorResponse(w, "failed to load user entry")
 		return
@@ -258,12 +253,12 @@ func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	if !(s.SSO_UserExists(userid)) {
+	if !(s.SSOUserExists(userid)) {
 		utils.SendErrorResponse(w, "user not found")
 		return
 	}
 
-	UserEntry, err := s.SSO_GetUser(userid)
+	UserEntry, err := s.GetSSOUser(userid)
 	if err != nil {
 		utils.SendErrorResponse(w, "failed to load user entry")
 		return
@@ -288,12 +283,12 @@ func (s *SSOHandler) HandleVerify2FA(w http.ResponseWriter, r *http.Request) (bo
 		return false, errors.New("invalid user ID given")
 	}
 
-	if !(s.SSO_UserExists(userid)) {
+	if !(s.SSOUserExists(userid)) {
 		utils.SendErrorResponse(w, "user not found")
 		return false, errors.New("user not found")
 	}
 
-	UserEntry, err := s.SSO_GetUser(userid)
+	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")

+ 18 - 2
mod/auth/sso/users.go

@@ -32,14 +32,14 @@ type UserEntry struct {
 	parent       *SSOHandler                     //Parent SSO handler
 }
 
-func (s *SSOHandler) SSO_UserExists(userid string) bool {
+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) SSO_GetUser(userid string) (UserEntry, error) {
+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)
@@ -50,6 +50,22 @@ func (s *SSOHandler) SSO_GetUser(userid string) (UserEntry, error) {
 	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

+ 11 - 2
mod/dynamicproxy/Server.go

@@ -21,7 +21,7 @@ import (
 			- Blacklist
 			- Whitelist
 		- Rate Limitor
-		- SSO Auth (wip)
+		- SSO Auth
 		- Basic Auth
 		- Vitrual Directory Proxy
 		- Subdomain Proxy
@@ -78,7 +78,16 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		if sep.RequireRateLimit {
 			err := h.handleRateLimitRouting(w, r, sep)
 			if err != nil {
-				h.Parent.Option.Logger.LogHTTPRequest(r, "host", 429)
+				h.Parent.Option.Logger.LogHTTPRequest(r, "host", 307)
+				return
+			}
+		}
+
+		//SSO Interception Mode
+		if sep.UseSSOIntercept {
+			allowPass := h.Parent.Option.SSOHandler.ServeForwardAuth(w, r)
+			if !allowPass {
+				h.Parent.Option.Logger.LogHTTPRequest(r, "sso-x", 307)
 				return
 			}
 		}

+ 3 - 0
mod/dynamicproxy/typedef.go

@@ -7,6 +7,7 @@ import (
 	"sync"
 
 	"imuslab.com/zoraxy/mod/access"
+	"imuslab.com/zoraxy/mod/auth/sso"
 	"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
 	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
 	"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
@@ -44,6 +45,7 @@ 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
 }
 
@@ -142,6 +144,7 @@ type ProxyEndpoint struct {
 	RequireBasicAuth        bool                      //Set to true to request basic auth before proxy
 	BasicAuthCredentials    []*BasicAuthCredentials   //Basic auth credentials
 	BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target
+	UseSSOIntercept         bool                      //Allow SSO to intercept this endpoint and provide authentication via Oauth2 credentials
 
 	// Rate Limiting
 	RequireRateLimit bool

+ 1 - 0
reverseproxy.go

@@ -98,6 +98,7 @@ func ReverseProxtInit() {
 		WebDirectory:       *staticWebServerRoot,
 		AccessController:   accessController,
 		LoadBalancer:       loadBalancer,
+		SSOHandler:         ssoHandler,
 		Logger:             SystemWideLogger,
 	})
 	if err != nil {

+ 4 - 1
start.go

@@ -37,7 +37,10 @@ import (
 	Startup Sequence
 
 	This function starts the startup sequence of all
-	required modules
+	required modules. Their startup sequences are inter-dependent
+	and must be started in a specific order.
+
+	Don't touch this function unless you know what you are doing
 */
 
 var (

+ 19 - 11
web/components/sso.html

@@ -29,20 +29,28 @@
                 </div>
                 <small>Listening port of the Zoraxy internal Oauth2 Server.You can create a subdomain proxy rule to <code>127.0.0.1:<span class="ssoPort">5488</span></code></small>
             </div>
-            <h4 class="ui dividing header">Zoraxy SSO</h4>
-            <div class="field">
-                <div class="ui toggle checkbox">
-                    <input type="checkbox" name="enableZoraxySSO">
-                    <label>Enable Zoraxy SSO<br>
-                    <small>Use Zoraxy SSO credentials with upstreams that do not support oauth</small></label>
+        </div>
+        <div class="ui divider"></div>
+        <div>
+            <h3 class="ui header">
+                <i class="ui blue user circle icon"></i>
+                <div class="content">
+                    Registered Users
+                  <div class="sub header">A list of users that are registered with the SSO server</div>
                 </div>
-            </div>
-            
+            </h3>
         </div>
+        <div class="ui divider"></div>
         <div>
-            <p>List of Registered Apps in</p>
+            <h3 class="ui header">
+                <i class="ui green th icon"></i>
+                <div class="content">
+                    Registered Apps
+                  <div class="sub header">A list of apps that are registered with the SSO server</div>
+                </div>
+            </h3>
+            <p></p>
         </div>
-
     </div>
 </div>
 
@@ -69,7 +77,7 @@
                 }
                 $("input[name=enableZoraxySSO]").prop("checked", data.Enabled);
                 $("input[name=oauth2Port]").val(data.ListeningPort);
-                
+
             }
         });
     }