package google import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "errors" "git.gorlug.de/code/golang/ersteller-lib/ersteller" "git.gorlug.de/code/golang/ersteller-lib/ersteller/authentication" "github.com/gorilla/sessions" "github.com/labstack/echo/v4" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "io/ioutil" "net/http" "time" ) const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" type AuthEnv struct { GoogleClientId string GoogleClientSecret string GoogleRedirectUrl string IsLocal bool EmailSessionKey string UserIdSessionKey string SessionName string } type Auth struct { Config oauth2.Config repo *GoogleAuthRepository isLocal bool userRepo authentication.UserRepository sessionStore *sessions.CookieStore GoogleLoginRoute ersteller.Route environment AuthEnv } func NewAuth(env AuthEnv, repo *GoogleAuthRepository, userRepo authentication.UserRepository, sessionStore *sessions.CookieStore) *Auth { config := oauth2.Config{ ClientID: env.GoogleClientId, ClientSecret: env.GoogleClientSecret, Endpoint: google.Endpoint, RedirectURL: env.GoogleRedirectUrl, Scopes: []string{"https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/userinfo.email"}, } return &Auth{ repo: repo, Config: config, isLocal: env.IsLocal, userRepo: userRepo, sessionStore: sessionStore, environment: env, } } func (a *Auth) GetAuthURL(state string) string { return a.Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce) } func (a *Auth) ParseUserData(code string) (GoogleUserData, error) { data, err := a.getUserDataFromGoogle(code) if err != nil { ersteller.LogError("failed getting user data from Google: %v", err) return GoogleUserData{}, err } ersteller.LogDebug("user data: %v", data) return data, nil } func (a *Auth) getUserDataFromGoogle(code string) (GoogleUserData, error) { // Use code to get token and get user info from Google. token, err := a.Config.Exchange(context.Background(), code) if err != nil { ersteller.LogError("code exchange wrong: %v", err) return GoogleUserData{}, err } response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken) if err != nil { ersteller.LogError("failed getting user info: %v", err) return GoogleUserData{}, nil } defer response.Body.Close() contents, err := ioutil.ReadAll(response.Body) if err != nil { ersteller.LogError("failed read response: %v", err) return GoogleUserData{}, nil } userData := GoogleUserData{} err = json.Unmarshal(contents, &userData) if err != nil { ersteller.LogError("failed to unmarshal user data: %v", err) return GoogleUserData{}, nil } userData.Token = token return userData, nil } func (a *Auth) SaveCredentials(userId int, token *oauth2.Token) error { ersteller.Debug("saving google credentials for user ", userId) googleAuth, err := a.repo.ReadByUserId(userId) if err != nil { ersteller.LogDebug("no GoogleAuth found for user %d, creating new one", userId) _, err = a.repo.Create(GoogleAuth{ UserId: userId, Credentials: Credentials{ Token: *token, }, }) return err } googleAuth.Credentials.Token = *token return a.repo.Update(userId, googleAuth) } func (a *Auth) GetCredentials(userId int) (*oauth2.Token, error) { credentials, err := a.repo.ReadByUserId(userId) if err != nil { ersteller.LogError("failed to get credentials for user %d: %v", userId, err) return nil, err } if credentials.Credentials.Token.AccessToken == "" { ersteller.LogError("no credentials found for user %d", userId) return nil, errors.New("no credentials found") } return &credentials.Credentials.Token, nil } const oAuthStateCookieName = "oauthstate" const GoogleLogin = "/login/google" const GoogleLoginCallback = "/email/authenticated" func (a *Auth) AddRoutes(e *echo.Echo) []ersteller.Route { googleLoginRoute := ersteller.NewGetRoute(GoogleLogin, func(c echo.Context) error { state := a.generateStateOauthCookie(c.Response()) ersteller.LogDebug("Value: %v", state) authenticationUrl := a.GetAuthURL(state) return c.Redirect(http.StatusTemporaryRedirect, authenticationUrl) }) googleLoginRoute.Add(e) a.GoogleLoginRoute = googleLoginRoute authenticatedRoute := ersteller.NewGetRoute(GoogleLoginCallback, func(c echo.Context) error { ersteller.LogDebug("email authenticated called") oauthstate, err := c.Cookie(oAuthStateCookieName) if err != nil { ersteller.LogError("Failed to get cookie: %v", err) return err } if c.FormValue("state") != oauthstate.Value { ersteller.LogError("Failed to verify google oauth state") return err } code := c.FormValue("code") data, err := a.ParseUserData(code) if err != nil { return err } userId, err := a.userRepo.GetUserId(data.Email) if err != nil { ersteller.LogError("Failed to get user id: %v", err) userId, err = a.userRepo.CreateFromEmail(data.Email) if err != nil { ersteller.LogError("Failed to create user: %v", err) return err } } err = a.saveEmailToSessionStore(c, data.Email, userId) if err != nil { return err } err = a.SaveCredentials(userId, data.Token) if err != nil { ersteller.Error("failed to save credentials for user ", userId, ": ", err) return err } ersteller.Debug("saved credentials for user", userId) return c.Redirect(http.StatusTemporaryRedirect, "/") }) authenticatedRoute.Add(e) logoutRoute := ersteller.NewGetRoute("/logout", func(c echo.Context) error { // Clear the session session, err := a.sessionStore.Get(c.Request(), a.environment.SessionName) if err != nil { ersteller.LogError("Failed to get session: %v", err) return c.Redirect(http.StatusTemporaryRedirect, "/") } session.Options.MaxAge = -1 err = session.Save(c.Request(), c.Response()) if err != nil { ersteller.LogError("Failed to save session: %v", err) return err } return c.Redirect(http.StatusTemporaryRedirect, "/") }) logoutRoute.Add(e) return []ersteller.Route{googleLoginRoute, authenticatedRoute, logoutRoute} } func (a *Auth) saveEmailToSessionStore(c echo.Context, email string, userId int) error { session, err := a.sessionStore.New(c.Request(), a.environment.SessionName) if err != nil { ersteller.LogError("Failed to create session: %v", err) return err } session.Values = map[interface{}]interface{}{ a.environment.EmailSessionKey: email, a.environment.UserIdSessionKey: userId, } err = session.Save(c.Request(), c.Response()) if err != nil { ersteller.LogError("Failed to save session: %v", err) return err } return nil } func (a *Auth) generateStateOauthCookie(w http.ResponseWriter) string { b := make([]byte, 16) _, err := rand.Read(b) if err != nil { ersteller.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 a.isLocal { cookie.SameSite = http.SameSiteLaxMode cookie.Secure = false } http.SetCookie(w, &cookie) return state } type GoogleUserData struct { Id string `json:"id"` Email string `json:"email"` VerifiedEmail bool `json:"verified_email"` Picture string `json:"picture"` Hd string `json:"hd"` Token *oauth2.Token }