Social login goth-tal, önállóan
A social login oldal a --social-lal generált kódot mutatja be egy gpsystem projekten belül. Ez az oldal ugyanennek az ötletnek az önálló változata: egy sima net/http alkalmazás, gpsystem CLI és oapi-codegen strict-server réteg nélkül, ami a markbates/goth-tal beszél a Discorddal, Facebookkal és Google-lel, a gp-system/auth-tal pedig ugyanazt a JWT/refresh párt állítja elő, mint amit egy email+jelszó bejelentkezés adna.
goth itt a te projekted függősége, nem az auth-é: az auth saját go.mod-ja csak a golang-jwt/jwt/v5-öt igényli (lásd Az auth önálló használata). Ezen a példa semmit nem változtat; a goth és a gorilla/sessions a te go.mod-odban landol, nem az auth-éban.go mod init example.com/social-demo
go get github.com/gp-system/auth
go get github.com/markbates/goth
Miért egyszerűbb ez, mint a generált változat
A kit generált social csomagja saját state paramétert ír alá szerver oldali session helyett, mert az oapi-codegen strict-server handler szignatúrája (func(ctx, req) (resp, error)) sosem ad hozzáférést a nyers http.ResponseWriter-hez, így nincs hol cookie-t beállítani a begin és a callback kérés között. Egy kézzel épített net/http alkalmazásnak nincs ilyen megkötése: használhatja a goth saját gothic segédcsomagját változtatás nélkül, cookie-session-nel együtt, pontosan úgy, ahogy a goth saját dokumentációja is feltételezi.
Providerek regisztrálása
package main
import (
"os"
"github.com/markbates/goth"
"github.com/markbates/goth/providers/discord"
"github.com/markbates/goth/providers/facebook"
"github.com/markbates/goth/providers/google"
)
const baseURL = "http://localhost:8080"
func registerProviders() {
goth.UseProviders(
discord.New(os.Getenv("DISCORD_CLIENT_ID"), os.Getenv("DISCORD_CLIENT_SECRET"),
baseURL+"/auth/discord/callback", discord.ScopeIdentify, discord.ScopeEmail),
facebook.New(os.Getenv("FACEBOOK_CLIENT_ID"), os.Getenv("FACEBOOK_CLIENT_SECRET"),
baseURL+"/auth/facebook/callback", "email"),
google.New(os.Getenv("GOOGLE_CLIENT_ID"), os.Getenv("GOOGLE_CLIENT_SECRET"),
baseURL+"/auth/google/callback", "email", "profile"),
)
}
Minden provider saját alkalmazás-konzoljában (Discord Developer Portal, Meta for Developers, Google Cloud Console) be kell állítani a megfelelő callback URL-t, pontosan úgy, mint a generált változat provider-oldali beállítás táblázatában.
Routing gothic-kal
A gothic.BeginAuthHandler és a gothic.CompleteUserAuth közvetlenül a kérésből olvassa ki a provider nevét; a Go net/http.ServeMux route mintáival (Go 1.22+) a req.PathValue("provider") már az egyik hely, amit a gothic.GetProviderName ellenőriz, így egy sima stdlib mux is elég, router könyvtár nélkül:
package main
import (
"net/http"
"os"
"github.com/gorilla/sessions"
"github.com/markbates/goth/gothic"
)
func main() {
gothic.Store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
registerProviders()
mux := http.NewServeMux()
mux.HandleFunc("GET /auth/{provider}", gothic.BeginAuthHandler)
mux.HandleFunc("GET /auth/{provider}/callback", handleCallback)
http.ListenAndServe(":8080", mux)
}
A SESSION_SECRET írja alá azt a cookie-t, amiben a gothic az OAuth-session-t tárolja a begin és a callback kérés között; ugyanúgy kezeld, mint a JWT_SECRET-et: környezetenként más érték, sosem commitolva.
A callback: goth.User egy gp-system/auth tokenné
Ez az a rész, ami az auth-hoz ér. A gothic.CompleteUserAuth egy goth.User-t ad vissza (a Provider, UserID, Email, Name, AvatarURL mezőivel); a lenti findOrCreateUser az a hely, ahol a generált változat által dokumentált ugyanazt a keresési szabályt alkalmaznád (elsőként provider + provider-kulcs egyezés, azután email, elutasítás egy nem-verifikált helyi fiókhoz való hozzákötésnél), csak kézzel megírva, generálás helyett:
package main
import (
"encoding/json"
"net/http"
"time"
"github.com/gp-system/auth"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
)
var issuer = auth.NewTokenIssuer[auth.DefaultClaims](auth.Config{
Secret: "dev-secret-change-me", // valós kódban: env-ből/secret-kezelőből
AccessTTL: 15 * time.Minute,
Issuer: "social-demo",
})
func handleCallback(w http.ResponseWriter, r *http.Request) {
gothUser, err := gothic.CompleteUserAuth(w, r)
if err != nil {
http.Error(w, "social login failed", http.StatusUnauthorized)
return
}
user, err := findOrCreateUser(gothUser)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
claims := auth.NewDefaultClaims(auth.Config{Issuer: "social-demo", AccessTTL: 15 * time.Minute},
user.ID, user.Username, user.Roles, user.Permissions)
token, err := issuer.Sign(claims)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
refresh, err := auth.NewRefreshToken()
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
// itt tárold el a refresh hash-ét user.ID-hoz kötve, ugyanúgy, ahogy egy email+jelszó bejelentkezés is tenné
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"accessToken": token, "refreshToken": refresh})
}
// a findOrCreateUser a saját perzisztenciád, nem az auth vagy a goth része.
// Keress először (provider, gothUser.UserID) alapján (visszatérő felhasználó);
// aztán email alapján (kösd hozzá egy létező, verifikált fiókhoz, vagy utasítsd
// el, ha az az email egy nem-verifikálthoz tartozik); egyébként hozz létre egy
// új, előre verifikált felhasználót, mert a provider már kezeskedett az email
// címért.
func findOrCreateUser(gothUser goth.User) (localUser, error) {
panic("gyakorlatként meghagyva: a saját users táblád")
}
type localUser struct {
ID string
Username string
Roles []string
Permissions []string
}
Innentől ennek a szekciónak minden más oldala változtatás nélkül alkalmazható: a handleCallback által visszaadott tokent ugyanazokkal az auth.Identify/rbac.CheckRole hívásokkal ellenőrzöd, mint egy email+jelszó bejelentkezésből származó tokent, mert az egy ilyen token, csak egy goth.User keresés után írva alá, bcrypt-összehasonlítás helyett.
Amit a kit hozzáad
A generált social csomag (go tool gpsystem add auth --social discord,facebook,google) ugyanez az ötlet, három olyan dologgal megtoldva, amit egy valós projekt általában szeretne: aláírt, állapotmentes state paraméter cookie-session helyett (hogy a strict-server handler alakjából is működjön), az account-linking szabály egyszer megvalósítva a kit users/auth_credentials sémájára, panic("gyakorlatként meghagyva") helyett, valamint az Apple form_post callback-je és JWT client-secret kezelése. Lásd a Social login oldalt ehhez a változathoz.