Files
honeyDueAPI/internal/services/google_auth.go
Trey t 780e699463 Add Google OAuth authentication support
- Add Google OAuth token verification and user lookup/creation
- Add GoogleAuthRequest and GoogleAuthResponse DTOs
- Add GoogleLogin handler in auth_handler.go
- Add google_auth.go service for token verification
- Add FindByGoogleID repository method for user lookup
- Add GoogleID field to User model
- Add Google OAuth configuration (client ID, enabled flag)
- Add i18n translations for Google auth error messages
- Add Google verification email template support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 00:51:44 -06:00

128 lines
3.8 KiB
Go

package services
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/treytartt/casera-api/internal/config"
)
const (
googleTokenInfoURL = "https://oauth2.googleapis.com/tokeninfo"
)
var (
ErrInvalidGoogleToken = errors.New("invalid Google ID token")
ErrGoogleTokenExpired = errors.New("Google ID token has expired")
ErrInvalidGoogleAudience = errors.New("invalid Google token audience")
)
// GoogleTokenInfo represents the response from Google's token info endpoint
type GoogleTokenInfo struct {
Sub string `json:"sub"` // Unique Google user ID
Email string `json:"email"` // User's email
EmailVerified string `json:"email_verified"` // "true" or "false"
Name string `json:"name"` // Full name
GivenName string `json:"given_name"` // First name
FamilyName string `json:"family_name"` // Last name
Picture string `json:"picture"` // Profile picture URL
Aud string `json:"aud"` // Audience (client ID)
Azp string `json:"azp"` // Authorized party
Exp string `json:"exp"` // Expiration time
Iss string `json:"iss"` // Issuer
}
// IsEmailVerified returns whether the email is verified
func (t *GoogleTokenInfo) IsEmailVerified() bool {
return t.EmailVerified == "true"
}
// GoogleAuthService handles Google Sign In token verification
type GoogleAuthService struct {
cache *CacheService
config *config.Config
client *http.Client
}
// NewGoogleAuthService creates a new Google auth service
func NewGoogleAuthService(cache *CacheService, cfg *config.Config) *GoogleAuthService {
return &GoogleAuthService{
cache: cache,
config: cfg,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// VerifyIDToken verifies a Google ID token and returns the token info
func (s *GoogleAuthService) VerifyIDToken(ctx context.Context, idToken string) (*GoogleTokenInfo, error) {
// Call Google's tokeninfo endpoint to verify the token
url := fmt.Sprintf("%s?id_token=%s", googleTokenInfoURL, idToken)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to verify token: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ErrInvalidGoogleToken
}
var tokenInfo GoogleTokenInfo
if err := json.NewDecoder(resp.Body).Decode(&tokenInfo); err != nil {
return nil, fmt.Errorf("failed to decode token info: %w", err)
}
// Verify the audience matches our client ID(s)
if !s.verifyAudience(tokenInfo.Aud, tokenInfo.Azp) {
return nil, ErrInvalidGoogleAudience
}
// Verify the token is not expired (tokeninfo endpoint already checks this,
// but we double-check for security)
if tokenInfo.Sub == "" {
return nil, ErrInvalidGoogleToken
}
return &tokenInfo, nil
}
// verifyAudience checks if the token audience matches our client ID(s)
func (s *GoogleAuthService) verifyAudience(aud, azp string) bool {
clientID := s.config.GoogleAuth.ClientID
if clientID == "" {
// If not configured, skip audience verification (for development)
return true
}
// Check both aud and azp (Android vs iOS may use different values)
if aud == clientID || azp == clientID {
return true
}
// Also check Android client ID if configured
androidClientID := s.config.GoogleAuth.AndroidClientID
if androidClientID != "" && (aud == androidClientID || azp == androidClientID) {
return true
}
// Also check iOS client ID if configured
iosClientID := s.config.GoogleAuth.IOSClientID
if iosClientID != "" && (aud == iosClientID || azp == iosClientID) {
return true
}
return false
}