Приложение для Android — Flet/Flutter Google OAuth FlowPython

Программы на Python
Ответить
Anonymous
 Приложение для Android — Flet/Flutter Google OAuth Flow

Сообщение Anonymous »

Я получаю сообщение об ошибке авторизации при посещении URL-адреса клиента Google OAuth из встроенного приложения Android с телефона. Метод oob работал нормально (urn:ietf:wg:oauth:2.0:oob), но в идеале мне бы хотелось, чтобы общий поток работал. Я оставил отдельный поток URL-адресов для настольного компьютера, чтобы я мог по-прежнему заходить в свое приложение с рабочего стола.
Мне нужна помощь, чтобы понять, что я делаю неправильно!
Изображение

Сведения об ошибке
  • Ошибка 400: valid_request
  • Сведения о запросе: redirect_uri=MyFirstApp://callback
  • flowName=GeneralOAuthFlow
Клиент Google Cloud Android OAUTH
  • Название пакета: com.silallybren.myfirstapp
  • Отпечаток сертификата SHA-1: вставьте ключ из хранилища ключей dev
  • Включена пользовательская схема URI (я надеялся, что это поможет разрешить настраиваемый uri перенаправления с использованием встроенного вызова URL-адреса из моего приложения)
Команда сборки пакета Flet
  • flet build apk --project "MyFirstApp" --product "MyFirstApp" --android-adaptive-icon-background "#1E1E1E" --deep-linking-scheme MyFirstApp --deep-linking-host callback --org com.sililybren
Код Python
файл google_auth.py целиком
"""
Google OAuth authentication handler - supports desktop and mobile
"""

import os
import json
import urllib.parse
import urllib.request
from pathlib import Path
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

# Scopes needed for the app
SCOPES = ['https://www.googleapis.com/auth/drive.file']

# Desktop OAuth client (uses OOB flow with code paste)
CLIENT_ID_DESKTOP = ".apps.googleusercontent.com"
CLIENT_SECRET_DESKTOP = ""
REDIRECT_URI_DESKTOP = "urn:ietf:wg:oauth:2.0:oob"

# Android OAuth client (uses deep link redirect)
CLIENT_ID_ANDROID = ".apps.googleusercontent.com"
REDIRECT_URI_ANDROID = "MyFirstApp://callback"

# Token storage
TOKEN_FILE = 'token.json'

def is_mobile():
"""Check if running on mobile (Android/iOS)"""
return 'FLET_APP_STORAGE_DATA' in os.environ

def get_client_id():
"""Get appropriate client ID based on platform"""
return CLIENT_ID_ANDROID if is_mobile() else CLIENT_ID_DESKTOP

def get_client_secret():
"""Get client secret (only needed for desktop)"""
return None if is_mobile() else CLIENT_SECRET_DESKTOP

def get_redirect_uri():
"""Get appropriate redirect URI based on platform"""
return REDIRECT_URI_ANDROID if is_mobile() else REDIRECT_URI_DESKTOP

class GoogleAuth:
"""Handles Google OAuth2 authentication"""

def __init__(self):
# Determine base path
if is_mobile():
self.base_path = Path(os.environ['FLET_APP_STORAGE_DATA'])
else:
self.base_path = Path(__file__).parent

self.token_path = self.base_path / TOKEN_FILE
self.credentials = None
self._sheets_service = None
self._drive_service = None

self._load_token()

def _load_token(self):
"""Load existing token if available"""
if os.path.exists(self.token_path):
try:
self.credentials = Credentials.from_authorized_user_file(
str(self.token_path), SCOPES
)
except Exception as e:
print(f"Error loading token: {e}")
self.credentials = None

def is_authenticated(self) -> bool:
"""Check if we have valid credentials"""
if not self.credentials:
return False

if self.credentials.expired and self.credentials.refresh_token:
try:
from google.auth.transport.requests import Request
self.credentials.refresh(Request())
self._save_token()
return True
except Exception:
return False

return self.credentials.valid

def get_auth_url(self) -> str:
"""Generate the OAuth URL"""
params = {
'client_id': get_client_id(),
'redirect_uri': get_redirect_uri(),
'response_type': 'code',
'scope': ' '.join(SCOPES),
'access_type': 'offline',
'prompt': 'consent',
}
return f"https://accounts.google.com/o/oauth2/au ... de(params)}"

def exchange_code(self, auth_code: str) -> bool:
"""Exchange authorization code for credentials"""
try:
# Build token request - Android doesn't need client_secret
token_params = {
'code': auth_code,
'client_id': get_client_id(),
'grant_type': 'authorization_code',
}

# Only include client_secret for desktop
client_secret = get_client_secret()
if client_secret:
token_params['client_secret'] = client_secret

data = urllib.parse.urlencode(token_params).encode('utf-8')

req = urllib.request.Request(
"https://oauth2.googleapis.com/token",
data=data,
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)

with urllib.request.urlopen(req) as response:
token_data = json.loads(response.read().decode('utf-8'))

# Create credentials - store client info for token refresh
self.credentials = Credentials(
token=token_data['access_token'],
refresh_token=token_data.get('refresh_token'),
token_uri="https://oauth2.googleapis.com/token",
client_id=get_client_id(),
client_secret=get_client_secret(),
scopes=SCOPES,
)

self._save_token()
return True

except Exception as e:
print(f"Error exchanging code: {e}")
return False

def _save_token(self):
"""Save credentials to file"""
try:
with open(self.token_path, 'w') as f:
f.write(self.credentials.to_json())
except Exception as e:
print(f"Error saving token: {e}")

def get_sheets_service(self):
"""Get Google Sheets API service"""
if not self._sheets_service and self.credentials:
self._sheets_service = build('sheets', 'v4', credentials=self.credentials)
return self._sheets_service

def get_drive_service(self):
"""Get Google Drive API service"""
if not self._drive_service and self.credentials:
self._drive_service = build('drive', 'v3', credentials=self.credentials)
return self._drive_service

def logout(self):
"""Clear stored credentials"""
self.credentials = None
self._sheets_service = None
self._drive_service = None

if os.path.exists(self.token_path):
os.remove(self.token_path)


Частичный файл main.py
from google_auth import GoogleAuth
class AppName
def __init__(self):
self.auth: GoogleAuth = None

async def do_startup():
import asyncio
await asyncio.sleep(0.1)

self.auth = GoogleAuth()

if self.auth.is_authenticated():
self._init_app()
else:
self._show_login_screen()
def main(self, page: ft.Page):
self.page = page
page.title = "My First Android App"
page.theme_mode = ft.ThemeMode.DARK
page.padding = ft.Padding(left=20, right=20, top=50, bottom=20)
page.window.width = 412
page.window.height = 915
page.window.icon = "MyFirstApp.png"

# Set up route change handler for OAuth deep link callback
def on_route_change(e):
route = page.route
# Check if this is an OAuth callback
if route and '/callback' in route:
# Extract code from URL params (e.g., /callback?code=ABC123)
if '?' in route:
params = route.split('?')[1]
for param in params.split('&'):
if param.startswith('code='):
code = param[5:] # Remove 'code='
# URL decode the code
import urllib.parse
code = urllib.parse.unquote(code)
self._handle_oauth_callback(code)
return

page.on_route_change = on_route_change

# Show loading screen
page.add(
ft.Container(
content=ft.Column(
[
ft.ProgressRing(),
ft.Text("Loading...", size=16)
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
),
expand=True,
alignment=ft.Alignment(0, 0)
)
)
page.update()

async def do_startup():
import asyncio
await asyncio.sleep(0.1)

self.auth = GoogleAuth()

if self.auth.is_authenticated():
self._init_app()
else:
self._show_login_screen()

page.run_task(do_startup)

def _show_login_screen(self):
"""Show the login/authorization screen"""
self.page.clean()
page = self.page
auth = self.auth

# Check if we're on mobile for simplified flow
from google_auth import is_mobile
self._is_mobile = is_mobile()

status_text = ft.Text("", size=14, color=ft.Colors.GREY_400)

# Different instructions for mobile vs desktop
if self._is_mobile:
instruction_text = ft.Text(
"Tap 'Sign in with Google' below.\nYou'll be redirected back automatically.",
size=14,
color=ft.Colors.GREY_300,
text_align=ft.TextAlign.CENTER,
visible=False,
)
else:
instruction_text = ft.Text(
"1. Tap 'Sign in with Google' below\n2. Sign in and copy the authorization code\n3. Paste the code and tap Submit",
size=14,
color=ft.Colors.GREY_300,
text_align=ft.TextAlign.CENTER,
visible=False,
)

# This button uses the url property which works on Android
google_btn = ft.Button(
"Sign in with Google",
icon=ft.Icons.LOGIN,
url="",
visible=False,
style=ft.ButtonStyle(
padding=ft.Padding.all(15),
shape=ft.RoundedRectangleBorder(radius=10),
)
)

# Code field and submit button only for desktop
code_field = ft.TextField(
label="Paste authorization code here",
width=300,
visible=False,
)

submit_btn = ft.Button(
"Submit Code",
icon=ft.Icons.CHECK,
visible=False,
)
submit_btn.on_click = lambda e: self._submit_auth_code(code_field.value, status_text, submit_btn)

def on_start_click(e):
# Generate auth URL and set it on the button
auth_url = auth.get_auth_url()
google_btn.url = auth_url

# Show login elements
start_btn.visible = False
instruction_text.visible = True
google_btn.visible = True

# Only show code field/submit on desktop
if not self._is_mobile:
code_field.visible = True
submit_btn.visible = True

page.update()

start_btn = ft.Button(
"Start Sign In",
icon=ft.Icons.LOGIN,
on_click=on_start_click,
style=ft.ButtonStyle(
padding=ft.Padding.all(20),
shape=ft.RoundedRectangleBorder(radius=10),
)
)

page.add(
ft.Container(
content=ft.Column(
[
ft.Image(
src="icon.png",
width=100,
height=100,
fit="contain",
),
ft.Text("My First Android App", size=32, weight=ft.FontWeight.BOLD),
ft.Text(
"This is my first android app",
size=16,
color=ft.Colors.GREY_400
),
ft.Container(height=40),
start_btn,
instruction_text,
google_btn,
ft.Container(height=30), # Buffer
code_field,
submit_btn,
status_text,
],
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
alignment=ft.MainAxisAlignment.CENTER,
spacing=10,
),
expand=True,
alignment=ft.Alignment(0, 0)
)
)
page.update()


Подробнее здесь: https://stackoverflow.com/questions/798 ... oauth-flow
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»