mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-30 21:55:43 +00:00 
			
		
		
		
	feat: implement backend logic for multiple oauth providers
This commit is contained in:
		
							
								
								
									
										20
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -27,11 +27,6 @@ var rootCmd = &cobra.Command{ | |||||||
| 			log.Fatal().Err(err).Msg("Failed to parse config") | 			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 | 		// Validate config | ||||||
| 		v := validator.New() | 		v := validator.New() | ||||||
|  |  | ||||||
| @@ -80,21 +75,6 @@ func init() { | |||||||
| 		{"users", "", "Comma separated list of users in the format username:hash."}, | 		{"users", "", "Comma separated list of users in the format username:hash."}, | ||||||
| 		{"users-file", "", "Path to a file containing 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."}, | 		{"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-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)"}, | 		{"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."}, | 		{"session-expiry", 86400, "Session (cookie) expiration time in seconds."}, | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package bootstrap | |||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"tinyauth/internal/config" | 	"tinyauth/internal/config" | ||||||
| 	"tinyauth/internal/controller" | 	"tinyauth/internal/controller" | ||||||
| @@ -45,6 +46,13 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Get OAuth configs | ||||||
|  | 	oauthProviders, err := utils.GetOAuthProvidersConfig(os.Environ(), os.Args, app.Config.AppURL) | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Get cookie domain | 	// Get cookie domain | ||||||
| 	cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) | 	cookieDomain, err := utils.GetCookieDomain(app.Config.AppURL) | ||||||
|  |  | ||||||
| @@ -112,7 +120,7 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	// Create services | 	// Create services | ||||||
| 	dockerService := service.NewDockerService() | 	dockerService := service.NewDockerService() | ||||||
| 	authService := service.NewAuthService(authConfig, dockerService, ldapService, database) | 	authService := service.NewAuthService(authConfig, dockerService, ldapService, database) | ||||||
| 	oauthBrokerService := service.NewOAuthBrokerService(app.getOAuthBrokerConfig()) | 	oauthBrokerService := service.NewOAuthBrokerService(oauthProviders) | ||||||
|  |  | ||||||
| 	// Initialize services | 	// Initialize services | ||||||
| 	services := []Service{ | 	services := []Service{ | ||||||
| @@ -132,13 +140,39 @@ func (app *BootstrapApp) Setup() error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Configured providers | 	// Configured providers | ||||||
| 	var configuredProviders []string | 	babysit := map[string]string{ | ||||||
|  | 		"google": "Google", | ||||||
|  | 		"github": "GitHub", | ||||||
|  | 	} | ||||||
|  | 	configuredProviders := make([]controller.Provider, 0) | ||||||
|  |  | ||||||
| 	if authService.UserAuthConfigured() || ldapService != nil { | 	for id, provider := range oauthProviders { | ||||||
| 		configuredProviders = append(configuredProviders, "username") | 		if id == "" { | ||||||
|  | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	configuredProviders = append(configuredProviders, oauthBrokerService.GetConfiguredServices()...) | 		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, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	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 { | 	if len(configuredProviders) == 0 { | ||||||
| 		return fmt.Errorf("no authentication providers configured") | 		return fmt.Errorf("no authentication providers configured") | ||||||
| @@ -179,9 +213,8 @@ func (app *BootstrapApp) Setup() error { | |||||||
|  |  | ||||||
| 	// Create controllers | 	// Create controllers | ||||||
| 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | 	contextController := controller.NewContextController(controller.ContextControllerConfig{ | ||||||
| 		ConfiguredProviders:   configuredProviders, | 		Providers:             configuredProviders, | ||||||
| 		Title:                 app.Config.Title, | 		Title:                 app.Config.Title, | ||||||
| 		GenericName:           app.Config.GenericName, |  | ||||||
| 		AppURL:                app.Config.AppURL, | 		AppURL:                app.Config.AppURL, | ||||||
| 		CookieDomain:          cookieDomain, | 		CookieDomain:          cookieDomain, | ||||||
| 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | 		ForgotPasswordMessage: app.Config.ForgotPasswordMessage, | ||||||
| @@ -235,30 +268,3 @@ func (app *BootstrapApp) Setup() error { | |||||||
|  |  | ||||||
| 	return nil | 	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, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -21,21 +21,6 @@ type Config struct { | |||||||
| 	Users                 string `mapstructure:"users"` | 	Users                 string `mapstructure:"users"` | ||||||
| 	UsersFile             string `mapstructure:"users-file"` | 	UsersFile             string `mapstructure:"users-file"` | ||||||
| 	SecureCookie          bool   `mapstructure:"secure-cookie"` | 	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"` | 	OAuthWhitelist        string `mapstructure:"oauth-whitelist"` | ||||||
| 	OAuthAutoRedirect     string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` | 	OAuthAutoRedirect     string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` | ||||||
| 	SessionExpiry         int    `mapstructure:"session-expiry"` | 	SessionExpiry         int    `mapstructure:"session-expiry"` | ||||||
|   | |||||||
| @@ -24,9 +24,8 @@ type UserContextResponse struct { | |||||||
| type AppContextResponse struct { | type AppContextResponse struct { | ||||||
| 	Status                int        `json:"status"` | 	Status                int        `json:"status"` | ||||||
| 	Message               string     `json:"message"` | 	Message               string     `json:"message"` | ||||||
| 	ConfiguredProviders   []string `json:"configuredProviders"` | 	Providers             []Provider `json:"providers"` | ||||||
| 	Title                 string     `json:"title"` | 	Title                 string     `json:"title"` | ||||||
| 	GenericName           string   `json:"genericName"` |  | ||||||
| 	AppURL                string     `json:"appUrl"` | 	AppURL                string     `json:"appUrl"` | ||||||
| 	CookieDomain          string     `json:"cookieDomain"` | 	CookieDomain          string     `json:"cookieDomain"` | ||||||
| 	ForgotPasswordMessage string     `json:"forgotPasswordMessage"` | 	ForgotPasswordMessage string     `json:"forgotPasswordMessage"` | ||||||
| @@ -34,10 +33,15 @@ type AppContextResponse struct { | |||||||
| 	OAuthAutoRedirect     string     `json:"oauthAutoRedirect"` | 	OAuthAutoRedirect     string     `json:"oauthAutoRedirect"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Provider struct { | ||||||
|  | 	Name  string `json:"name"` | ||||||
|  | 	ID    string `json:"id"` | ||||||
|  | 	OAuth bool   `json:"oauth"` | ||||||
|  | } | ||||||
|  |  | ||||||
| type ContextControllerConfig struct { | type ContextControllerConfig struct { | ||||||
| 	ConfiguredProviders   []string | 	Providers             []Provider | ||||||
| 	Title                 string | 	Title                 string | ||||||
| 	GenericName           string |  | ||||||
| 	AppURL                string | 	AppURL                string | ||||||
| 	CookieDomain          string | 	CookieDomain          string | ||||||
| 	ForgotPasswordMessage string | 	ForgotPasswordMessage string | ||||||
| @@ -96,9 +100,8 @@ func (controller *ContextController) appContextHandler(c *gin.Context) { | |||||||
| 	c.JSON(200, AppContextResponse{ | 	c.JSON(200, AppContextResponse{ | ||||||
| 		Status:                200, | 		Status:                200, | ||||||
| 		Message:               "Success", | 		Message:               "Success", | ||||||
| 		ConfiguredProviders:   controller.config.ConfiguredProviders, | 		Providers:             controller.config.Providers, | ||||||
| 		Title:                 controller.config.Title, | 		Title:                 controller.config.Title, | ||||||
| 		GenericName:           controller.config.GenericName, |  | ||||||
| 		AppURL:                fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), | 		AppURL:                fmt.Sprintf("%s://%s", appUrl.Scheme, appUrl.Host), | ||||||
| 		CookieDomain:          controller.config.CookieDomain, | 		CookieDomain:          controller.config.CookieDomain, | ||||||
| 		ForgotPasswordMessage: controller.config.ForgotPasswordMessage, | 		ForgotPasswordMessage: controller.config.ForgotPasswordMessage, | ||||||
|   | |||||||
| @@ -12,9 +12,19 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var controllerCfg = controller.ContextControllerConfig{ | 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", | 	Title:                 "Test App", | ||||||
| 	GenericName:           "Generic", |  | ||||||
| 	AppURL:                "http://localhost:8080", | 	AppURL:                "http://localhost:8080", | ||||||
| 	CookieDomain:          "localhost", | 	CookieDomain:          "localhost", | ||||||
| 	ForgotPasswordMessage: "Contact admin to reset your password.", | 	ForgotPasswordMessage: "Contact admin to reset your password.", | ||||||
| @@ -58,9 +68,8 @@ func TestAppContextHandler(t *testing.T) { | |||||||
| 	expectedRes := controller.AppContextResponse{ | 	expectedRes := controller.AppContextResponse{ | ||||||
| 		Status:                200, | 		Status:                200, | ||||||
| 		Message:               "Success", | 		Message:               "Success", | ||||||
| 		ConfiguredProviders:   controllerCfg.ConfiguredProviders, | 		Providers:             controllerCfg.Providers, | ||||||
| 		Title:                 controllerCfg.Title, | 		Title:                 controllerCfg.Title, | ||||||
| 		GenericName:           controllerCfg.GenericName, |  | ||||||
| 		AppURL:                controllerCfg.AppURL, | 		AppURL:                controllerCfg.AppURL, | ||||||
| 		CookieDomain:          controllerCfg.CookieDomain, | 		CookieDomain:          controllerCfg.CookieDomain, | ||||||
| 		ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, | 		ForgotPasswordMessage: controllerCfg.ForgotPasswordMessage, | ||||||
|   | |||||||
| @@ -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) | 	providers := make(map[string]config.OAuthServiceConfig) | ||||||
|  |  | ||||||
| 	// Get from environment variables | 	// Get from environment variables | ||||||
| @@ -181,6 +181,18 @@ func GetOAuthProvidersConfig(env []string, args []string) (map[string]config.OAu | |||||||
| 		providers[name] = provider | 		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 combined providers | ||||||
| 	return providers, nil | 	return providers, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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.NilError(t, err) | ||||||
| 	assert.DeepEqual(t, expected, result) | 	assert.DeepEqual(t, expected, result) | ||||||
|  |  | ||||||
| @@ -226,7 +226,7 @@ func TestGetOAuthProvidersConfig(t *testing.T) { | |||||||
| 	args = []string{"/tinyauth/tinyauth"} | 	args = []string{"/tinyauth/tinyauth"} | ||||||
| 	expected = map[string]config.OAuthServiceConfig{} | 	expected = map[string]config.OAuthServiceConfig{} | ||||||
|  |  | ||||||
| 	result, err = utils.GetOAuthProvidersConfig(env, args) | 	result, err = utils.GetOAuthProvidersConfig(env, args, "") | ||||||
| 	assert.NilError(t, err) | 	assert.NilError(t, err) | ||||||
| 	assert.DeepEqual(t, expected, result) | 	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.NilError(t, err) | ||||||
| 	assert.DeepEqual(t, expected, result) | 	assert.DeepEqual(t, expected, result) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros