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

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

View File

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

View File

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

View File

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

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 { 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 = () => {
</Button>
</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>
<Divider
label="Or continue with email"