Yimaru-BackEnd/internal/services/authentication/google.go

225 lines
5.3 KiB
Go

package authentication
import (
"Yimaru-Backend/internal/domain"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"
"cloud.google.com/go/auth/credentials/idtoken"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
var googleOAuthConfig *oauth2.Config
var ErrGoogleOAuthNotInitialized = errors.New("google oauth not initialized")
func (s *Service) InitGoogleOAuth(clientID, clientSecret, redirectURL string) {
if clientID == "" || clientSecret == "" || redirectURL == "" {
return
}
googleOAuthConfig = &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
},
Endpoint: google.Endpoint,
}
}
func (s *Service) GenerateGoogleLoginURL(state string) string {
if googleOAuthConfig == nil {
return ""
}
return googleOAuthConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
}
func (s *Service) ExchangeGoogleCode(
ctx context.Context,
code string,
) (*oauth2.Token, error) {
if googleOAuthConfig == nil {
return nil, ErrGoogleOAuthNotInitialized
}
if code == "" {
return nil, errors.New("missing google auth code")
}
return googleOAuthConfig.Exchange(ctx, code)
}
func (s *Service) FetchGoogleUser(
ctx context.Context,
token *oauth2.Token,
) (*domain.GoogleUser, error) {
if googleOAuthConfig == nil {
return nil, ErrGoogleOAuthNotInitialized
}
if token == nil {
return nil, errors.New("missing google oauth token")
}
client := googleOAuthConfig.Client(ctx, token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
return nil, fmt.Errorf("failed to fetch google user: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("google userinfo failed: status=%d body=%s", resp.StatusCode, string(body))
}
var user domain.GoogleUser
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("failed to decode google user: %w", err)
}
if user.Email == "" {
return nil, errors.New("google account has no email")
}
return &user, nil
}
func (s *Service) LoginWithGoogleAndroid(
ctx context.Context,
idToken string,
clientID string,
) (domain.LoginSuccess, error) {
if idToken == "" {
return domain.LoginSuccess{}, errors.New("missing id token")
}
if clientID == "" {
return domain.LoginSuccess{}, errors.New("missing google client id")
}
payload, err := idtoken.Validate(ctx, idToken, clientID)
if err != nil {
return domain.LoginSuccess{}, errors.New("invalid google id token")
}
email, _ := payload.Claims["email"].(string)
if email == "" {
return domain.LoginSuccess{}, errors.New("google id token missing email")
}
verified := false
switch v := payload.Claims["email_verified"].(type) {
case bool:
verified = v
case string:
verified = (v == "true")
}
givenName, _ := payload.Claims["given_name"].(string)
familyName, _ := payload.Claims["family_name"].(string)
picture, _ := payload.Claims["picture"].(string)
gUser := domain.GoogleUser{
ID: payload.Subject,
Email: email,
VerifiedEmail: verified,
GivenName: givenName,
FamilyName: familyName,
Picture: picture,
}
return s.LoginWithGoogle(ctx, gUser)
}
func (s *Service) LoginWithGoogle(
ctx context.Context,
gUser domain.GoogleUser,
) (domain.LoginSuccess, error) {
var user domain.User
var err error
user, err = s.userStore.GetUserByGoogleID(ctx, gUser.ID)
if err != nil {
if !errors.Is(err, domain.ErrUserNotFound) {
return domain.LoginSuccess{}, err
}
user, err = s.userStore.GetUserByEmailPhone(ctx, gUser.Email, "")
if err != nil {
if !errors.Is(err, domain.ErrUserNotFound) {
return domain.LoginSuccess{}, err
}
user, err = s.userStore.CreateGoogleUser(ctx, gUser)
if err != nil {
return domain.LoginSuccess{}, err
}
} else {
if err := s.userStore.LinkGoogleAccount(ctx, user.ID, gUser.ID); err != nil {
return domain.LoginSuccess{}, err
}
}
}
if user.Status == domain.UserStatusPending {
return domain.LoginSuccess{}, domain.ErrUserNotVerified
}
if user.Status == domain.UserStatusSuspended {
return domain.LoginSuccess{}, ErrUserSuspended
}
oldToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
if err != nil {
if !errors.Is(err, ErrRefreshTokenNotFound) {
return domain.LoginSuccess{}, err
}
} else if !oldToken.Revoked {
if err := s.tokenStore.RevokeRefreshToken(ctx, oldToken.Token); err != nil {
return domain.LoginSuccess{}, err
}
}
refreshToken, err := generateRefreshToken()
if err != nil {
return domain.LoginSuccess{}, err
}
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
Token: refreshToken,
UserID: user.ID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
}); err != nil {
return domain.LoginSuccess{}, err
}
return domain.LoginSuccess{
UserId: user.ID,
Role: user.Role,
RfToken: refreshToken,
}, nil
}
func (s *Service) verifyGoogleIDToken(
ctx context.Context,
idToken string,
clientID string,
) (*idtoken.Payload, error) {
payload, err := idtoken.Validate(ctx, idToken, clientID)
if err != nil {
return nil, err
}
return payload, nil
}