Added experimental authelia support

- Integrated #33 code snippet
- Added UI for setting Authelia server address
- Updated authentication provider implementation
This commit is contained in:
Toby Chui 2024-12-15 15:52:59 +08:00
parent bb0f55018c
commit 2423d0fb3a
24 changed files with 267 additions and 2029 deletions

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -1 +0,0 @@
package sso

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Auth</title>
<link
rel="stylesheet"
href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
/>
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="jumbotron">
<form action="/oauth2/authorize" method="POST">
<h1>Authorize</h1>
<p>The client would like to perform actions on your behalf.</p>
<p>
<button
type="submit"
class="btn btn-primary btn-lg"
style="width:200px;"
>
Allow
</button>
</p>
</form>
</div>
</div>
</body>
</html>

View File

@ -1,43 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Login Page</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div class="ui container">
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui teal image header">
<div class="content">
Log in to your account
</div>
</h2>
<form class="ui large form">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="Password">
</div>
</div>
<div class="ui fluid large teal submit button">Login</div>
</div>
<div class="ui error message"></div>
</form>
<div class="ui message">
New to us? <a href="#">Sign Up</a>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.js"></script>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<script src="//code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<h1>Login In</h1>
<form action="/oauth2/login" method="POST">
<div class="form-group">
<label for="username">User Name</label>
<input type="text" class="form-control" name="username" required placeholder="Please enter your user name">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Please enter your password">
</div>
<button type="submit" class="btn btn-success">Login</button>
</form>
</div>
</body>
</html>

View File

@ -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
}

View File

@ -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: "",
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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{

View File

@ -125,10 +125,12 @@
</td>
<td data-label="" editable="true" datatype="vdir">${vdList}</td>
<td data-label="" editable="true" datatype="advanced" style="width: 350px;">
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui green check icon"></i> Basic Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x1 && subd.RequireRateLimit?"<br>":""}
${subd.AuthenticationProvider.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${!subd.AuthenticationProvider.AuthMethod == 0x1 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
${subd.AuthenticationProvider.AuthMethod == 0x1?`<i class="ui grey key icon"></i> Basic Auth`:``}
${subd.AuthenticationProvider.AuthMethod == 0x2?`<i class="ui blue key icon"></i> Authelia`:``}
${subd.AuthenticationProvider.AuthMethod == 0x3?`<i class="ui yellow key icon"></i> Oauth2`:``}
${subd.AuthenticationProvider.AuthMethod != 0x0 && subd.RequireRateLimit?"<br>":""}
${subd.RequireRateLimit?`<i class="ui green check icon"></i> Rate Limit @ ${subd.RateLimit} req/s`:``}
${subd.AuthenticationProvider.AuthMethod == 0x0 && !subd.RequireRateLimit?`<small style="opacity: 0.3; pointer-events: none; user-select: none;">No Special Settings</small>`:""}
</td>
<td class="center aligned ignoremw" editable="true" datatype="action" data-label="">
<div class="ui toggle tiny fitted checkbox" style="margin-bottom: -0.5em; margin-right: 0.4em;" title="Enable / Disable Rule">
@ -194,6 +196,11 @@
}
let rule = accessRuleMap[thisAccessRuleID];
if (rule == undefined){
//Missing config or config too old
$(this).html(`<i class="ui red exclamation triangle icon"></i> <b style="color: #db2828;">Access Rule Error</b>`);
return;
}
let icon = `<i class="ui grey filter icon"></i>`;
if (rule.ID == "default"){
icon = `<i class="ui yellow star icon"></i>`;
@ -269,12 +276,8 @@
</button>`);
}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(`<div class="ui checkbox" style="margin-top: 0.4em;">
<input type="checkbox" class="RequireBasicAuth" ${basicAuthCheckstate}>
<label>Require Basic Auth</label>
column.empty().append(`
<div class="grouped fields authProviderPicker">
<label><b>Authentication Provider</b></label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="0" name="authProviderType" ${authProvider==0x0?"checked":""}>
<label>None (Anyone can access)</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="1" name="authProviderType" ${authProvider==0x1?"checked":""}>
<label>Basic Auth</label>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" value="2" name="authProviderType" ${authProvider==0x2?"checked":""}>
<label>Authelia</label>
</div>
</div>
</div>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editBasicAuthCredentials('${uuid}');"><i class="ui blue user circle icon"></i> Edit Credentials</button>
<br>
<button class="ui basic compact tiny button" style="margin-left: 0.4em; margin-top: 0.4em;" onclick="editCustomHeaders('${uuid}');"><i class="heading icon"></i> Custom Headers</button>
<div class="ui basic advance segment" style="padding: 0.4em !important; border-radius: 0.4em;">
@ -328,6 +347,7 @@
<div>
`);
$('.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,
},

View File

@ -1,381 +1,79 @@
<div class="standardContainer">
<div class="ui basic segment">
<div class="ui message">
<div class="header">
Work in Progress
</div>
<p>The SSO feature is currently under development.</p>
</div>
<h2>SSO</h2>
<p>Single Sign-On (SSO) and authentication providers settings </p>
</div>
</div>
<!--
<div class="standardContainer">
<div class="ui basic segment">
<h2>Zoraxy SSO / Oauth</h2>
<p>A centralized authentication system for all your subdomains</p>
<div class="ui divider"></div>
<div class="ui basic segment enabled ssoRunningState">
<h4 class="ui header" id="ssoRunningState">
<i class="circle check icon"></i>
<div class="content">
<span class="webserv_status">Running</span>
<div class="sub header">Listen port :<span class="oauthserv_port">8081</span></div>
</div>
</h4>
</div>
<div class="ui form">
<h3 class="ui dividing header">Oauth2 Server Settings</h3>
<div class="field">
<div class="ui toggle checkbox">
<input type="checkbox" name="enableOauth2">
<label>Enable Oauth2 Server<br>
<small>Oauth2 server for handling external authentication requests</small></label>
</div>
</div>
<div class="field">
<label>Oauth2 Server Port</label>
<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>
<div class="field">
<label>Auth URL</label>
<div class="ui action input">
<input type="text" name="authURL" placeholder="https://auth.yourdomain.com">
<button id="saveAuthURLBtn" class="ui basic blue button"><i class="ui blue save icon"></i> Save</button>
</div>
<small>The exposed authentication URL of the Oauth2 server, usually <code>https://auth.example.com</code> or <code>https://sso.yourdomain.com</code>. <b>Remember to include the http:// or https:// in your URL.</b></small>
</div>
</div>
<br>
<div class="ui form">
<h3 class="ui dividing header">Zoraxy SSO Settings</h3>
<div class="field">
<label>Default Redirection URL </label>
<div class="ui fluid input">
<input type="text" name="defaultSiteURL" placeholder="https://yourdomain.com">
</div>
<small>The default URL to redirect to after login if redirection target is not set</small>
</div>
<button class="ui basic button"> <i class="ui green check icon"></i> Apply Changes </button>
</div>
<div class="ui basic message">
<div class="ui basic segment">
<div class="ui yellow message">
<div class="header">
<i class="ui yellow exclamation triangle icon"></i> Important Notes about Zoraxy SSO
Experimental Feature
</div>
<p>Zoraxy SSO, if enabled in HTTP Proxy rule, will automatically intercept the proxy request and provide an SSO interface on upstreams that do not support OAuth natively.
It is basically like basic auth with a login page. <b> The same user credential can be used in OAuth sign-in and Zoraxy SSO sign-in.</b>
</p>
</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>
</h3>
<table class="ui celled table">
<thead>
<tr>
<th>Username</th>
<th>Registered On</th>
<th>Reset Password</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoUsers">
<tr>
<td>admin</td>
<td>2020-01-01</td>
<td><button class="ui blue basic small icon button"><i class="ui blue key icon"></i></button></td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleUserListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisteredUserManager();" class="ui basic button"><i class="ui blue users icon"></i> Manage Registered Users</button>
</div>
<div class="ui divider"></div>
<div>
<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>
<table class="ui celled table">
<thead>
<tr>
<th>App Name</th>
<th>Domain</th>
<th>App ID</th>
<th>Registered On</th>
<th>Remove</th>
</tr>
</thead>
<tbody id="registeredSsoApps">
<tr>
<td>My App</td>
<td><a href="//example.com" target="_blank">example.com</a></td>
<td>123456</td>
<td>2020-01-01</td>
<td><button class="ui red basic small icon button"><i class="ui red trash icon"></i></button></td>
</tr>
</tbody>
</table>
<button onclick="handleRegisterAppListRefresh();" class="ui basic right floated button"><i class="ui green refresh icon"></i> Refresh</button>
<button onclick="openRegisterAppManagementSnippet();" class="ui basic button"><i style="font-size: 1em; margin-top: -0.2em;" class="ui green th large icon"></i> Manage Registered App</button>
<p></p>
<p>Please note that this feature is still in development and may not work as expected.</p>
</div>
</div>
<div class="ui divider"></div>
<div class="ui basic segment">
<h3>Authelia</h3>
<p>Configuration settings for Authelia authentication provider.</p>
<form class="ui form">
<div class="field">
<label for="autheliaServerUrl">Authelia Server URL</label>
<input type="text" id="autheliaServerUrl" name="autheliaServerUrl" placeholder="Enter Authelia Server URL">
<small>Example: auth.example.com</small>
</div>
<div class="field">
<div class="ui checkbox">
<input type="checkbox" id="useHttps" name="useHttps">
<label for="useHttps">Use HTTPS</label>
<small>Check this if your authelia server uses HTTPS</small>
</div>
</div>
<button class="ui basic button" onclick="event.preventDefault(); updateAutheliaSettings();"><i class="green check icon"></i> Apply Change</button>
</form>
</div>
<div class="ui divider"></div>
</div>
<script>
$("input[name=oauth2Port]").on("change", function() {
$(".ssoPort").text($(this).val());
});
function updateSSOStatus(){
$.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");
$("input[name=enableOauth2]").parent().checkbox("set checked");
}else{
$(".ssoRunningState").removeClass("enabled");
$("#ssoRunningState .webserv_status").html('Stopped');
$(".ssoRunningState i").attr("class", "circle times icon");
$("input[name=enableOauth2]").parent().checkbox("set unchecked");
}
$("input[name=oauth2Port]").val(data.ListeningPort);
$(".oauthserv_port").text(data.ListeningPort);
$("input[name=authURL]").val(data.AuthURL);
$(document).ready(function() {
$.cjax({
url: '/api/sso/Authelia',
method: 'GET',
dataType: 'json',
success: function(data) {
$('#autheliaServerUrl').val(data.autheliaURL);
$('#useHttps').prop('checked', data.useHTTPS);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching SSO settings:', textStatus, errorThrown);
}
});
}
});
function updateAutheliaSettings(){
var autheliaServerUrl = $('#autheliaServerUrl').val();
var useHttps = $('#useHttps').prop('checked');
function initSSOStatus(){
$.get("/api/sso/status", function(data){
//Update the SSO status from the server
updateSSOStatus();
//Bind events to the enable checkbox
$("input[name=enableOauth2]").off("change").on("change", function(){
var checked = $(this).prop("checked");
$.cjax({
url: "/api/sso/enable",
method: "POST",
data: {
enable: checked
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to toggle SSO: " + data.error, false);
//Unbind the event to prevent infinite loop
$("input[name=enableOauth2]").off("change");
}else{
initSSOStatus();
}
}
});
});
});
}
initSSOStatus();
/* Save the Oauth server port */
function saveOauthServerPort(){
var port = $("input[name=oauth2Port]").val();
//Check if the port is valid
if (port < 1 || port > 65535){
msgbox("Invalid port number", false);
return;
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setPort",
method: "POST",
url: '/api/sso/Authelia',
method: 'POST',
data: {
port: port
autheliaURL: autheliaServerUrl,
useHTTPS: useHttps
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
$.msgbox(data.error, false);
return;
}
updateSSOStatus();
}
});
}
//Bind the save button to the saveOauthServerPort function
$("#saveOauthServerPortBtn").on("click", function() {
saveOauthServerPort();
});
$("input[name=oauth2Port]").on("keypress", function(e) {
if (e.which == 13) {
saveOauthServerPort();
}
});
/* Save the Oauth server URL (aka AuthURL) */
function saveAuthURL(){
var url = $("input[name=authURL]").val();
//Make sure the url contains http:// or https://
if (!url.startsWith("http://") && !url.startsWith("https://")){
msgbox("Invalid URL. Make sure to include http:// or https://", false);
$("input[name=authURL]").parent().parent().addClass("error");
return;
}else{
$("input[name=authURL]").parent().parent().removeClass("error");
}
//Use cjax to send the port to the server with csrf token
$.cjax({
url: "/api/sso/setAuthURL",
method: "POST",
data: {
"auth_url": url
msgbox('Authelia settings updated', true);
console.log('Authelia settings updated:', data);
},
success: function(data) {
if (data.error != undefined) {
msgbox("Failed to update Oauth server port: " + data.error, false);
} else {
msgbox("Oauth server port updated", true);
}
updateSSOStatus();
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error updating Authelia settings:', textStatus, errorThrown);
}
});
}
//Bind the save button to the saveAuthURL function
$("#saveAuthURLBtn").on("click", function() {
saveAuthURL();
});
$("input[name=authURL]").on("keypress", function(e) {
if (e.which == 13) {
saveAuthURL();
}
});
/* Registered Apps Event Handlers */
//Function to initialize the registered app table
function initRegisteredAppTable(){
$.get("/api/sso/app/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered apps: " + data.error, false);
}else{
var tbody = $("#registeredSsoApps");
tbody.empty();
for(var i = 0; i < data.length; i++){
var app = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(app.AppName));
tr.append($("<td>").html('<a href="//'+app.Domain+'" target="_blank">'+app.Domain+'</a>'));
tr.append($("<td>").text(app.AppID));
tr.append($("<td>").text(app.RegisteredOn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeApp(app.AppID);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
initRegisteredAppTable();
//Also bind the refresh button to the initRegisteredAppTable function
function handleRegisterAppListRefresh(){
initRegisteredAppTable();
}
function openRegisterAppManagementSnippet(){
//Open the register app management snippet
showSideWrapper("snippet/sso_app.html");
}
//Bind the remove button to the removeApp function
function removeApp(appID){
$.cjax({
url: "/api/sso/removeApp",
method: "POST",
data: {
appID: appID
},
success: function(data){
if(data.error != undefined){
msgbox("Failed to remove app: " + data.error, false);
}else{
msgbox("App removed", true);
updateSSOStatus();
}
}
});
}
/* Registered Users Event Handlers */
function initUserList(){
$.get("/api/sso/user/list", function(data){
if(data.error != undefined){
msgbox("Failed to get registered users: " + data.error, false);
}else{
var tbody = $("#registeredSsoUsers");
tbody.empty();
for(var i = 0; i < data.length; i++){
var user = data[i];
var tr = $("<tr>");
tr.append($("<td>").text(user.Username));
tr.append($("<td>").text(user.RegisteredOn));
var resetBtn = $("<button>").addClass("ui blue basic small icon button").html('<i class="ui blue key icon"></i>');
resetBtn.on("click", function(){
resetPassword(user.Username);
});
tr.append($("<td>").append(resetBtn));
var removeBtn = $("<button>").addClass("ui red basic small icon button").html('<i class="ui red trash icon"></i>');
removeBtn.on("click", function(){
removeUser(user.Username);
});
tr.append($("<td>").append(removeBtn));
tbody.append(tr);
}
if (data.length == 0){
tbody.append($("<tr>").append($("<td>").attr("colspan", 4).html(`<i class="ui green circle check icon"></i> No registered users`)));
}
}
});
}
//Bind the refresh button to the initUserList function
function handleUserListRefresh(){
initUserList();
}
function openRegisteredUserManager(){
//Open the registered user management snippet
showSideWrapper("snippet/sso_user.html");
}
</script>
-->
</script>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/utils.js"></script>
<style>
body{
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui basic segment">
<h2 class="ui header">SSO App Management</h2>
<div class="ui divider"></div>
<h3>Work in progress</h3>
</div>
</div>
</body>
</html>

View File

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="zoraxy.csrf.Token" content="{{.csrfToken}}">
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
<script src="../script/jquery-3.6.0.min.js"></script>
<script src="../script/semantic/semantic.min.js"></script>
<script src="../script/utils.js"></script>
<style>
body{
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<link rel="stylesheet" href="../darktheme.css">
<script src="../script/darktheme.js"></script>
<br>
<div class="ui container">
<div class="ui basic segment">
<h2 class="ui header">SSO User Management</h2>
<div class="ui divider"></div>
<h3>Work in progress</h3>
</div>
</div>
</body>
</html>