feat: generic oauth

This commit is contained in:
Stavros
2025-01-24 17:13:51 +02:00
parent f487e25ac5
commit 90f4c3c980
8 changed files with 165 additions and 33 deletions

View File

@@ -61,11 +61,17 @@ var rootCmd = &cobra.Command{
// Create OAuth config // Create OAuth config
oauthConfig := types.OAuthConfig{ oauthConfig := types.OAuthConfig{
GithubClientId: config.GithubClientId, GithubClientId: config.GithubClientId,
GithubClientSecret: config.GithubClientSecret, GithubClientSecret: config.GithubClientSecret,
GoogleClientId: config.GoogleClientId, GoogleClientId: config.GoogleClientId,
GoogleClientSecret: config.GoogleClientSecret, GoogleClientSecret: config.GoogleClientSecret,
AppURL: config.AppURL, GenericClientId: config.GenericClientId,
GenericClientSecret: config.GenericClientSecret,
GenericScopes: config.GenericScopes,
GenericAuthURL: config.GenericAuthURL,
GenericTokenURL: config.GenericTokenURL,
GenericUserInfoURL: config.GenericUserInfoURL,
AppURL: config.AppURL,
} }
// Create auth service // Create auth service
@@ -127,6 +133,12 @@ func init() {
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.") 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-id", "", "Google OAuth client ID.")
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.") 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("port", "PORT")
viper.BindEnv("address", "ADDRESS") viper.BindEnv("address", "ADDRESS")
viper.BindEnv("secret", "SECRET") viper.BindEnv("secret", "SECRET")
@@ -138,5 +150,11 @@ func init() {
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET") viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID") viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") 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()) viper.BindPFlags(rootCmd.Flags())
} }

View File

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

View File

@@ -7,12 +7,12 @@ import (
"net/http" "net/http"
) )
type GithubEmailsResponse []struct { type GithubUserInfoResponse []struct {
Email string `json:"email"` Email string `json:"email"`
Primary bool `json:"primary"` Primary bool `json:"primary"`
} }
func GithubScopes() ([]string) { func GithubScopes() []string {
return []string{"user:email"} return []string{"user:email"}
} }
@@ -29,7 +29,7 @@ func GetGithubEmail(client *http.Client) (string, error) {
return "", bodyErr return "", bodyErr
} }
var emails GithubEmailsResponse var emails GithubUserInfoResponse
jsonErr := json.Unmarshal(body, &emails) jsonErr := json.Unmarshal(body, &emails)
@@ -44,4 +44,4 @@ func GetGithubEmail(client *http.Client) (string, error) {
} }
return "", errors.New("no primary email found") return "", errors.New("no primary email found")
} }

View File

@@ -6,7 +6,7 @@ import (
"net/http" "net/http"
) )
type GoogleUserinfoResponse struct { type GoogleUserInfoResponse struct {
Email string `json:"email"` Email string `json:"email"`
} }
@@ -27,7 +27,7 @@ func GetGoogleEmail(client *http.Client) (string, error) {
return "", bodyErr return "", bodyErr
} }
var user GoogleUserinfoResponse var user GoogleUserInfoResponse
jsonErr := json.Unmarshal(body, &user) jsonErr := json.Unmarshal(body, &user)

View File

@@ -17,10 +17,10 @@ func NewProviders(config types.OAuthConfig) *Providers {
} }
type Providers struct { type Providers struct {
Config types.OAuthConfig Config types.OAuthConfig
Github *oauth.OAuth Github *oauth.OAuth
Google *oauth.OAuth Google *oauth.OAuth
Microsoft *oauth.OAuth Generic *oauth.OAuth
} }
func (providers *Providers) Init() { func (providers *Providers) Init() {
@@ -46,6 +46,20 @@ func (providers *Providers) Init() {
}) })
providers.Google.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 { func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
@@ -54,6 +68,8 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth {
return providers.Github return providers.Github
case "google": case "google":
return providers.Google return providers.Google
case "generic":
return providers.Generic
default: default:
return nil return nil
} }
@@ -81,6 +97,16 @@ func (providers *Providers) GetUser(provider string) (string, error) {
return "", emailErr return "", emailErr
} }
return email, nil 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: default:
return "", nil return "", nil
} }
@@ -94,5 +120,8 @@ func (provider *Providers) GetConfiguredProviders() []string {
if provider.Google != nil { if provider.Google != nil {
providers = append(providers, "google") providers = append(providers, "google")
} }
if provider.Generic != nil {
providers = append(providers, "generic")
}
return providers return providers
} }

View File

@@ -19,17 +19,23 @@ type User struct {
type Users []User type Users []User
type Config struct { type Config struct {
Port int `validate:"number" mapstructure:"port"` Port int `validate:"number" mapstructure:"port"`
Address string `mapstructure:"address, ip4_addr"` Address string `mapstructure:"address, ip4_addr"`
Secret string `validate:"required,len=32" mapstructure:"secret"` Secret string `validate:"required,len=32" mapstructure:"secret"`
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"`
CookieSecure bool `mapstructure:"cookie-secure"` CookieSecure bool `mapstructure:"cookie-secure"`
GithubClientId string `mapstructure:"github-client-id"` GithubClientId string `mapstructure:"github-client-id"`
GithubClientSecret string `mapstructure:"github-client-secret"` GithubClientSecret string `mapstructure:"github-client-secret"`
GoogleClientId string `mapstructure:"google-client-id"` GoogleClientId string `mapstructure:"google-client-id"`
GoogleClientSecret string `mapstructure:"google-client-secret"` 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 { type UserContext struct {
@@ -48,11 +54,17 @@ type APIConfig struct {
} }
type OAuthConfig struct { type OAuthConfig struct {
GithubClientId string GithubClientId string
GithubClientSecret string GithubClientSecret string
GoogleClientId string GoogleClientId string
GoogleClientSecret string GoogleClientSecret string
AppURL string GenericClientId string
GenericClientSecret string
GenericScopes string
GenericAuthURL string
GenericTokenURL string
GenericUserInfoURL string
AppURL string
} }
type OAuthRequest struct { type OAuthRequest struct {

24
site/src/icons/oauth.tsx Normal file
View File

@@ -0,0 +1,24 @@
import type { SVGProps } from "react";
export function OAuthIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
{...props}
>
<g
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
>
<path d="M2 12a10 10 0 1 0 20 0a10 10 0 1 0-20 0"></path>
<path d="M12.556 6c.65 0 1.235.373 1.508.947l2.839 7.848a1.646 1.646 0 0 1-1.01 2.108a1.673 1.673 0 0 1-2.068-.851L13.365 15h-2.73l-.398.905A1.67 1.67 0 0 1 8.26 16.95l-.153-.047a1.647 1.647 0 0 1-1.056-1.956l2.824-7.852a1.66 1.66 0 0 1 1.409-1.087z"></path>
</g>
</svg>
);
}

View File

@@ -18,6 +18,7 @@ import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
import { GoogleIcon } from "../icons/google"; import { GoogleIcon } from "../icons/google";
import { GithubIcon } from "../icons/github"; import { GithubIcon } from "../icons/github";
import { OAuthIcon } from "../icons/oauth";
export const LoginPage = () => { export const LoginPage = () => {
const queryString = window.location.search; const queryString = window.location.search;
@@ -125,6 +126,19 @@ export const LoginPage = () => {
</Button> </Button>
</Grid.Col> </Grid.Col>
)} )}
{configuredProviders.includes("generic") && (
<Grid.Col span="content">
<Button
radius="xl"
leftSection={<OAuthIcon style={{ width: 14, height: 14 }} />}
variant="default"
onClick={() => loginOAuthMutation.mutate("generic")}
loading={loginOAuthMutation.isLoading}
>
Generic
</Button>
</Grid.Col>
)}
</Grid> </Grid>
<Divider <Divider
label="Or continue with email" label="Or continue with email"