From f30ba9bee3260cc314a52eb1aa9a1b28a797f87e Mon Sep 17 00:00:00 2001 From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com> Date: Sat, 24 Aug 2024 22:27:01 +1200 Subject: [PATCH] feat(auth): add google & gitlab auth providers --- README.md | 30 ++++++++++++++-------------- auth/auth.go | 17 ++++++++++++++++ config/config.go | 46 ++++++++++++------------------------------- config/config_test.go | 21 -------------------- data/data.go | 31 +++++++++++++++++++++++++---- data/libsql.go | 4 +++- go.mod | 2 ++ go.sum | 4 ++++ 8 files changed, 81 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index f9258eb..5fabc44 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,14 @@ Some reasons you might want to self-host mnemstart: Luckily, self-hosting mnemstart is easy. The whole application (including the database) is contained in a single Docker container. You can run it on any machine that has Docker installed. +> [!IMPORTANT] +> To self-host mnemstart, you will need to register a new OAuth application with at least one of the supported providers: +> +> - [GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) +> - [Discord](https://discord.com/developers/docs/topics/oauth2) +> - [Google](https://developers.google.com/identity/protocols/oauth2) +> - [GitLab](https://docs.gitlab.com/ee/integration/oauth_provider.html) + ### 🐋 Using Docker Compose 1. Register a new OAuth application with GitHub and/or Discord. You will need to provide a callback URL in the format `https://your-domain.com[:PORT]/auth/[provider]/callback` where `[provider]` is either `github` or `discord`. Make a note of the client ID and client secret. @@ -48,11 +56,15 @@ Luckily, self-hosting mnemstart is easy. The whole application (including the da 3. In the empty directory, create an `.env` file with the following contents: ```env -# Required - replace with your own values +# Required - at least one OAuth provider is required GITHUB_CLIENT_ID=your-github-client-id GITHUB_CLIENT_SECRET=your-github-client-secret DISCORD_CLIENT_ID=your-discord-client-id DISCORD_CLIENT_SECRET=your-discord-client-secret +GOOGLE_CLIENT_ID=your-google-client-id +GOOGLE_CLIENT_SECRET=your-google-client-secret +GITLAB_CLIENT_ID=your-gitlab-client-id +GITLAB_CLIENT_SECRET=your-gitlab-client-secret # Optional PUBLIC_HOST=https://your-domain.com # Defaults to http://localhost @@ -90,19 +102,7 @@ services: ### 🐋 Using Docker (Recommended) 1. Clone the repository and navigate to the root directory. -2. Create an `.env` file with the following contents: - -```env -GITHUB_CLIENT_ID=your-github-client-id -GITHUB_CLIENT_SECRET=your-github-client-secret -DISCORD_CLIENT_ID=your-discord-client-id -DISCORD_CLIENT_SECRET=your-discord-client-secret -``` - -> [!NOTE] -> Only one valid OAuth provider is required to run the application. You can leave the other provider's client ID and secret blank if you wish. -> Documentation for registering a new OAuth application with GitHub can be found [here](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app) and Discord [here](https://discord.com/developers/docs/topics/oauth2). - +2. Create an `.env` file and populate at least one OAuth provider. See the example above in **Self-Hosting**. 3. Run `docker-compose up` to start the development server. The application will be available at `http://localhost:3000`. ### 🚀 Without Docker @@ -116,7 +116,7 @@ DISCORD_CLIENT_SECRET=your-discord-client-secret **Steps:** 1. Clone the repository and navigate to the root directory. -2. Create an `.env` file - see above for contents. +2. Create an `.env` file with an OAuth provider. See the example above in **Self-Hosting**. 3. Run `templ generate` to ensure all `_templ.go` files are up to date. 4. Run `air` to start the development server. The application will be available at `http://localhost:3000`. diff --git a/auth/auth.go b/auth/auth.go index 793da91..76e3710 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,6 +1,7 @@ package auth import ( + "encoding/gob" "fmt" "log" "net/http" @@ -10,12 +11,18 @@ import ( "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/discord" "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gitlab" + "github.com/markbates/goth/providers/google" "github.com/scottmckendry/mnemstart/config" ) type AuthService struct{} +func init() { + gob.Register(map[string]interface{}{}) +} + func NewAuthService(store sessions.Store) *AuthService { gothic.Store = store @@ -30,6 +37,16 @@ func NewAuthService(store sessions.Store) *AuthService { config.Envs.DiscordClientSecret, buildCallbackURL("discord"), "identify", "email", ), + google.New( + config.Envs.GoogleClientID, + config.Envs.GoogleClientSecret, + buildCallbackURL("google"), "email", "profile", + ), + gitlab.New( + config.Envs.GitlabClientID, + config.Envs.GitlabClientSecret, + buildCallbackURL("gitlab"), "read_user", "email", + ), ) return &AuthService{} diff --git a/config/config.go b/config/config.go index 751b815..56f3c24 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,10 @@ type Config struct { GithubClientSecret string DiscordClientID string DiscordClientSecret string + GoogleClientID string + GoogleClientSecret string + GitlabClientID string + GitlabClientSecret string } var Envs = initConfig() @@ -33,13 +37,7 @@ var Envs = initConfig() func initConfig() *Config { err := godotenv.Load() if err != nil { - log.Print("Error loading .env file") - log.Println("Creating empty .env file, with default values for required variables") - createEmptyEnvFile() - err = godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } + log.Print("No .env file found. Using default environment variables.") } return &Config{ @@ -51,10 +49,14 @@ func initConfig() *Config { CookiesAuthAgeInSeconds: getEnvAsInt("COOKIES_AUTH_AGE_IN_SECONDS", thirtyDaysInSeconds), CookiesAuthIsSecure: getEnvAsBool("COOKIES_AUTH_IS_SECURE", false), CookiesAuthIsHttpOnly: getEnvAsBool("COOKIES_AUTH_IS_HTTP_ONLY", true), - GithubClientID: getEnvOrPanic("GITHUB_CLIENT_ID"), - GithubClientSecret: getEnvOrPanic("GITHUB_CLIENT_SECRET"), - DiscordClientID: getEnvOrPanic("DISCORD_CLIENT_ID"), - DiscordClientSecret: getEnvOrPanic("DISCORD_CLIENT_SECRET"), + GithubClientID: getEnv("GITHUB_CLIENT_ID", ""), + GithubClientSecret: getEnv("GITHUB_CLIENT_SECRET", ""), + DiscordClientID: getEnv("DISCORD_CLIENT_ID", ""), + DiscordClientSecret: getEnv("DISCORD_CLIENT_SECRET", ""), + GoogleClientID: getEnv("GOOGLE_CLIENT_ID", ""), + GoogleClientSecret: getEnv("GOOGLE_CLIENT_SECRET", ""), + GitlabClientID: getEnv("GITLAB_CLIENT_ID", ""), + GitlabClientSecret: getEnv("GITLAB_CLIENT_SECRET", ""), } } @@ -65,13 +67,6 @@ func getEnv(key, fallback string) string { return fallback } -func getEnvOrPanic(key string) string { - if value, ok := os.LookupEnv(key); ok { - return value - } - panic("Missing required environment variable: " + key) -} - func getEnvAsInt(key string, fallback int) int { if value, ok := os.LookupEnv(key); ok { if intValue, err := strconv.Atoi(value); err == nil { @@ -89,18 +84,3 @@ func getEnvAsBool(key string, fallback bool) bool { } return fallback } - -func createEmptyEnvFile() { - f, err := os.Create(".env") - if err != nil { - log.Fatal(err) - } - defer f.Close() - - _, err = f.WriteString( - "GITHUB_CLIENT_ID=\nGITHUB_CLIENT_SECRET=\nDISCORD_CLIENT_ID=\nDISCORD_CLIENT_SECRET=\n", - ) - if err != nil { - log.Fatal(err) - } -} diff --git a/config/config_test.go b/config/config_test.go index fddc1db..9b80ca6 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -23,27 +23,6 @@ func TestGetEnv(t *testing.T) { }) } -func TestGetEnvOrPanic(t *testing.T) { - os.Setenv("TEST_ENV", "test_value") - - result := getEnvOrPanic("TEST_ENV") - if result != "test_value" { - t.Errorf("Expected 'test_value', got '%s'", result) - } - - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected a panic for non-existent environment variable") - } - }() - - getEnvOrPanic("NON_EXISTENT_ENV") - - t.Cleanup(func() { - os.Unsetenv("TEST_ENV") - }) -} - func TestGetEnvAsInt(t *testing.T) { os.Setenv("TEST_ENV", "123") diff --git a/data/data.go b/data/data.go index ca880dc..f67de5e 100644 --- a/data/data.go +++ b/data/data.go @@ -18,6 +18,8 @@ type User struct { Email string DiscordID string GithubID string + GoogleID string + GitlabID string } type Mapping struct { @@ -62,12 +64,27 @@ func appendProviderID(provider string, userId string, user *User) { user.DiscordID = userId case "github": user.GithubID = userId + case "google": + user.GoogleID = userId + case "gitlab": + user.GitlabID = userId } } func getUser(db *sql.DB, user *User) error { - row := db.QueryRow("SELECT * FROM users WHERE email = ?", user.Email) - err := row.Scan(&user.ID, &user.Name, &user.Email, &user.DiscordID, &user.GithubID) + row := db.QueryRow( + "SELECT id, name, email, discord_id, github_id, google_id, gitlab_id FROM users WHERE email = ?", + user.Email, + ) + err := row.Scan( + &user.ID, + &user.Name, + &user.Email, + &user.DiscordID, + &user.GithubID, + &user.GoogleID, + &user.GitlabID, + ) if err != nil { return err } @@ -77,11 +94,13 @@ func getUser(db *sql.DB, user *User) error { func createUser(db *sql.DB, user *User) error { _, err := db.Exec( - "INSERT INTO users (name, email, discord_id, github_id) VALUES (?, ?, ?, ?)", + "INSERT INTO users (name, email, discord_id, github_id, google_id, gitlab_id) VALUES (?, ?, ?, ?, ?, ?)", user.Name, user.Email, user.DiscordID, user.GithubID, + user.GoogleID, + user.GitlabID, ) if err != nil { return err @@ -99,12 +118,16 @@ func updateUser(db *sql.DB, user *User) error { ELSE name END, discord_id = ?, - github_id = ? + github_id = ?, + google_id = ?, + gitlab_id = ? WHERE email = ? `, user.Name, user.DiscordID, user.GithubID, + user.GoogleID, + user.GitlabID, user.Email, ) if err != nil { diff --git a/data/libsql.go b/data/libsql.go index e274c02..c457c07 100644 --- a/data/libsql.go +++ b/data/libsql.go @@ -30,7 +30,9 @@ func generateSchema(db *sql.DB) error { name TEXT, email TEXT, discord_id TEXT, - github_id TEXT + github_id TEXT, + google_id TEXT, + gitlab_id TEXT ); `) if err != nil { diff --git a/go.mod b/go.mod index 4382ee2..4f2920c 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,8 @@ require ( ) require ( + cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/dustin/go-humanize v1.0.1 // indirect diff --git a/go.sum b/go.sum index b5fb5fd..5ad3607 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=