feat: implement backend logic for multiple oauth providers

This commit is contained in:
Stavros
2025-09-12 13:46:07 +03:00
parent 5fcc50d5fd
commit fbf5843592
7 changed files with 125 additions and 115 deletions

View File

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

View File

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

View File

@@ -15,45 +15,30 @@ var RedirectCookieName = "tinyauth-redirect"
// Main app config // Main app config
type Config struct { type Config struct {
Port int `mapstructure:"port" validate:"required"` Port int `mapstructure:"port" validate:"required"`
Address string `validate:"required,ip4_addr" mapstructure:"address"` Address string `validate:"required,ip4_addr" mapstructure:"address"`
AppURL string `validate:"required,url" mapstructure:"app-url"` AppURL string `validate:"required,url" mapstructure:"app-url"`
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"` OAuthWhitelist string `mapstructure:"oauth-whitelist"`
GithubClientSecret string `mapstructure:"github-client-secret"` OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"`
GithubClientSecretFile string `mapstructure:"github-client-secret-file"` SessionExpiry int `mapstructure:"session-expiry"`
GoogleClientId string `mapstructure:"google-client-id"` LogLevel string `mapstructure:"log-level" validate:"oneof=trace debug info warn error fatal panic"`
GoogleClientSecret string `mapstructure:"google-client-secret"` Title string `mapstructure:"app-title"`
GoogleClientSecretFile string `mapstructure:"google-client-secret-file"` LoginTimeout int `mapstructure:"login-timeout"`
GenericClientId string `mapstructure:"generic-client-id"` LoginMaxRetries int `mapstructure:"login-max-retries"`
GenericClientSecret string `mapstructure:"generic-client-secret"` ForgotPasswordMessage string `mapstructure:"forgot-password-message"`
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"` BackgroundImage string `mapstructure:"background-image" validate:"required"`
GenericScopes string `mapstructure:"generic-scopes"` LdapAddress string `mapstructure:"ldap-address"`
GenericAuthURL string `mapstructure:"generic-auth-url"` LdapBindDN string `mapstructure:"ldap-bind-dn"`
GenericTokenURL string `mapstructure:"generic-token-url"` LdapBindPassword string `mapstructure:"ldap-bind-password"`
GenericUserURL string `mapstructure:"generic-user-url"` LdapBaseDN string `mapstructure:"ldap-base-dn"`
GenericName string `mapstructure:"generic-name"` LdapInsecure bool `mapstructure:"ldap-insecure"`
GenericSkipSSL bool `mapstructure:"generic-skip-ssl"` LdapSearchFilter string `mapstructure:"ldap-search-filter"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"` ResourcesDir string `mapstructure:"resources-dir"`
OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` DatabasePath string `mapstructure:"database-path" validate:"required"`
SessionExpiry int `mapstructure:"session-expiry"` TrustedProxies string `mapstructure:"trusted-proxies"`
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 // OAuth/OIDC config

View File

@@ -22,22 +22,26 @@ 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"` BackgroundImage string `json:"backgroundImage"`
BackgroundImage string `json:"backgroundImage"` 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,

View File

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

View File

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

View File

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