Browse Source

Added more oauth

Toby Chui 6 months ago
parent
commit
37866e114c
9 changed files with 564 additions and 261 deletions
  1. 13 0
      api.go
  2. 2 0
      main.go
  3. 34 0
      mod/auth/sso/app.go
  4. 107 236
      mod/auth/sso/handlers.go
  5. 11 0
      mod/auth/sso/server.go
  6. 21 10
      mod/auth/sso/sso.go
  7. 314 0
      mod/auth/sso/userHandlers.go
  8. 15 14
      start.go
  9. 47 1
      web/components/sso.html

+ 13 - 0
api.go

@@ -95,6 +95,19 @@ func initAPIs(targetMux *http.ServeMux) {
 	authRouter.HandleFunc("/api/cert/checkDefault", handleDefaultCertCheck)
 	authRouter.HandleFunc("/api/cert/delete", handleCertRemove)
 
+	//SSO and Oauth
+	authRouter.HandleFunc("/api/sso/status", ssoHandler.HandleSSOStatus)
+	authRouter.HandleFunc("/api/sso/start", ssoHandler.HandleStartSSOPortal)
+	authRouter.HandleFunc("/api/sso/stop", ssoHandler.HandleStopSSOPortal)
+	authRouter.HandleFunc("/api/sso/setPort", ssoHandler.HandlePortChange)
+	authRouter.HandleFunc("/api/sso/setAuthURL", ssoHandler.HandleSetAuthURL)
+	//authRouter.HandleFunc("/api/sso/registerApp", ssoHandler.HandleRegisterApp)
+
+	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)
+
 	//Redirection config
 	authRouter.HandleFunc("/api/redirect/list", handleListRedirectionRules)
 	authRouter.HandleFunc("/api/redirect/add", handleAddRedirectionRule)

+ 2 - 0
main.go

@@ -16,6 +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/database"
 	"imuslab.com/zoraxy/mod/dockerux"
 	"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
@@ -95,6 +96,7 @@ 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
 
 	//Helper modules
 	EmailSender       *email.Sender         //Email sender that handle email sending

+ 34 - 0
mod/auth/sso/app.go

@@ -0,0 +1,34 @@
+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
+}

+ 107 - 236
mod/auth/sso/handlers.go

@@ -1,329 +1,200 @@
 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"
-	"errors"
 	"net/http"
+	"strings"
 
 	"github.com/gofrs/uuid"
-	"imuslab.com/zoraxy/mod/auth"
 	"imuslab.com/zoraxy/mod/utils"
 )
 
-/* Handlers for SSO user management */
-
-// 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
+// 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
 	}
 
-	password, err := utils.PostPara(r, "password")
-	if err != nil {
-		utils.SendErrorResponse(w, "invalid password given")
-		return
+	status := SSOStatus{
+		Enabled: s.ssoPortalServer != nil,
+		//SSOInterceptEnabled: s.ssoInterceptEnabled,
+		ListeningPort: s.Config.PortalServerPort,
+		AuthURL:       s.Config.AuthURL,
 	}
 
-	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)
+	js, _ := json.Marshal(status)
+	utils.SendJSONResponse(w, string(js))
 }
 
-// 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")
+// HandleStartSSOPortal handle the request to start the SSO portal server
+func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
+	err := s.StartSSOPortal()
 	if err != nil {
-		utils.SendErrorResponse(w, "invalid user ID given")
-		return
-	}
-
-	if !(s.SSO_UserExists(userID)) {
-		utils.SendErrorResponse(w, "user not found")
+		s.Log("Failed to start SSO portal server", err)
+		utils.SendErrorResponse(w, "failed to start SSO portal server")
 		return
 	}
-
-	//Load the user entry from database
-	userEntry, err := s.SSO_GetUser(userID)
+	//Write current state to database
+	err = s.Config.Database.Write("sso_conf", "enabled", true)
 	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")
+		utils.SendErrorResponse(w, "failed to update SSO state")
 		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")
+// HandleStopSSOPortal handle the request to stop the SSO portal server
+func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
+	err := s.ssoPortalServer.Close()
 	if err != nil {
-		utils.SendErrorResponse(w, "invalid user ID given")
-		return
-	}
-
-	if !(s.SSO_UserExists(userID)) {
-		utils.SendErrorResponse(w, "user not found")
+		s.Log("Failed to stop SSO portal server", err)
+		utils.SendErrorResponse(w, "failed to stop SSO portal server")
 		return
 	}
+	s.ssoPortalServer = nil
 
-	//Remove the user from the database
-	err = s.Config.Database.Delete("sso_users", userID)
+	//Write current state to database
+	err = s.Config.Database.Write("sso_conf", "enabled", false)
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to remove user")
+		utils.SendErrorResponse(w, "failed to update SSO state")
 		return
 	}
-	utils.SendOK(w)
-}
 
-// 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")
+	//Clear the cookie store and restart the server
+	err = s.RestartSSOServer()
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to list users")
+		utils.SendErrorResponse(w, "failed to restart SSO server")
 		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))
+	utils.SendOK(w)
 }
 
-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.SSO_UserExists(userid)) {
-		utils.SendErrorResponse(w, "user not found")
+// 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
 	}
 
-	UserEntry, err := s.SSO_GetUser(userid)
+	port, err := utils.PostInt(r, "port")
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to load user entry")
+		utils.SendErrorResponse(w, "invalid port given")
 		return
 	}
 
-	subdomain, err := utils.PostPara(r, "subdomain")
-	if err != nil {
-		utils.SendErrorResponse(w, "invalid subdomain given")
-		return
-	}
+	s.Config.PortalServerPort = port
 
-	allowAccess, err := utils.PostBool(r, "allow_access")
+	//Write to the database
+	err = s.Config.Database.Write("sso_conf", "port", port)
 	if err != nil {
-		utils.SendErrorResponse(w, "invalid allow access value given")
+		utils.SendErrorResponse(w, "failed to update port")
 		return
 	}
 
-	UserEntry.Subdomains[subdomain] = &SubdomainAccessRule{
-		Subdomain:   subdomain,
-		AllowAccess: allowAccess,
-	}
-
-	err = UserEntry.Update()
+	//Clear the cookie store and restart the server
+	err = s.RestartSSOServer()
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to update user entry")
+		utils.SendErrorResponse(w, "failed to restart SSO server")
 		return
 	}
-
 	utils.SendOK(w)
 }
 
-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")
+// 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
 	}
 
-	if !(s.SSO_UserExists(userid)) {
-		utils.SendErrorResponse(w, "user not found")
-		return
-	}
-
-	UserEntry, err := s.SSO_GetUser(userid)
-	if err != nil {
-		utils.SendErrorResponse(w, "failed to load user entry")
-		return
-	}
-
-	subdomain, err := utils.PostPara(r, "subdomain")
+	//Get the auth URL
+	authURL, err := utils.PostPara(r, "auth_url")
 	if err != nil {
-		utils.SendErrorResponse(w, "invalid subdomain given")
+		utils.SendErrorResponse(w, "invalid auth URL given")
 		return
 	}
 
-	delete(UserEntry.Subdomains, subdomain)
+	s.Config.AuthURL = authURL
 
-	err = UserEntry.Update()
+	//Write to the database
+	err = s.Config.Database.Write("sso_conf", "authurl", authURL)
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to update user entry")
+		utils.SendErrorResponse(w, "failed to update auth URL")
 		return
 	}
 
-	utils.SendOK(w)
-}
-
-func (s *SSOHandler) HandleEnable2FA(w http.ResponseWriter, r *http.Request) {
-	userid, err := utils.PostPara(r, "user_id")
+	//Clear the cookie store and restart the server
+	err = s.RestartSSOServer()
 	if err != nil {
-		utils.SendErrorResponse(w, "invalid user ID given")
+		utils.SendErrorResponse(w, "failed to restart SSO server")
 		return
 	}
-
-	if !(s.SSO_UserExists(userid)) {
-		utils.SendErrorResponse(w, "user not found")
-		return
-	}
-
-	UserEntry, err := s.SSO_GetUser(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))
+	utils.SendOK(w)
 }
 
-// Handle Disable 2FA for a user
-func (s *SSOHandler) HandleDisable2FA(w http.ResponseWriter, r *http.Request) {
-	userid, err := utils.PostPara(r, "user_id")
+// 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 user ID given")
+		utils.SendErrorResponse(w, "invalid app name given")
 		return
 	}
 
-	if !(s.SSO_UserExists(userid)) {
-		utils.SendErrorResponse(w, "user not found")
-		return
-	}
-
-	UserEntry, err := s.SSO_GetUser(userid)
+	id, err := utils.PostPara(r, "app_id")
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to load user entry")
-		return
+		//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()
 	}
 
-	UserEntry.Enable2FA = false
-	UserEntry.TOTPCode = ""
-
-	err = UserEntry.Update()
+	appDomain, err := utils.PostPara(r, "app_domain")
 	if err != nil {
-		utils.SendErrorResponse(w, "failed to update user entry")
+		utils.SendErrorResponse(w, "invalid app URL given")
 		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")
+	appURLs := strings.Split(appDomain, ",")
+	//Remove padding and trailing spaces in each URL
+	for i := range appURLs {
+		appURLs[i] = strings.TrimSpace(appURLs[i])
 	}
 
-	if !(s.SSO_UserExists(userid)) {
-		utils.SendErrorResponse(w, "user not found")
-		return false, errors.New("user not found")
+	//Create a new app entry
+	thisAppEntry := RegisteredUpstreamApp{
+		ID:              id,
+		Secret:          "",
+		Domain:          appURLs,
+		Scopes:          []string{},
+		SessionDuration: 3600,
 	}
 
-	UserEntry, err := s.SSO_GetUser(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
-
-}
-
-/* Handlers for SSO portal server */
+	js, _ := json.Marshal(thisAppEntry)
 
-func (s *SSOHandler) HandleStartSSOPortal(w http.ResponseWriter, r *http.Request) {
-	err := s.StartSSOPortal()
-	if err != nil {
-		s.Log("Failed to start SSO portal server", err)
-		utils.SendErrorResponse(w, "failed to start SSO portal server")
-		return
-	}
-	utils.SendOK(w)
-}
-
-func (s *SSOHandler) HandleStopSSOPortal(w http.ResponseWriter, r *http.Request) {
-	err := s.ssoPortalServer.Close()
+	//Create a new app in the database
+	err = s.Config.Database.Write("sso_apps", appName, string(js))
 	if err != nil {
-		s.Log("Failed to stop SSO portal server", err)
-		utils.SendErrorResponse(w, "failed to stop SSO portal server")
+		utils.SendErrorResponse(w, "failed to create new app")
 		return
 	}
 	utils.SendOK(w)

+ 11 - 0
mod/auth/sso/server.go

@@ -37,6 +37,7 @@ func (h *SSOHandler) InitSSOPortal(portalServerPort int) {
 }
 
 // StartSSOPortal start the SSO portal server
+// This function will block the main thread, call it in a goroutine
 func (h *SSOHandler) StartSSOPortal() error {
 	h.ssoPortalServer = &http.Server{
 		Addr:    ":" + strconv.Itoa(h.Config.PortalServerPort),
@@ -61,6 +62,16 @@ func (h *SSOHandler) StopSSOPortal() error {
 	return nil
 }
 
+// StartSSOPortal start the SSO portal server
+func (h *SSOHandler) RestartSSOServer() error {
+	err := h.StopSSOPortal()
+	if err != nil {
+		return err
+	}
+	go h.StartSSOPortal()
+	return nil
+}
+
 // HandleLogin handle the login request
 func (h *SSOHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
 	//Handle the login request

+ 21 - 10
mod/auth/sso/sso.go

@@ -30,16 +30,6 @@ type SSOConfig struct {
 	Logger           *logger.Logger
 }
 
-// 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
-}
-
 // SSOHandler is the main SSO handler structure
 type SSOHandler struct {
 	cookieStore     *sessions.CookieStore
@@ -87,6 +77,27 @@ func NewSSOHandler(config *SSOConfig) (*SSOHandler, error) {
 	return &thisHandler, nil
 }
 
+func (h *SSOHandler) RestorePreviousRunningState() {
+	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 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

+ 314 - 0
mod/auth/sso/userHandlers.go

@@ -0,0 +1,314 @@
+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.SSO_UserExists(userID)) {
+		utils.SendErrorResponse(w, "user not found")
+		return
+	}
+
+	//Load the user entry from database
+	userEntry, err := s.SSO_GetUser(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.SSO_UserExists(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) {
+	entries, err := s.Config.Database.ListTable("sso_users")
+	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))
+}
+
+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.SSO_UserExists(userid)) {
+		utils.SendErrorResponse(w, "user not found")
+		return
+	}
+
+	UserEntry, err := s.SSO_GetUser(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)
+}
+
+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.SSO_UserExists(userid)) {
+		utils.SendErrorResponse(w, "user not found")
+		return
+	}
+
+	UserEntry, err := s.SSO_GetUser(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)
+}
+
+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.SSO_UserExists(userid)) {
+		utils.SendErrorResponse(w, "user not found")
+		return
+	}
+
+	UserEntry, err := s.SSO_GetUser(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.SSO_UserExists(userid)) {
+		utils.SendErrorResponse(w, "user not found")
+		return
+	}
+
+	UserEntry, err := s.SSO_GetUser(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.SSO_UserExists(userid)) {
+		utils.SendErrorResponse(w, "user not found")
+		return false, errors.New("user not found")
+	}
+
+	UserEntry, err := s.SSO_GetUser(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
+}

+ 15 - 14
start.go

@@ -125,6 +125,21 @@ func startupSequence() {
 		panic(err)
 	}
 
+	//Create an SSO handler
+	sysdb.NewTable("sso_conf")
+	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 a statistic collector
 	statisticCollector, err = statistic.NewStatisticCollector(statistic.CollectorOption{
 		Database: sysdb,
@@ -297,20 +312,6 @@ func startupSequence() {
 		SystemWideLogger.PrintAndLog("warning", "Invalid start flag combination: docker=true && runtime.GOOS == windows. Running in docker UX development mode.", nil)
 	}
 	DockerUXOptimizer = dockerux.NewDockerOptimizer(*runningInDocker, SystemWideLogger)
-
-	/* Test Code */
-	//TODO: MOVE THIS INTO DYNAMIC PROXY CORE
-	ssoHandler, err := sso.NewSSOHandler(&sso.SSOConfig{
-		SystemUUID:       nodeUUID,
-		PortalServerPort: 5488,
-		AuthURL:          "http://auth.localhost",
-		Database:         sysdb,
-		Logger:           SystemWideLogger,
-	})
-	if err != nil {
-		log.Fatal(err)
-	}
-	go ssoHandler.StartSSOPortal()
 }
 
 // This sequence start after everything is initialized

+ 47 - 1
web/components/sso.html

@@ -23,7 +23,10 @@
             </div>
             <div class="field">
                 <label>Oauth2 Server Port</label>
-                <input type="number" name="oauth2Port" placeholder="Port" value="5488">
+                <div class="ui action input">
+                    <input type="number" name="oauth2Port" placeholder="Port" value="5488">
+                    <button id="saveOauthServerPortBtn" class="ui basic green button"><i class="ui green circle check icon"></i> Update</button>         
+                </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>
@@ -47,4 +50,47 @@
     $("input[name=oauth2Port]").on("change", function() {
         $(".ssoPort").text($(this).val());
     });
+
+    function initSSOStatus(){
+        $.get("/api/sso/status", function(data){
+            if(data.error != undefined){
+                //Show error message
+                $(".ssoRunningState").removeClass("enabled").addClass("disabled");
+                $("#ssoRunningState .webserv_status").html('Error: '+data.error);
+            }else{
+                if (data.Enabled){
+                    $(".ssoRunningState").addClass("enabled");
+                    $("#ssoRunningState .webserv_status").html('Running');
+                    $(".ssoRunningState i").attr("class", "circle check icon");
+                }else{
+                    $(".ssoRunningState").removeClass("enabled");
+                    $("#ssoRunningState .webserv_status").html('Stopped');
+                    $(".ssoRunningState i").attr("class", "circle times icon");
+                }
+                $("input[name=enableZoraxySSO]").prop("checked", data.Enabled);
+                $("input[name=oauth2Port]").val(data.ListeningPort);
+                
+            }
+        });
+    }
+    initSSOStatus();
+
+    $("#saveOauthServerPortBtn").on("click", function() {
+        var port = $("input[name=oauth2Port]").val();
+        //Use cjax to send the port to the server with csrf token
+        $.cjax({
+            url: "/api/sso/oauth2/setPort",
+            method: "POST",
+            data: {
+                port: port
+            },
+            success: function(data) {
+                if (data.error != undefined) {
+                    msgbox("Oauth server port updated", true);
+                } else {
+                    msgbox("Failed to update Oauth server port", false);
+                }
+            }
+        });
+    });
 </script>