package google_http import ( "context" "crypto/rand" "encoding/base64" "encoding/json" . "git.gorlug.de/code/ersteller" "git.gorlug.de/code/ersteller/authentication" "io/ioutil" "net/http" "time" "github.com/gorilla/sessions" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) const oAuthStateCookieName = "oauthstate" const GoogleLogin = "/login/google" const GoogleLoginCallback = "/google/authenticated" const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" type GoogleUserData struct { Email string `json:"email"` Token *oauth2.Token `json:"token"` } type GoogleAuth struct { db Database server *http.ServeMux environment Environment config oauth2.Config sessionStore *sessions.CookieStore } type Database interface { GetUserIdByEmail(ctx context.Context, email string) (int, error) CreateUser(ctx context.Context, email string) (int, error) } type Environment struct { ClientId string ClientSecret string BaseUrl string IsLocal bool } func NewGoogleAuth(db Database, server *http.ServeMux, environment Environment, sessionStore *sessions.CookieStore) *GoogleAuth { config := oauth2.Config{ ClientID: environment.ClientId, ClientSecret: environment.ClientSecret, Endpoint: google.Endpoint, RedirectURL: environment.BaseUrl + GoogleLoginCallback, Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"}, } return &GoogleAuth{ db: db, server: server, environment: environment, config: config, sessionStore: sessionStore, } } func (g *GoogleAuth) AddRoutes() { g.server.HandleFunc("GET "+GoogleLogin, func(writer http.ResponseWriter, request *http.Request) { state := g.generateStateOauthCookie(writer) url := g.config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) http.Redirect(writer, request, url, http.StatusTemporaryRedirect) }) // OAuth callback handler g.server.HandleFunc("GET "+GoogleLoginCallback, func(writer http.ResponseWriter, request *http.Request) { LogDebug("email authenticated called") // Get OAuth state cookie oauthstateCookie, err := request.Cookie(oAuthStateCookieName) if err != nil { LogError("Failed to get cookie: %v", err) http.Error(writer, "Failed to get OAuth state cookie", http.StatusBadRequest) return } // Verify OAuth state if request.FormValue("state") != oauthstateCookie.Value { LogError("Failed to verify google oauth state") http.Error(writer, "Invalid OAuth state", http.StatusBadRequest) return } // Get authorization code code := request.FormValue("code") data, err := g.ParseUserData(code) if err != nil { LogError("Failed to parse user data: %v", err) http.Error(writer, "Failed to parse user data", http.StatusInternalServerError) return } // Get or create user userId, err := g.db.GetUserIdByEmail(request.Context(), data.Email) if err != nil { LogError("Failed to get user id: %v", err) userId, err = g.db.CreateUser(request.Context(), data.Email) if err != nil { LogError("Failed to create user: %v", err) http.Error(writer, "Failed to create user", http.StatusInternalServerError) return } } // Save email to session err = authentication.SaveEmailAndUserIdToSessionStore(request, writer, g.sessionStore, data.Email, userId) if err != nil { LogError("Failed to save session: %v", err) http.Error(writer, "Failed to save session", http.StatusInternalServerError) return } // Save credentials err = g.SaveCredentials(userId, data.Token) if err != nil { Error("failed to save credentials for user ", userId, ": ", err) http.Error(writer, "Failed to save credentials", http.StatusInternalServerError) return } Debug("saved credentials for user", userId) http.Redirect(writer, request, "/", http.StatusTemporaryRedirect) }) // Logout handler g.server.HandleFunc("GET /logout", func(writer http.ResponseWriter, request *http.Request) { authentication.LogoutSession(writer, request, g.sessionStore, "/") }) } func (g *GoogleAuth) ParseUserData(code string) (GoogleUserData, error) { data, err := g.getUserDataFromGoogle(code) if err != nil { LogError("failed getting user data from Google: %v", err) return GoogleUserData{}, err } LogDebug("user data: %v", data) return data, nil } func (g *GoogleAuth) getUserDataFromGoogle(code string) (GoogleUserData, error) { // Use code to get token and get user info from Google. token, err := g.config.Exchange(context.Background(), code) if err != nil { LogError("code exchange wrong: %v", err) return GoogleUserData{}, err } response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken) if err != nil { LogError("failed getting user info: %v", err) return GoogleUserData{}, err } defer response.Body.Close() contents, err := ioutil.ReadAll(response.Body) if err != nil { LogError("failed read response: %v", err) return GoogleUserData{}, err } userData := GoogleUserData{} err = json.Unmarshal(contents, &userData) if err != nil { LogError("failed to unmarshal user data: %v", err) return GoogleUserData{}, err } userData.Token = token return userData, nil } func (g *GoogleAuth) SaveCredentials(userId int, token *oauth2.Token) error { Debug("saving google credentials for user ", userId) // For now, we'll just log this - in a real implementation you'd save to database Debug("saved credentials for user", userId) return nil } func (g *GoogleAuth) generateStateOauthCookie(w http.ResponseWriter) string { b := make([]byte, 16) _, err := rand.Read(b) if err != nil { LogError("Failed to read random state: %v", err) } state := base64.URLEncoding.EncodeToString(b) var expiration = time.Now().Add(time.Hour) cookie := http.Cookie{Name: oAuthStateCookieName, Value: state, Expires: expiration, HttpOnly: true, Path: "/", Secure: false} if g.environment.IsLocal { cookie.SameSite = http.SameSiteLaxMode cookie.Secure = false } http.SetCookie(w, &cookie) return state }