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

View File

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

View File

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

View File

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

View File

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

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

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