diff --git a/cmd/root.go b/cmd/root.go index d8c7362..3b3d61a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -61,11 +61,17 @@ var rootCmd = &cobra.Command{ // Create OAuth config oauthConfig := types.OAuthConfig{ - GithubClientId: config.GithubClientId, - GithubClientSecret: config.GithubClientSecret, - GoogleClientId: config.GoogleClientId, - GoogleClientSecret: config.GoogleClientSecret, - AppURL: config.AppURL, + GithubClientId: config.GithubClientId, + GithubClientSecret: config.GithubClientSecret, + GoogleClientId: config.GoogleClientId, + GoogleClientSecret: config.GoogleClientSecret, + GenericClientId: config.GenericClientId, + GenericClientSecret: config.GenericClientSecret, + GenericScopes: config.GenericScopes, + GenericAuthURL: config.GenericAuthURL, + GenericTokenURL: config.GenericTokenURL, + GenericUserInfoURL: config.GenericUserInfoURL, + AppURL: config.AppURL, } // Create auth service @@ -127,6 +133,12 @@ func init() { rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.") rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.") rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.") + rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.") + rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.") + rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.") + rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") + rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") + rootCmd.Flags().String("generic-user-info-url", "", "Generic OAuth user info URL.") viper.BindEnv("port", "PORT") viper.BindEnv("address", "ADDRESS") viper.BindEnv("secret", "SECRET") @@ -138,5 +150,11 @@ func init() { viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET") viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID") viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") + viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID") + viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET") + viper.BindEnv("generic-scopes", "GENERIC_SCOPES") + viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL") + viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL") + viper.BindEnv("generic-user-info-url", "GENERIC_USER_INFO_URL") viper.BindPFlags(rootCmd.Flags()) } diff --git a/internal/providers/generic.go b/internal/providers/generic.go new file mode 100644 index 0000000..22b538c --- /dev/null +++ b/internal/providers/generic.go @@ -0,0 +1,35 @@ +package providers + +import ( + "encoding/json" + "io" + "net/http" +) + +type GenericUserInfoResponse struct { + Email string `json:"email"` +} + +func GetGenericEmail(client *http.Client, url string) (string, error) { + res, resErr := client.Get(url) + + if resErr != nil { + return "", resErr + } + + body, bodyErr := io.ReadAll(res.Body) + + if bodyErr != nil { + return "", bodyErr + } + + var user GenericUserInfoResponse + + jsonErr := json.Unmarshal(body, &user) + + if jsonErr != nil { + return "", jsonErr + } + + return user.Email, nil +} diff --git a/internal/providers/github.go b/internal/providers/github.go index 00f988c..687be6b 100644 --- a/internal/providers/github.go +++ b/internal/providers/github.go @@ -7,12 +7,12 @@ import ( "net/http" ) -type GithubEmailsResponse []struct { - Email string `json:"email"` - Primary bool `json:"primary"` +type GithubUserInfoResponse []struct { + Email string `json:"email"` + Primary bool `json:"primary"` } -func GithubScopes() ([]string) { +func GithubScopes() []string { return []string{"user:email"} } @@ -29,7 +29,7 @@ func GetGithubEmail(client *http.Client) (string, error) { return "", bodyErr } - var emails GithubEmailsResponse + var emails GithubUserInfoResponse jsonErr := json.Unmarshal(body, &emails) @@ -44,4 +44,4 @@ func GetGithubEmail(client *http.Client) (string, error) { } return "", errors.New("no primary email found") -} \ No newline at end of file +} diff --git a/internal/providers/google.go b/internal/providers/google.go index 1d44e3d..49eaf6d 100644 --- a/internal/providers/google.go +++ b/internal/providers/google.go @@ -6,7 +6,7 @@ import ( "net/http" ) -type GoogleUserinfoResponse struct { +type GoogleUserInfoResponse struct { Email string `json:"email"` } @@ -27,7 +27,7 @@ func GetGoogleEmail(client *http.Client) (string, error) { return "", bodyErr } - var user GoogleUserinfoResponse + var user GoogleUserInfoResponse jsonErr := json.Unmarshal(body, &user) diff --git a/internal/providers/providers.go b/internal/providers/providers.go index 7a1ba81..dc6f354 100644 --- a/internal/providers/providers.go +++ b/internal/providers/providers.go @@ -17,10 +17,10 @@ func NewProviders(config types.OAuthConfig) *Providers { } type Providers struct { - Config types.OAuthConfig - Github *oauth.OAuth - Google *oauth.OAuth - Microsoft *oauth.OAuth + Config types.OAuthConfig + Github *oauth.OAuth + Google *oauth.OAuth + Generic *oauth.OAuth } func (providers *Providers) Init() { @@ -46,6 +46,20 @@ func (providers *Providers) Init() { }) providers.Google.Init() } + if providers.Config.GenericClientId != "" && providers.Config.GenericClientSecret != "" { + log.Info().Msg("Initializing Generic OAuth") + providers.Generic = oauth.NewOAuth(oauth2.Config{ + ClientID: providers.Config.GenericClientId, + ClientSecret: providers.Config.GenericClientSecret, + RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL), + Scopes: []string{providers.Config.GenericScopes}, + Endpoint: oauth2.Endpoint{ + AuthURL: providers.Config.GenericAuthURL, + TokenURL: providers.Config.GenericTokenURL, + }, + }) + providers.Generic.Init() + } } func (providers *Providers) GetProvider(provider string) *oauth.OAuth { @@ -54,6 +68,8 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth { return providers.Github case "google": return providers.Google + case "generic": + return providers.Generic default: return nil } @@ -81,6 +97,16 @@ func (providers *Providers) GetUser(provider string) (string, error) { return "", emailErr } return email, nil + case "generic": + if providers.Generic == nil { + return "", nil + } + client := providers.Generic.GetClient() + email, emailErr := GetGenericEmail(client, providers.Config.GenericUserInfoURL) + if emailErr != nil { + return "", emailErr + } + return email, nil default: return "", nil } @@ -94,5 +120,8 @@ func (provider *Providers) GetConfiguredProviders() []string { if provider.Google != nil { providers = append(providers, "google") } + if provider.Generic != nil { + providers = append(providers, "generic") + } return providers } diff --git a/internal/types/types.go b/internal/types/types.go index def1f46..f6a3adb 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -19,17 +19,23 @@ type User struct { type Users []User type Config struct { - Port int `validate:"number" mapstructure:"port"` - Address string `mapstructure:"address, ip4_addr"` - Secret string `validate:"required,len=32" mapstructure:"secret"` - 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"` - GoogleClientId string `mapstructure:"google-client-id"` - GoogleClientSecret string `mapstructure:"google-client-secret"` + Port int `validate:"number" mapstructure:"port"` + Address string `mapstructure:"address, ip4_addr"` + Secret string `validate:"required,len=32" mapstructure:"secret"` + 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"` + GoogleClientId string `mapstructure:"google-client-id"` + GoogleClientSecret string `mapstructure:"google-client-secret"` + GenericClientId string `mapstructure:"generic-client-id"` + GenericClientSecret string `mapstructure:"generic-client-secret"` + GenericScopes string `mapstructure:"generic-scopes"` + GenericAuthURL string `mapstructure:"generic-auth-url"` + GenericTokenURL string `mapstructure:"generic-token-url"` + GenericUserInfoURL string `mapstructure:"generic-user-info-url"` } type UserContext struct { @@ -48,11 +54,17 @@ type APIConfig struct { } type OAuthConfig struct { - GithubClientId string - GithubClientSecret string - GoogleClientId string - GoogleClientSecret string - AppURL string + GithubClientId string + GithubClientSecret string + GoogleClientId string + GoogleClientSecret string + GenericClientId string + GenericClientSecret string + GenericScopes string + GenericAuthURL string + GenericTokenURL string + GenericUserInfoURL string + AppURL string } type OAuthRequest struct { diff --git a/site/src/icons/oauth.tsx b/site/src/icons/oauth.tsx new file mode 100644 index 0000000..3ca531d --- /dev/null +++ b/site/src/icons/oauth.tsx @@ -0,0 +1,24 @@ +import type { SVGProps } from "react"; + +export function OAuthIcon(props: SVGProps) { + return ( + + + + + + + ); +} diff --git a/site/src/pages/login-page.tsx b/site/src/pages/login-page.tsx index 795b534..6000d4e 100644 --- a/site/src/pages/login-page.tsx +++ b/site/src/pages/login-page.tsx @@ -18,6 +18,7 @@ import { Navigate } from "react-router"; import { Layout } from "../components/layouts/layout"; import { GoogleIcon } from "../icons/google"; import { GithubIcon } from "../icons/github"; +import { OAuthIcon } from "../icons/oauth"; export const LoginPage = () => { const queryString = window.location.search; @@ -125,6 +126,19 @@ export const LoginPage = () => { )} + {configuredProviders.includes("generic") && ( + + + + )}