refactor(auth): type auth providers and decouple Google auth from disk storage

- Make MailIntakeAuthProvider generic over credential type to enforce
  typed auth contracts between providers and adapters
- Refactor Google OAuth provider to use CredentialStore abstraction
  instead of filesystem-based pickle persistence
- Remove node-local state assumptions from Google auth implementation
- Clarify documentation to distinguish credential lifecycle from
  credential persistence responsibilities

This change enables distributed-safe authentication providers and
allows multiple credential persistence strategies without modifying
auth logic.
This commit is contained in:
2026-01-10 16:40:51 +05:30
parent 24b3b04cfe
commit 4e63c36199
6 changed files with 399 additions and 47 deletions

View File

@@ -8,19 +8,21 @@ It encapsulates all Google-specific authentication concerns, including:
- Credential loading and persistence
- Token refresh handling
- Interactive OAuth flow initiation
- Coordination with a credential persistence layer
No Google authentication details should leak outside this module.
"""
import os
import pickle
from typing import Sequence
import google.auth.exceptions
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from google.oauth2.credentials import Credentials
from mail_intake.auth.base import MailIntakeAuthProvider
from mail_intake.credentials.store import CredentialStore
from mail_intake.exceptions import MailIntakeAuthError
@@ -32,10 +34,10 @@ class MailIntakeGoogleAuth(MailIntakeAuthProvider):
Google's OAuth 2.0 flow and credential management libraries.
Responsibilities:
- Load cached credentials from disk when available
- Load cached credentials from a credential store when available
- Refresh expired credentials when possible
- Initiate an interactive OAuth flow only when required
- Persist refreshed or newly obtained credentials
- Persist refreshed or newly obtained credentials via the store
This class is synchronous by design and maintains a minimal internal state.
"""
@@ -43,48 +45,47 @@ class MailIntakeGoogleAuth(MailIntakeAuthProvider):
def __init__(
self,
credentials_path: str,
token_path: str,
store: CredentialStore[Credentials],
scopes: Sequence[str],
):
"""
Initialize the Google authentication provider.
Args:
credentials_path: Path to the Google OAuth client secrets file.
token_path: Path where OAuth tokens will be cached.
scopes: OAuth scopes required for access.
credentials_path:
Path to the Google OAuth client secrets file used to
initiate the OAuth 2.0 flow.
store:
Credential store responsible for persisting and
retrieving Google OAuth credentials.
scopes:
OAuth scopes required for Gmail access.
"""
self.credentials_path = credentials_path
self.token_path = token_path
self.store = store
self.scopes = list(scopes)
def get_credentials(self):
def get_credentials(self) -> Credentials:
"""
Retrieve valid Google OAuth credentials.
This method attempts to:
1. Load cached credentials from disk
1. Load cached credentials from the configured credential store
2. Refresh expired credentials when possible
3. Perform an interactive OAuth login as a fallback
4. Persist valid credentials for future use
Returns:
Google OAuth credentials object suitable for use with
Google API clients.
A ``google.oauth2.credentials.Credentials`` instance suitable
for use with Google API clients.
Raises:
MailIntakeAuthError: If credentials cannot be loaded, refreshed,
or obtained via interactive authentication.
"""
creds = None
# Attempt to load cached credentials
if os.path.exists(self.token_path):
try:
with open(self.token_path, "rb") as fh:
creds = pickle.load(fh)
except Exception:
creds = None
creds = self.store.load()
# Validate / refresh credentials
if not creds or not creds.valid:
@@ -92,6 +93,7 @@ class MailIntakeGoogleAuth(MailIntakeAuthProvider):
try:
creds.refresh(Request())
except google.auth.exceptions.RefreshError:
self.store.clear()
creds = None
# Interactive login if refresh failed or creds missing
@@ -112,13 +114,12 @@ class MailIntakeGoogleAuth(MailIntakeAuthProvider):
"Failed to complete Google OAuth flow"
) from exc
# Persist refreshed / new credentials
# Persist refreshed or newly obtained credentials
try:
with open(self.token_path, "wb") as fh:
pickle.dump(creds, fh)
self.store.save(creds)
except Exception as exc:
raise MailIntakeAuthError(
f"Failed to write token file: {self.token_path}"
"Failed to persist Google OAuth credentials"
) from exc
return creds