From 8bf5a6067e0b361ffc0f161fb5f2eda9976aed68 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 3 Apr 2025 15:44:47 +0300 Subject: [PATCH] wip --- cmd/root.go | 17 ++-- internal/api/api.go | 21 +---- internal/api/api_test.go | 38 +++++---- internal/auth/auth.go | 121 +++++++++++++++++++--------- internal/handlers/handlers.go | 4 +- internal/hooks/hooks.go | 8 +- internal/oauth/oauth.go | 4 +- internal/providers/providers.go | 2 +- internal/types/api.go | 49 ++++++++++++ internal/types/config.go | 79 +++++++++++++++++++ internal/types/types.go | 134 ++------------------------------ 11 files changed, 266 insertions(+), 211 deletions(-) create mode 100644 internal/types/api.go create mode 100644 internal/types/config.go diff --git a/cmd/root.go b/cmd/root.go index 908cf8d..40178c9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -105,12 +105,8 @@ var rootCmd = &cobra.Command{ // Create api config apiConfig := types.APIConfig{ - Port: config.Port, - Address: config.Address, - Secret: config.Secret, - CookieSecure: config.CookieSecure, - SessionExpiry: config.SessionExpiry, - Domain: domain, + Port: config.Port, + Address: config.Address, } // Create docker service @@ -121,7 +117,14 @@ var rootCmd = &cobra.Command{ HandleError(err, "Failed to initialize docker") // Create auth service - auth := auth.NewAuth(docker, users, oauthWhitelist, config.SessionExpiry) + auth := auth.NewAuth(types.AuthConfig{ + Domain: domain, + Secret: config.Secret, + SessionExpiry: config.SessionExpiry, + CookieSecure: config.CookieSecure, + Users: users, + OAuthWhitelist: oauthWhitelist, + }, docker) // Create OAuth providers service providers := providers.NewProviders(oauthConfig) diff --git a/internal/api/api.go b/internal/api/api.go index ba3e4c3..080ad33 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -11,23 +11,21 @@ import ( "tinyauth/internal/handlers" "tinyauth/internal/types" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API { return &API{ - Config: config, Handlers: handlers, + Config: config, } } type API struct { - Config types.APIConfig Router *gin.Engine Handlers *handlers.Handlers + Config types.APIConfig } func (api *API) Init() { @@ -51,21 +49,6 @@ func (api *API) Init() { log.Debug().Msg("Setting up file server") fileServer := http.FileServer(http.FS(dist)) - // Setup cookie store - log.Debug().Msg("Setting up cookie store") - store := cookie.NewStore([]byte(api.Config.Secret)) - - // Use session middleware - store.Options(sessions.Options{ - Domain: api.Config.Domain, - Path: "/", - HttpOnly: true, - Secure: api.Config.CookieSecure, - MaxAge: api.Config.SessionExpiry, - }) - - router.Use(sessions.Sessions("tinyauth", store)) - // UI middleware router.Use(func(c *gin.Context) { // If not an API request, serve the UI diff --git a/internal/api/api_test.go b/internal/api/api_test.go index b9d49b6..fc0ae03 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -19,13 +19,16 @@ import ( "github.com/magiconair/properties/assert" ) +// User +var User = types.User{ + Username: "user", + Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass +} + // Simple API config for tests var apiConfig = types.APIConfig{ - Port: 8080, - Address: "0.0.0.0", - Secret: "super-secret-api-thing-for-tests", // It is 32 chars long - CookieSecure: false, - SessionExpiry: 3600, + Port: 8080, + Address: "0.0.0.0", } // Simple handlers config for tests @@ -38,15 +41,21 @@ var handlersConfig = types.HandlersConfig{ GenericName: "Generic", } +// Simple auth config for tests +var authConfig = types.AuthConfig{ + Domain: "localhost", + Secret: "super-secret-api-thing-for-tests", // It is 32 chars long + CookieSecure: false, + SessionExpiry: 3600, + Users: types.Users{ + User, + }, + OAuthWhitelist: []string{}, +} + // Cookie var cookie string -// User -var user = types.User{ - Username: "user", - Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass -} - // We need all this to be able to test the API func getAPI(t *testing.T) *api.API { // Create docker service @@ -61,12 +70,7 @@ func getAPI(t *testing.T) *api.API { } // Create auth service - auth := auth.NewAuth(docker, types.Users{ - { - Username: user.Username, - Password: user.Password, - }, - }, nil, apiConfig.SessionExpiry) + auth := auth.NewAuth(authConfig, docker) // Create providers service providers := providers.NewProviders(types.OAuthConfig{}) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index bfcef5f..9688e63 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,6 +1,7 @@ package auth import ( + "fmt" "regexp" "slices" "strings" @@ -8,31 +9,27 @@ import ( "tinyauth/internal/docker" "tinyauth/internal/types" - "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/gorilla/sessions" "github.com/rs/zerolog/log" "golang.org/x/crypto/bcrypt" ) -func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string, sessionExpiry int) *Auth { +func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth { return &Auth{ - Docker: docker, - Users: userList, - OAuthWhitelist: oauthWhitelist, - SessionExpiry: sessionExpiry, + Docker: docker, + Config: config, } } type Auth struct { - Users types.Users - Docker *docker.Docker - OAuthWhitelist []string - SessionExpiry int + Docker *docker.Docker + Config types.AuthConfig } func (auth *Auth) GetUser(username string) *types.User { // Loop through users and return the user if the username matches - for _, user := range auth.Users { + for _, user := range auth.Config.Users { if user.Username == username { return &user } @@ -47,12 +44,12 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool { func (auth *Auth) EmailWhitelisted(emailSrc string) bool { // If the whitelist is empty, allow all emails - if len(auth.OAuthWhitelist) == 0 { + if len(auth.Config.OAuthWhitelist) == 0 { return true } // Loop through the whitelist and return true if the email matches - for _, email := range auth.OAuthWhitelist { + for _, email := range auth.Config.OAuthWhitelist { if email == emailSrc { return true } @@ -62,11 +59,35 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool { return false } -func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) { +func (auth *Auth) GetCookieStore() *sessions.CookieStore { + // Create a new cookie store + store := sessions.NewCookieStore([]byte(auth.Config.Secret)) + + // Configure the cookie store + store.Options = &sessions.Options{ + Path: "/", + Domain: fmt.Sprintf(".%s", auth.Config.Domain), + Secure: auth.Config.CookieSecure, + MaxAge: auth.Config.SessionExpiry, + HttpOnly: true, + } + + // Set the cookie store + return store +} + +func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { log.Debug().Msg("Creating session cookie") + // Get cookie store + store := auth.GetCookieStore() + // Get session - sessions := sessions.Default(c) + sessions, err := store.Get(c.Request, "tinyauth") + + if err != nil { + return err + } log.Debug().Msg("Setting session cookie") @@ -76,43 +97,73 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) if data.TotpPending { sessionExpiry = 3600 } else { - sessionExpiry = auth.SessionExpiry + sessionExpiry = auth.Config.SessionExpiry } // Set data - sessions.Set("username", data.Username) - sessions.Set("provider", data.Provider) - sessions.Set("expiry", time.Now().Add(time.Duration(sessionExpiry)*time.Second).Unix()) - sessions.Set("totpPending", data.TotpPending) + sessions.Values["username"] = data.Username + sessions.Values["provider"] = data.Provider + sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() + sessions.Values["totpPending"] = data.TotpPending // Save session - sessions.Save() + err = sessions.Save(c.Request, c.Writer) + + if err != nil { + return err + } + + // Return nil + return nil } -func (auth *Auth) DeleteSessionCookie(c *gin.Context) { +func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { log.Debug().Msg("Deleting session cookie") + // Get cookie store + store := auth.GetCookieStore() + // Get session - sessions := sessions.Default(c) + sessions, err := store.Get(c.Request, "tinyauth") + + if err != nil { + return err + } // Clear session - sessions.Clear() + for key := range sessions.Values { + delete(sessions.Values, key) + } // Save session - sessions.Save() + err = sessions.Save(c.Request, c.Writer) + + if err != nil { + return err + } + + // Return nil + return nil } -func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { +func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { log.Debug().Msg("Getting session cookie") + // Get cookie store + store := auth.GetCookieStore() + // Get session - sessions := sessions.Default(c) + sessions, err := store.Get(c.Request, "tinyauth") + + if err != nil { + return types.SessionCookie{}, err + } // Get data - cookieUsername := sessions.Get("username") - cookieProvider := sessions.Get("provider") - cookieExpiry := sessions.Get("expiry") - cookieTotpPending := sessions.Get("totpPending") + cookieUsername := sessions.Values["username"] + cookieProvider := sessions.Values["provider"] + cookieExpiry := sessions.Values["expiry"] + cookieTotpPending := sessions.Values["totpPending"] // Convert interfaces to correct types username, usernameOk := cookieUsername.(string) @@ -123,7 +174,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { // Check if the cookie is invalid if !usernameOk || !providerOk || !expiryOk || !totpPendingOk { log.Warn().Msg("Session cookie invalid") - return types.SessionCookie{} + return types.SessionCookie{}, nil } // Check if the cookie has expired @@ -134,7 +185,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { auth.DeleteSessionCookie(c) // Return empty cookie - return types.SessionCookie{} + return types.SessionCookie{}, nil } log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie") @@ -144,12 +195,12 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { Username: username, Provider: provider, TotpPending: totpPending, - } + }, nil } func (auth *Auth) UserAuthConfigured() bool { // If there are users, return true - return len(auth.Users) > 0 + return len(auth.Config.Users) > 0 } func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) { diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 08a5839..1237191 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -19,20 +19,20 @@ import ( func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers { return &Handlers{ - Config: config, Auth: auth, Hooks: hooks, Providers: providers, Docker: docker, + Config: config, } } type Handlers struct { - Config types.HandlersConfig Auth *auth.Auth Hooks *hooks.Hooks Providers *providers.Providers Docker *docker.Docker + Config types.HandlersConfig } func (h *Handlers) AuthHandler(c *gin.Context) { diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 6921372..3079703 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -23,7 +23,13 @@ type Hooks struct { func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { // Get session cookie and basic auth - cookie := hooks.Auth.GetSessionCookie(c) + cookie, err := hooks.Auth.GetSessionCookie(c) + + if err != nil { + log.Error().Err(err).Msg("Failed to get session cookie") + return types.UserContext{} + } + basic := hooks.Auth.GetBasicAuth(c) // Check if basic auth is set diff --git a/internal/oauth/oauth.go b/internal/oauth/oauth.go index 86ca010..c3155b7 100644 --- a/internal/oauth/oauth.go +++ b/internal/oauth/oauth.go @@ -14,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth { } type OAuth struct { - Config oauth2.Config + Verifier string Context context.Context Token *oauth2.Token - Verifier string + Config oauth2.Config } func (oauth *OAuth) Init() { diff --git a/internal/providers/providers.go b/internal/providers/providers.go index a2ad349..ee99b30 100644 --- a/internal/providers/providers.go +++ b/internal/providers/providers.go @@ -17,11 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers { } type Providers struct { - Config types.OAuthConfig Github *oauth.OAuth Google *oauth.OAuth Tailscale *oauth.OAuth Generic *oauth.OAuth + Config types.OAuthConfig } func (providers *Providers) Init() { diff --git a/internal/types/api.go b/internal/types/api.go new file mode 100644 index 0000000..5faa4c8 --- /dev/null +++ b/internal/types/api.go @@ -0,0 +1,49 @@ +package types + +// LoginQuery is the query parameters for the login endpoint +type LoginQuery struct { + RedirectURI string `url:"redirect_uri"` +} + +// LoginRequest is the request body for the login endpoint +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// OAuthRequest is the request for the OAuth endpoint +type OAuthRequest struct { + Provider string `uri:"provider" binding:"required"` +} + +// UnauthorizedQuery is the query parameters for the unauthorized endpoint +type UnauthorizedQuery struct { + Username string `url:"username"` + Resource string `url:"resource"` +} + +// TailscaleQuery is the query parameters for the tailscale endpoint +type TailscaleQuery struct { + Code int `url:"code"` +} + +// Proxy is the uri parameters for the proxy endpoint +type Proxy struct { + Proxy string `uri:"proxy" binding:"required"` +} + +// User Context response is the response for the user context endpoint +type UserContextResponse struct { + Status int `json:"status"` + Message string `json:"message"` + IsLoggedIn bool `json:"isLoggedIn"` + Username string `json:"username"` + Provider string `json:"provider"` + Oauth bool `json:"oauth"` + TotpPending bool `json:"totpPending"` +} + +// Totp request is the request for the totp endpoint +type TotpRequest struct { + Code string `json:"code"` +} diff --git a/internal/types/config.go b/internal/types/config.go new file mode 100644 index 0000000..999a2f8 --- /dev/null +++ b/internal/types/config.go @@ -0,0 +1,79 @@ +package types + +// Config is the configuration for the tinyauth server +type Config struct { + Port int `mapstructure:"port" validate:"required"` + Address string `validate:"required,ip4_addr" mapstructure:"address"` + Secret string `validate:"required,len=32" mapstructure:"secret"` + SecretFile string `mapstructure:"secret-file"` + AppURL string `validate:"required,url" mapstructure:"app-url"` + Users string `mapstructure:"users"` + UsersFile string `mapstructure:"users-file"` + CookieSecure bool `mapstructure:"cookie-secure"` + GithubClientId string `mapstructure:"github-client-id"` + GithubClientSecret string `mapstructure:"github-client-secret"` + GithubClientSecretFile string `mapstructure:"github-client-secret-file"` + GoogleClientId string `mapstructure:"google-client-id"` + GoogleClientSecret string `mapstructure:"google-client-secret"` + GoogleClientSecretFile string `mapstructure:"google-client-secret-file"` + TailscaleClientId string `mapstructure:"tailscale-client-id"` + TailscaleClientSecret string `mapstructure:"tailscale-client-secret"` + TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"` + GenericClientId string `mapstructure:"generic-client-id"` + GenericClientSecret string `mapstructure:"generic-client-secret"` + GenericClientSecretFile string `mapstructure:"generic-client-secret-file"` + GenericScopes string `mapstructure:"generic-scopes"` + GenericAuthURL string `mapstructure:"generic-auth-url"` + GenericTokenURL string `mapstructure:"generic-token-url"` + GenericUserURL string `mapstructure:"generic-user-url"` + GenericName string `mapstructure:"generic-name"` + DisableContinue bool `mapstructure:"disable-continue"` + OAuthWhitelist string `mapstructure:"oauth-whitelist"` + SessionExpiry int `mapstructure:"session-expiry"` + LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` + Title string `mapstructure:"app-title"` + EnvFile string `mapstructure:"env-file"` +} + +// APIConfig is the configuration for the API +type APIConfig struct { + Port int + Address string +} + +// OAuthConfig is the configuration for the providers +type OAuthConfig struct { + GithubClientId string + GithubClientSecret string + GoogleClientId string + GoogleClientSecret string + TailscaleClientId string + TailscaleClientSecret string + GenericClientId string + GenericClientSecret string + GenericScopes []string + GenericAuthURL string + GenericTokenURL string + GenericUserURL string + AppURL string +} + +// Server configuration +type HandlersConfig struct { + AppURL string + Domain string + CookieSecure bool + DisableContinue bool + GenericName string + Title string +} + +// Auth configuration +type AuthConfig struct { + Domain string + Secret string + CookieSecure bool + SessionExpiry int + Users Users + OAuthWhitelist []string +} diff --git a/internal/types/types.go b/internal/types/types.go index 8e55866..1df71e0 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2,17 +2,6 @@ package types import "tinyauth/internal/oauth" -// LoginQuery is the query parameters for the login endpoint -type LoginQuery struct { - RedirectURI string `url:"redirect_uri"` -} - -// LoginRequest is the request body for the login endpoint -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - // User is the struct for a user type User struct { Username string @@ -23,82 +12,6 @@ type User struct { // Users is a list of users type Users []User -// Config is the configuration for the tinyauth server -type Config struct { - Port int `mapstructure:"port" validate:"required"` - Address string `validate:"required,ip4_addr" mapstructure:"address"` - Secret string `validate:"required,len=32" mapstructure:"secret"` - SecretFile string `mapstructure:"secret-file"` - AppURL string `validate:"required,url" mapstructure:"app-url"` - Users string `mapstructure:"users"` - UsersFile string `mapstructure:"users-file"` - CookieSecure bool `mapstructure:"cookie-secure"` - GithubClientId string `mapstructure:"github-client-id"` - GithubClientSecret string `mapstructure:"github-client-secret"` - GithubClientSecretFile string `mapstructure:"github-client-secret-file"` - GoogleClientId string `mapstructure:"google-client-id"` - GoogleClientSecret string `mapstructure:"google-client-secret"` - GoogleClientSecretFile string `mapstructure:"google-client-secret-file"` - TailscaleClientId string `mapstructure:"tailscale-client-id"` - TailscaleClientSecret string `mapstructure:"tailscale-client-secret"` - TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"` - GenericClientId string `mapstructure:"generic-client-id"` - GenericClientSecret string `mapstructure:"generic-client-secret"` - GenericClientSecretFile string `mapstructure:"generic-client-secret-file"` - GenericScopes string `mapstructure:"generic-scopes"` - GenericAuthURL string `mapstructure:"generic-auth-url"` - GenericTokenURL string `mapstructure:"generic-token-url"` - GenericUserURL string `mapstructure:"generic-user-url"` - GenericName string `mapstructure:"generic-name"` - DisableContinue bool `mapstructure:"disable-continue"` - OAuthWhitelist string `mapstructure:"oauth-whitelist"` - SessionExpiry int `mapstructure:"session-expiry"` - LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` - Title string `mapstructure:"app-title"` - EnvFile string `mapstructure:"env-file"` -} - -// UserContext is the context for the user -type UserContext struct { - Username string - IsLoggedIn bool - OAuth bool - Provider string - TotpPending bool -} - -// APIConfig is the configuration for the API -type APIConfig struct { - Port int - Address string - Secret string - CookieSecure bool - SessionExpiry int - Domain string -} - -// OAuthConfig is the configuration for the providers -type OAuthConfig struct { - GithubClientId string - GithubClientSecret string - GoogleClientId string - GoogleClientSecret string - TailscaleClientId string - TailscaleClientSecret string - GenericClientId string - GenericClientSecret string - GenericScopes []string - GenericAuthURL string - GenericTokenURL string - GenericUserURL string - AppURL string -} - -// OAuthRequest is the request for the OAuth endpoint -type OAuthRequest struct { - Provider string `uri:"provider" binding:"required"` -} - // OAuthProviders is the struct for the OAuth providers type OAuthProviders struct { Github *oauth.OAuth @@ -106,12 +19,6 @@ type OAuthProviders struct { Microsoft *oauth.OAuth } -// UnauthorizedQuery is the query parameters for the unauthorized endpoint -type UnauthorizedQuery struct { - Username string `url:"username"` - Resource string `url:"resource"` -} - // SessionCookie is the cookie for the session (exculding the expiry) type SessionCookie struct { Username string @@ -127,25 +34,13 @@ type TinyauthLabels struct { Headers map[string]string } -// TailscaleQuery is the query parameters for the tailscale endpoint -type TailscaleQuery struct { - Code int `url:"code"` -} - -// Proxy is the uri parameters for the proxy endpoint -type Proxy struct { - Proxy string `uri:"proxy" binding:"required"` -} - -// User Context response is the response for the user context endpoint -type UserContextResponse struct { - Status int `json:"status"` - Message string `json:"message"` - IsLoggedIn bool `json:"isLoggedIn"` - Username string `json:"username"` - Provider string `json:"provider"` - Oauth bool `json:"oauth"` - TotpPending bool `json:"totpPending"` +// UserContext is the context for the user +type UserContext struct { + Username string + IsLoggedIn bool + OAuth bool + Provider string + TotpPending bool } // App Context is the response for the app context endpoint @@ -157,18 +52,3 @@ type AppContext struct { Title string `json:"title"` GenericName string `json:"genericName"` } - -// Totp request is the request for the totp endpoint -type TotpRequest struct { - Code string `json:"code"` -} - -// Server configuration -type HandlersConfig struct { - AppURL string - Domain string - CookieSecure bool - DisableContinue bool - GenericName string - Title string -}