mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-07 05:38:30 +02:00
Added basic oauth module structure (wip)
- Added struct for oauth - Added interception handler for Zoraxy SSO - Added user structure for SSO
This commit is contained in:
34
src/mod/auth/sso/app.go
Normal file
34
src/mod/auth/sso/app.go
Normal file
@@ -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
|
||||
}
|
240
src/mod/auth/sso/handlers.go
Normal file
240
src/mod/auth/sso/handlers.go
Normal file
@@ -0,0 +1,240 @@
|
||||
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))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
s.Log("Failed to start SSO portal server", err)
|
||||
utils.SendErrorResponse(w, "failed to start SSO portal server")
|
||||
return
|
||||
}
|
||||
//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) {
|
||||
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
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
//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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
}
|
295
src/mod/auth/sso/oauth2.go
Normal file
295
src/mod/auth/sso/oauth2.go
Normal file
@@ -0,0 +1,295 @@
|
||||
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)
|
||||
}
|
1
src/mod/auth/sso/oauth_test.go
Normal file
1
src/mod/auth/sso/oauth_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package sso
|
123
src/mod/auth/sso/server.go
Normal file
123
src/mod/auth/sso/server.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package sso
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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 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 {
|
||||
h.ssoPortalServer = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(h.Config.PortalServerPort),
|
||||
Handler: h.ssoPortalMux,
|
||||
}
|
||||
err := h.ssoPortalServer.ListenAndServe()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
158
src/mod/auth/sso/sso.go
Normal file
158
src/mod/auth/sso/sso.go
Normal file
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
33
src/mod/auth/sso/static/auth.html
Normal file
33
src/mod/auth/sso/static/auth.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<!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>
|
43
src/mod/auth/sso/static/index.html
Normal file
43
src/mod/auth/sso/static/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!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>
|
29
src/mod/auth/sso/static/login.html
Normal file
29
src/mod/auth/sso/static/login.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!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>
|
309
src/mod/auth/sso/userHandlers.go
Normal file
309
src/mod/auth/sso/userHandlers.go
Normal file
@@ -0,0 +1,309 @@
|
||||
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
|
||||
}
|
115
src/mod/auth/sso/users.go
Normal file
115
src/mod/auth/sso/users.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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 //User ID, in UUIDv4 format
|
||||
Username string //Username
|
||||
PasswordHash string //Password hash
|
||||
TOTPCode string //2FA TOTP code
|
||||
Enable2FA bool //Enable 2FA for this user
|
||||
Subdomains map[string]*SubdomainAccessRule //Subdomain and access rule
|
||||
parent *SSOHandler //Parent SSO handler
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
Reference in New Issue
Block a user