250 lines
7.4 KiB
Go
250 lines
7.4 KiB
Go
package google
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"ersteller-lib"
|
|
"ersteller-lib/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_lib.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_lib.LogError("failed getting user data from Google: %v", err)
|
|
return GoogleUserData{}, err
|
|
}
|
|
ersteller_lib.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_lib.LogError("code exchange wrong: %v", err)
|
|
return GoogleUserData{}, err
|
|
}
|
|
|
|
response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
|
|
if err != nil {
|
|
ersteller_lib.LogError("failed getting user info: %v", err)
|
|
return GoogleUserData{}, nil
|
|
}
|
|
defer response.Body.Close()
|
|
contents, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
ersteller_lib.LogError("failed read response: %v", err)
|
|
return GoogleUserData{}, nil
|
|
}
|
|
userData := GoogleUserData{}
|
|
err = json.Unmarshal(contents, &userData)
|
|
if err != nil {
|
|
ersteller_lib.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_lib.Debug("saving google credentials for user ", userId)
|
|
googleAuth, err := a.repo.ReadByUserId(userId)
|
|
if err != nil {
|
|
ersteller_lib.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_lib.LogError("failed to get credentials for user %d: %v", userId, err)
|
|
return nil, err
|
|
}
|
|
if credentials.Credentials.Token.AccessToken == "" {
|
|
ersteller_lib.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_lib.Route {
|
|
googleLoginRoute := ersteller_lib.NewGetRoute(GoogleLogin, func(c echo.Context) error {
|
|
state := a.generateStateOauthCookie(c.Response())
|
|
ersteller_lib.LogDebug("Value: %v", state)
|
|
authenticationUrl := a.GetAuthURL(state)
|
|
return c.Redirect(http.StatusTemporaryRedirect, authenticationUrl)
|
|
})
|
|
googleLoginRoute.Add(e)
|
|
a.GoogleLoginRoute = googleLoginRoute
|
|
|
|
authenticatedRoute := ersteller_lib.NewGetRoute(GoogleLoginCallback, func(c echo.Context) error {
|
|
ersteller_lib.LogDebug("email authenticated called")
|
|
oauthstate, err := c.Cookie(oAuthStateCookieName)
|
|
if err != nil {
|
|
ersteller_lib.LogError("Failed to get cookie: %v", err)
|
|
return err
|
|
}
|
|
if c.FormValue("state") != oauthstate.Value {
|
|
ersteller_lib.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_lib.LogError("Failed to get user id: %v", err)
|
|
userId, err = a.userRepo.Create(data.Email)
|
|
if err != nil {
|
|
ersteller_lib.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_lib.Error("failed to save credentials for user ", userId, ": ", err)
|
|
return err
|
|
}
|
|
ersteller_lib.Debug("saved credentials for user", userId)
|
|
return c.Redirect(http.StatusTemporaryRedirect, "/")
|
|
})
|
|
authenticatedRoute.Add(e)
|
|
|
|
logoutRoute := ersteller_lib.NewGetRoute("/logout", func(c echo.Context) error {
|
|
// Clear the session
|
|
session, err := a.sessionStore.Get(c.Request(), a.environment.SessionName)
|
|
if err != nil {
|
|
ersteller_lib.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_lib.LogError("Failed to save session: %v", err)
|
|
return err
|
|
}
|
|
return c.Redirect(http.StatusTemporaryRedirect, "/")
|
|
})
|
|
logoutRoute.Add(e)
|
|
return []ersteller_lib.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_lib.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_lib.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_lib.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
|
|
}
|