From fbf584359255edae8a0346d99137b5ffb17b29a6 Mon Sep 17 00:00:00 2001 From: Stavros Date: Fri, 12 Sep 2025 13:46:07 +0300 Subject: [PATCH] feat: implement backend logic for multiple oauth providers --- cmd/root.go | 20 ----- internal/bootstrap/app_bootstrap.go | 74 ++++++++++--------- internal/config/config.go | 63 ++++++---------- internal/controller/context_controller.go | 31 ++++---- .../controller/context_controller_test.go | 17 ++++- internal/utils/app_utils.go | 14 +++- internal/utils/app_utils_test.go | 21 +++++- 7 files changed, 125 insertions(+), 115 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 155ccd2..d72cd34 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,11 +27,6 @@ var rootCmd = &cobra.Command{ log.Fatal().Err(err).Msg("Failed to parse config") } - // Check if secrets have a file associated with them - conf.GithubClientSecret = utils.GetSecret(conf.GithubClientSecret, conf.GithubClientSecretFile) - conf.GoogleClientSecret = utils.GetSecret(conf.GoogleClientSecret, conf.GoogleClientSecretFile) - conf.GenericClientSecret = utils.GetSecret(conf.GenericClientSecret, conf.GenericClientSecretFile) - // Validate config v := validator.New() @@ -80,21 +75,6 @@ func init() { {"users", "", "Comma separated list of users in the format username:hash."}, {"users-file", "", "Path to a file containing users in the format username:hash."}, {"secure-cookie", false, "Send cookie over secure connection only."}, - {"github-client-id", "", "Github OAuth client ID."}, - {"github-client-secret", "", "Github OAuth client secret."}, - {"github-client-secret-file", "", "Github OAuth client secret file."}, - {"google-client-id", "", "Google OAuth client ID."}, - {"google-client-secret", "", "Google OAuth client secret."}, - {"google-client-secret-file", "", "Google OAuth client secret file."}, - {"generic-client-id", "", "Generic OAuth client ID."}, - {"generic-client-secret", "", "Generic OAuth client secret."}, - {"generic-client-secret-file", "", "Generic OAuth client secret file."}, - {"generic-scopes", "", "Generic OAuth scopes."}, - {"generic-auth-url", "", "Generic OAuth auth URL."}, - {"generic-token-url", "", "Generic OAuth token URL."}, - {"generic-user-url", "", "Generic OAuth user info URL."}, - {"generic-name", "Generic", "Generic OAuth provider name."}, - {"generic-skip-ssl", false, "Skip SSL verification for the generic OAuth provider."}, {"oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth."}, {"oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)"}, {"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index db2e564..684e4bd 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -3,6 +3,7 @@ package bootstrap import ( "fmt" "net/url" + "os" "strings" "tinyauth/internal/config" "tinyauth/internal/controller" @@ -45,6 +46,13 @@ func (app *BootstrapApp) Setup() error { return err } + // Get OAuth configs + oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL) + + if err != nil { + return err + } + // Get cookie domain cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) @@ -112,7 +120,7 @@ func (app *BootstrapApp) Setup() error { // Create services dockerService := service.NewDockerService() authService := service.NewAuthService(authConfig, dockerService, ldapService, database) - oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig()) + oauthBrokerService := service.NewOAuthBrokerService(oauthProviders) // Initialize services services := []Service{ @@ -132,13 +140,39 @@ func (app *BootstrapApp) Setup() error { } // Configured providers - var configuredProviders []string + babysit := map[string]string{ + "google": "Google", + "github": "GitHub", + } + configuredProviders := make([]controller.Provider, 0) - if authService.UserAuthConfigured() || ldapService != nil { - configuredProviders = append(configuredProviders, "username") + for id, provider := range oauthProviders { + if id == "" { + continue + } + + if provider.Name == "" && babysit[id] != "" { + provider.Name = babysit[id] + } else { + provider.Name = utils.Capitalize(id) + } + + configuredProviders = append(configuredProviders, controller.Provider{ + Name: provider.Name, + ID: id, + OAuth: true, + }) } - configuredProviders = append(configuredProviders, oauthBrokerService.GetConfiguredServices()...) + if authService.UserAuthConfigured() || ldapService != nil { + configuredProviders = append(configuredProviders, controller.Provider{ + Name: "Username", + ID: "username", + OAuth: false, + }) + } + + log.Debug().Interface("providers", configuredProviders).Msg("Authentication providers") if len(configuredProviders) == 0 { return fmt.Errorf("no authentication providers configured") @@ -179,9 +213,8 @@ func (app *BootstrapApp) Setup() error { // Create controllers contextController := controller.NewContextController(controller.ContextControllerConfig{ - ConfiguredProviders: configuredProviders, + Providers: configuredProviders, Title: app.Config.Title, - GenericName: app.Config.GenericName, AppURL: app.Config.AppURL, CookieDomain: cookieDomain, ForgotPasswordMessage: app.Config.ForgotPasswordMessage, @@ -235,30 +268,3 @@ func (app *BootstrapApp) Setup() error { return nil } - -// Temporary -func (app *BootstrapApp) getOAuthBrokerConfig() map[string]config.OAuthServiceConfig { - return map[string]config.OAuthServiceConfig{ - "google": { - ClientID: app.Config.GoogleClientId, - ClientSecret: app.Config.GoogleClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/google", app.Config.AppURL), - }, - "github": { - ClientID: app.Config.GithubClientId, - ClientSecret: app.Config.GithubClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/github", app.Config.AppURL), - }, - "generic": { - ClientID: app.Config.GenericClientId, - ClientSecret: app.Config.GenericClientSecret, - RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", app.Config.AppURL), - Scopes: strings.Split(app.Config.GenericScopes, ","), - AuthURL: app.Config.GenericAuthURL, - TokenURL: app.Config.GenericTokenURL, - UserinfoURL: app.Config.GenericUserURL, - InsecureSkipVerify: app.Config.GenericSkipSSL, - }, - } - -} diff --git a/internal/config/config.go b/internal/config/config.go index 880d663..cdb02ae 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,45 +15,30 @@ var RedirectCookieName = "tinyauth-redirect" // Main app config type Config struct { - Port int `mapstructure:"port" validate:"required"` - Address string `validate:"required,ip4_addr" mapstructure:"address"` - AppURL string `validate:"required,url" mapstructure:"app-url"` - Users string `mapstructure:"users"` - UsersFile string `mapstructure:"users-file"` - SecureCookie bool `mapstructure:"secure-cookie"` - 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"` - 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"` - GenericSkipSSL bool `mapstructure:"generic-skip-ssl"` - OAuthWhitelist string `mapstructure:"oauth-whitelist"` - OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` - SessionExpiry int `mapstructure:"session-expiry"` - LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"` - Title string `mapstructure:"app-title"` - LoginTimeout int `mapstructure:"login-timeout"` - LoginMaxRetries int `mapstructure:"login-max-retries"` - ForgotPasswordMessage string `mapstructure:"forgot-password-message"` - BackgroundImage string `mapstructure:"background-image" validate:"required"` - LdapAddress string `mapstructure:"ldap-address"` - LdapBindDN string `mapstructure:"ldap-bind-dn"` - LdapBindPassword string `mapstructure:"ldap-bind-password"` - LdapBaseDN string `mapstructure:"ldap-base-dn"` - LdapInsecure bool `mapstructure:"ldap-insecure"` - LdapSearchFilter string `mapstructure:"ldap-search-filter"` - ResourcesDir string `mapstructure:"resources-dir"` - DatabasePath string `mapstructure:"database-path" validate:"required"` - TrustedProxies string `mapstructure:"trusted-proxies"` + Port int `mapstructure:"port" validate:"required"` + Address string `validate:"required,ip4_addr" mapstructure:"address"` + AppURL string `validate:"required,url" mapstructure:"app-url"` + Users string `mapstructure:"users"` + UsersFile string `mapstructure:"users-file"` + SecureCookie bool `mapstructure:"secure-cookie"` + OAuthWhitelist string `mapstructure:"oauth-whitelist"` + OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` + SessionExpiry int `mapstructure:"session-expiry"` + LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"` + Title string `mapstructure:"app-title"` + LoginTimeout int `mapstructure:"login-timeout"` + LoginMaxRetries int `mapstructure:"login-max-retries"` + ForgotPasswordMessage string `mapstructure:"forgot-password-message"` + BackgroundImage string `mapstructure:"background-image" validate:"required"` + LdapAddress string `mapstructure:"ldap-address"` + LdapBindDN string `mapstructure:"ldap-bind-dn"` + LdapBindPassword string `mapstructure:"ldap-bind-password"` + LdapBaseDN string `mapstructure:"ldap-base-dn"` + LdapInsecure bool `mapstructure:"ldap-insecure"` + LdapSearchFilter string `mapstructure:"ldap-search-filter"` + ResourcesDir string `mapstructure:"resources-dir"` + DatabasePath string `mapstructure:"database-path" validate:"required"` + TrustedProxies string `mapstructure:"trusted-proxies"` } // OAuth/OIDC config diff --git a/internal/controller/context_controller.go b/internal/controller/context_controller.go index ee3eec6..148bc1c 100644 --- a/internal/controller/context_controller.go +++ b/internal/controller/context_controller.go @@ -22,22 +22,26 @@ type UserContextResponse struct { } type AppContextResponse struct { - Status int `json:"status"` - Message string `json:"message"` - ConfiguredProviders []string `json:"configuredProviders"` - Title string `json:"title"` - GenericName string `json:"genericName"` - AppURL string `json:"appUrl"` - CookieDomain string `json:"cookieDomain"` - ForgotPasswordMessage string `json:"forgotPasswordMessage"` - BackgroundImage string `json:"backgroundImage"` - OAuthAutoRedirect string `json:"oauthAutoRedirect"` + Status int `json:"status"` + Message string `json:"message"` + Providers []Provider `json:"providers"` + Title string `json:"title"` + AppURL string `json:"appUrl"` + CookieDomain string `json:"cookieDomain"` + ForgotPasswordMessage string `json:"forgotPasswordMessage"` + BackgroundImage string `json:"backgroundImage"` + OAuthAutoRedirect string `json:"oauthAutoRedirect"` +} + +type Provider struct { + Name string `json:"name"` + ID string `json:"id"` + OAuth bool `json:"oauth"` } type ContextControllerConfig struct { - ConfiguredProviders []string + Providers []Provider Title string - GenericName string AppURL string CookieDomain string ForgotPasswordMessage string @@ -96,9 +100,8 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { c.JSON(200, AppContextResponse{ Status: 200, Message: "Success", - ConfiguredProviders: controller.config.ConfiguredProviders, + Providers: controller.config.Providers, Title: controller.config.Title, - GenericName: controller.config.GenericName, AppURL: fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), CookieDomain: controller.config.CookieDomain, ForgotPasswordMessage: controller.config.ForgotPasswordMessage, diff --git a/internal/controller/context_controller_test.go b/internal/controller/context_controller_test.go index 44f77a1..85be0b5 100644 --- a/internal/controller/context_controller_test.go +++ b/internal/controller/context_controller_test.go @@ -12,9 +12,19 @@ import ( ) var controllerCfg = controller.ContextControllerConfig{ - ConfiguredProviders: []string{"github", "google", "generic"}, + Providers: []controller.Provider{ + { + Name: "Username", + ID: "username", + OAuth: false, + }, + { + Name: "Google", + ID: "google", + OAuth: true, + }, + }, Title: "Test App", - GenericName: "Generic", AppURL: "http://localhost:8080", CookieDomain: "localhost", ForgotPasswordMessage: "Contact admin to reset your password.", @@ -58,9 +68,8 @@ func TestAppContextHandler(t *testing.T) { expectedRes := controller.AppContextResponse{ Status: 200, Message: "Success", - ConfiguredProviders: controllerCfg.ConfiguredProviders, + Providers: controllerCfg.Providers, Title: controllerCfg.Title, - GenericName: controllerCfg.GenericName, AppURL: controllerCfg.AppURL, CookieDomain: controllerCfg.CookieDomain, ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, diff --git a/internal/utils/app_utils.go b/internal/utils/app_utils.go index ed06746..bf2a176 100644 --- a/internal/utils/app_utils.go +++ b/internal/utils/app_utils.go @@ -134,7 +134,7 @@ func GetLogLevel(level string) zerolog.Level { } } -func GetOAuthProvidersConfig(env []string, args []string) (map[string]config.OAuthServiceConfig, error) { +func GetOAuthProvidersConfig(env []string, args []string, appUrl string) (map[string]config.OAuthServiceConfig, error) { providers := make(map[string]config.OAuthServiceConfig) // Get from environment variables @@ -181,6 +181,18 @@ func GetOAuthProvidersConfig(env []string, args []string) (map[string]config.OAu providers[name] = provider } + // If we have google/github providers and no redirect URL babysit them + babysitProviders := []string{"google", "github"} + + for _, name := range babysitProviders { + if provider, exists := providers[name]; exists { + if provider.RedirectURL == "" { + provider.RedirectURL = appUrl + "/api/oauth/callback/" + name + providers[name] = provider + } + } + } + // Return combined providers return providers, nil } diff --git a/internal/utils/app_utils_test.go b/internal/utils/app_utils_test.go index 48bb915..a7f09fe 100644 --- a/internal/utils/app_utils_test.go +++ b/internal/utils/app_utils_test.go @@ -217,7 +217,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { }, } - result, err := utils.GetOAuthProvidersConfig(env, args) + result, err := utils.GetOAuthProvidersConfig(env, args, "") assert.NilError(t, err) assert.DeepEqual(t, expected, result) @@ -226,7 +226,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { args = []string{"/tinyauth/tinyauth"} expected = map[string]config.OAuthServiceConfig{} - result, err = utils.GetOAuthProvidersConfig(env, args) + result, err = utils.GetOAuthProvidersConfig(env, args, "") assert.NilError(t, err) assert.DeepEqual(t, expected, result) @@ -250,7 +250,22 @@ func TestGetOAuthProvidersConfig(t *testing.T) { }, } - result, err = utils.GetOAuthProvidersConfig(env, args) + result, err = utils.GetOAuthProvidersConfig(env, args, "") + assert.NilError(t, err) + assert.DeepEqual(t, expected, result) + + // Case with google provider and no redirect URL + env = []string{"PROVIDERS_GOOGLE_CLIENT_ID=google-id", "PROVIDERS_GOOGLE_CLIENT_SECRET=google-secret"} + args = []string{"/tinyauth/tinyauth"} + expected = map[string]config.OAuthServiceConfig{ + "google": { + ClientID: "google-id", + ClientSecret: "google-secret", + RedirectURL: "http://app.url/api/oauth/callback/google", + }, + } + + result, err = utils.GetOAuthProvidersConfig(env, args, "http://app.url") assert.NilError(t, err) assert.DeepEqual(t, expected, result) }