File size: 7,470 Bytes
bcc8074
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
"""
Modular Google OAuth Service

A self-contained, plug-and-play service for verifying Google ID tokens.
Can be used in any Python application with minimal configuration.

Usage:
    from services.google_auth_service import GoogleAuthService, GoogleUserInfo
    
    # Initialize with client ID
    auth_service = GoogleAuthService(client_id="your-google-client-id")
    
    # Or use environment variable GOOGLE_CLIENT_ID
    auth_service = GoogleAuthService()
    
    # Verify a Google ID token
    user_info = auth_service.verify_token(id_token)
    print(user_info.email, user_info.google_id, user_info.name)

Environment Variables:
    GOOGLE_CLIENT_ID: Your Google OAuth 2.0 Client ID

Dependencies:
    google-auth>=2.0.0
    google-auth-oauthlib>=1.0.0
"""

import os
import logging
from dataclasses import dataclass
from typing import Optional
from google.oauth2 import id_token as google_id_token
from google.auth.transport import requests as google_requests

logger = logging.getLogger(__name__)


@dataclass
class GoogleUserInfo:
    """
    User information extracted from a verified Google ID token.
    
    Attributes:
        google_id: Unique Google user identifier (sub claim)
        email: User's email address
        email_verified: Whether Google has verified the email
        name: User's display name (may be None)
        picture: URL to user's profile picture (may be None)
        given_name: User's first name (may be None)
        family_name: User's last name (may be None)
        locale: User's locale preference (may be None)
    """
    google_id: str
    email: str
    email_verified: bool = True
    name: Optional[str] = None
    picture: Optional[str] = None
    given_name: Optional[str] = None
    family_name: Optional[str] = None
    locale: Optional[str] = None


class GoogleAuthError(Exception):
    """Base exception for Google Auth errors."""
    pass


class InvalidTokenError(GoogleAuthError):
    """Raised when the token is invalid or expired."""
    pass


class ConfigurationError(GoogleAuthError):
    """Raised when the service is not properly configured."""
    pass


class GoogleAuthService:
    """
    Service for verifying Google OAuth ID tokens.
    
    This service validates ID tokens issued by Google Sign-In and extracts
    user information. It's designed to be modular and reusable across
    different applications.
    
    Example:
        service = GoogleAuthService()
        try:
            user_info = service.verify_token(token_from_frontend)
            print(f"Welcome {user_info.name}!")
        except InvalidTokenError:
            print("Invalid or expired token")
    """
    
    def __init__(
        self,
        client_id: Optional[str] = None,
        clock_skew_seconds: int = 0
    ):
        """
        Initialize the Google Auth Service.
        
        Args:
            client_id: Google OAuth 2.0 Client ID. If not provided,
                      falls back to GOOGLE_CLIENT_ID environment variable.
            clock_skew_seconds: Allowed clock skew in seconds for token
                               validation (default: 0).
        
        Raises:
            ConfigurationError: If no client_id is provided or found.
        """
        self.client_id = client_id or os.getenv("AUTH_SIGN_IN_GOOGLE_CLIENT_ID")
        self.clock_skew_seconds = clock_skew_seconds
        
        if not self.client_id:
            raise ConfigurationError(
                "Google Client ID is required. Either pass client_id parameter "
                "or set GOOGLE_CLIENT_ID environment variable."
            )
        
        logger.info(f"GoogleAuthService initialized with client_id: {self.client_id[:20]}...")
    
    def verify_token(self, id_token: str) -> GoogleUserInfo:
        """
        Verify a Google ID token and extract user information.
        
        Args:
            id_token: The ID token received from the frontend after
                     Google Sign-In.
        
        Returns:
            GoogleUserInfo: Dataclass containing user's Google profile info.
        
        Raises:
            InvalidTokenError: If the token is invalid, expired, or
                              doesn't match the expected client ID.
        """
        if not id_token:
            raise InvalidTokenError("Token cannot be empty")
        
        try:
            # Verify the token with Google
            idinfo = google_id_token.verify_oauth2_token(
                id_token,
                google_requests.Request(),
                self.client_id,
                clock_skew_in_seconds=self.clock_skew_seconds
            )
            
            # Validate issuer
            if idinfo.get("iss") not in ["accounts.google.com", "https://accounts.google.com"]:
                raise InvalidTokenError("Invalid token issuer")
            
            # Validate audience
            if idinfo.get("aud") != self.client_id:
                raise InvalidTokenError("Token was not issued for this application")
            
            # Extract user info
            return GoogleUserInfo(
                google_id=idinfo["sub"],
                email=idinfo["email"],
                email_verified=idinfo.get("email_verified", False),
                name=idinfo.get("name"),
                picture=idinfo.get("picture"),
                given_name=idinfo.get("given_name"),
                family_name=idinfo.get("family_name"),
                locale=idinfo.get("locale")
            )
            
        except ValueError as e:
            logger.warning(f"Token verification failed: {e}")
            raise InvalidTokenError(f"Token verification failed: {str(e)}")
        except Exception as e:
            logger.error(f"Unexpected error during token verification: {e}")
            raise InvalidTokenError(f"Token verification error: {str(e)}")
    
    def verify_token_safe(self, id_token: str) -> Optional[GoogleUserInfo]:
        """
        Verify a Google ID token without raising exceptions.
        
        Useful for cases where you want to check validity without
        exception handling.
        
        Args:
            id_token: The ID token to verify.
        
        Returns:
            GoogleUserInfo if valid, None if invalid.
        """
        try:
            return self.verify_token(id_token)
        except GoogleAuthError:
            return None


# Singleton instance for convenience (initialized on first use)
_default_service: Optional[GoogleAuthService] = None


def get_google_auth_service() -> GoogleAuthService:
    """
    Get the default GoogleAuthService instance.
    
    Creates a singleton instance using environment variables.
    
    Returns:
        GoogleAuthService: The default service instance.
    
    Raises:
        ConfigurationError: If GOOGLE_CLIENT_ID is not set.
    """
    global _default_service
    if _default_service is None:
        _default_service = GoogleAuthService()
    return _default_service


def verify_google_token(id_token: str) -> GoogleUserInfo:
    """
    Convenience function to verify a token using the default service.
    
    Args:
        id_token: The Google ID token to verify.
    
    Returns:
        GoogleUserInfo: Verified user information.
    
    Raises:
        InvalidTokenError: If verification fails.
        ConfigurationError: If service is not configured.
    """
    return get_google_auth_service().verify_token(id_token)