feat: implement Google OAuth support for Android and iOS

- Added support for Google OAuth with separate client IDs for Android and iOS.
- Updated `verify_google_id_token` to validate `aud` against both client IDs and check `email_verified`.
- Modified `google_oauth_handler` to accept and process the new client IDs.
- Enhanced security by enforcing explicit JWT algorithm validation.
- Updated mobile app to handle Google OAuth flow using `expo-auth-session`.
- Fixed API request to send `id_token` in snake_case as expected by the backend.
- Added necessary environment variables for Google client IDs in mobile app.
- Implemented intent filter for Google OAuth redirect in AndroidManifest.xml.
This commit is contained in:
2026-03-19 15:24:08 -03:00
parent 31c47fe69f
commit 34cbd4a861
16 changed files with 972 additions and 220 deletions
+40 -11
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import axios from 'axios';
import { Link, useRouter } from 'expo-router';
@@ -6,14 +6,19 @@ import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Ionicons } from '@expo/vector-icons';
import * as Google from 'expo-auth-session/providers/google';
import * as WebBrowser from 'expo-web-browser';
import { Screen } from '@presentation/components/layout/Screen';
import { Input } from '@presentation/components/ui/Input';
import { Button } from '@presentation/components/ui/Button';
import { colors, typography, spacing } from '@shared/theme';
import { loginUseCase } from '@infrastructure/container';
import { loginUseCase, googleLoginUseCase } from '@infrastructure/container';
import { useAuthStore } from '@store/authStore';
import { CatIcon } from '@presentation/components/icons/CatIcon';
// Necessário para fechar o browser após redirect OAuth no Android/iOS
WebBrowser.maybeCompleteAuthSession();
const schema = z.object({
email: z.string().email('E-mail inválido'),
password: z.string().min(1, 'Senha obrigatória'),
@@ -31,6 +36,36 @@ export default function LoginScreen(): React.ReactElement {
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
// Hook do expo-auth-session para Google OAuth.
// Usa os client IDs de Android e iOS configurados via variáveis de ambiente.
const [_request, response, promptAsync] = Google.useAuthRequest({
androidClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID_ANDROID,
iosClientId: process.env.EXPO_PUBLIC_GOOGLE_CLIENT_ID_IOS,
});
// Reage ao resultado do fluxo OAuth assim que o browser fecha
useEffect(() => {
if (response?.type === 'success') {
const idToken = response.authentication?.idToken;
if (!idToken) {
Alert.alert('Erro', 'Não foi possível obter o token do Google.');
setIsGoogleLoading(false);
return;
}
googleLoginUseCase
.execute({ idToken })
.then((session) => setSession(session))
.then(() => router.replace('/(app)/(tabs)/home'))
.catch(() => Alert.alert('Erro', 'Não foi possível entrar com o Google.'))
.finally(() => setIsGoogleLoading(false));
} else if (response?.type === 'error') {
Alert.alert('Erro', 'Autenticação com Google cancelada ou falhou.');
setIsGoogleLoading(false);
} else if (response?.type === 'dismiss') {
setIsGoogleLoading(false);
}
}, [response]);
const { control, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '' },
@@ -57,16 +92,10 @@ export default function LoginScreen(): React.ReactElement {
}
}
async function onGoogleLogin(): Promise<void> {
function onGoogleLogin(): void {
setIsGoogleLoading(true);
try {
// TODO: Google Sign-In + GoogleLoginUseCase
router.replace('/(app)/(tabs)/home');
} catch {
Alert.alert('Erro', 'Não foi possível entrar com o Google.');
} finally {
setIsGoogleLoading(false);
}
// promptAsync abre o browser; o resultado chega via useEffect no `response`
promptAsync();
}
return (