Autenticación Social (Google)
Índice de contenido
- Autenticación Social (Google)
- Flujo de Trabajo General
- 1. Configuración de Google Cloud Console
- Cómo Obtener las Huellas SHA-1
- 2. Implementación en Flutter (Servicio en Dart)
- 3. Integración con el Backend
- Opción A: Servidor Laravel con Laravel Socialite
- Opción B: Proceso Manual en Backend (Python / Django)
La autenticación social es una de las funcionalidades más requeridas en las aplicaciones móviles modernas. Permite a los usuarios registrarse e iniciar sesión con un solo toque utilizando sus cuentas existentes de Google, GitHub u otras plataformas. En esta sección, analizaremos cómo implementar el flujo completo de autenticación de forma segura combinando Flutter y un backend web.
Ofrecer esta característica en entornos móviles es fundamental para mantener la coherencia de todo el ecosistema de software. Si una plataforma ya permite la autenticación social en su versión web y cuenta además con una aplicación móvil, lo correcto es replicar este mecanismo en el dispositivo. De lo contrario, un usuario que haya creado su cuenta originalmente a través de un proveedor externo se verá imposibilitado para iniciar sesión en la aplicación móvil.
Flujo de Trabajo General
El flujo recomendado para realizar autenticación social de manera segura en dispositivos móviles consta de los siguientes pasos:
- Autenticación Nativa/Web en el Dispositivo: La app móvil inicia el flujo correspondiente (nativo para Google o a través de un WebView controlado para plataformas como GitHub) para que el usuario inicie sesión.
- Obtención del Token: Una vez autenticado con el proveedor social, la app obtiene un token de acceso (Access Token) o de identidad (ID Token). Para mitigar problemas con estados persistentes en caché que fuercen inicios de sesión automáticos no deseados, es una excelente práctica ejecutar un cierre de sesión explícito en el cliente (
signOut) inmediatamente antes de disparar el nuevo flujo de autenticación. - Envío al Servidor (Backend): La app envía este token a la API del backend mediante HTTPS junto al nombre del proveedor. Es indispensable comprender que la autenticación social no debe gestionarse exclusivamente de manera local en el dispositivo móvil; los datos de sesión y del usuario deben persistirse e interactuar directamente con un backend (desarrollado en Laravel, Django, FastAPI o Node.js) para que la autenticación tenga un valor real y seguro dentro del sistema.
- Validación en el Servidor: El servidor contacta al proveedor para validar el token y obtener los datos del usuario de forma segura. Si es válido, busca o crea el usuario en la base de datos y genera el token de sesión definitivo (por ejemplo, con Laravel Sanctum).
- Respuesta y Persistencia: El backend retorna el token al dispositivo, el cual se guarda de manera segura para autenticar futuras peticiones. La información del usuario devuelta por la API debe ser idéntica a la que se entregaría en un flujo de autenticación tradicional mediante usuario y contraseña.
1. Configuración de Google Cloud Console
Para implementar el inicio de sesión con Google, es indispensable configurar correctamente las credenciales en la Google Cloud Console. Uno de los errores más comunes es confundir los distintos tipos de credenciales necesarios:
- Credencial de Aplicación Web (Backend): Debes crear una credencial de tipo "Aplicación Web". El ID de cliente generado aquí (denominado
serverClientIden Flutter) se utiliza para indicarle al SDK de Google que necesitamos un token compatible para que el backend pueda validarlo. Si se omite este parámetro o se configura con las credenciales de Android, el servidor web será incapaz de comprobar la validez del token recibido. - Credencial de Android: Debes crear una credencial de tipo "Android". En esta credencial debes ingresar el nombre del paquete de tu app (por ejemplo,
com.your.app) y la huella digital SHA-1 de firma de tu aplicación. A nivel de código en Flutter no es necesario mapear explícitamente este ID de cliente de Android, ya que el ecosistema de Google realiza la validación de manera interna cruzando el nombre del paquete y la firma digital.
Cómo Obtener las Huellas SHA-1
Google exige la huella digital SHA-1 para autorizar las peticiones de inicio de sesión nativas desde Android.
SHA-1 para Pruebas (Debug):
En tu máquina de desarrollo, la aplicación se firma automáticamente con un almacén de claves por defecto. Puedes obtener esta firma ejecutando el siguiente comando en la terminal (la contraseña predeterminada del almacén es android):
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass androidSHA-1 para Producción:
Si estás compilando un APK de producción en tu máquina mediante flutter build apk --release usando tu propio archivo Almacén de claves (.jks o .keystore), necesitas extraer la SHA-1 directamente de ese archivo privado. Ejecuta el siguiente comando en tu terminal (necesitas tener instalado Java/Keytool en tu sistema):
keytool -list -v -keystore /ruta/de/tu/archivo-llave.jks -alias tu-alias-de-produccionReemplaza /ruta/de/tu/archivo-llave.jks por la ubicación real de tu archivo de firmas. Te pedirá la contraseña que le asignaste a tu llave al crearla. En la salida verás el bloque de Huellas digitales con la SHA-1 de producción.
Nota importante sobre Producción y Google Play: Al subir la aplicación a producción, Google Play Console suele modificar la firma digital de la aplicación a través del servicio de protección de la Play Store. Esto significa que la SHA-1 generada localmente para release dejará de funcionar en producción. Para solucionarlo, debes ingresar a la consola de desarrollo de Google Play, seleccionar tu aplicación, navegar a la sección de Protección de la Play Store (Play App Signing), copiar la huella digital SHA-1 provista directamente por Google e ingresarla como un nuevo cliente Android dentro de tu proyecto en Google Cloud Console.
2. Implementación en Flutter (Servicio en Dart)
Para manejar la lógica de Google nativo se utiliza el paquete google_sign_in. A continuación se presenta una estructura limpia del servicio:
import 'dart:convert';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
class SocialAuthService {
// Es indispensable configurar el ID de cliente correspondiente a la APLICACIÓN WEB
final GoogleSignIn _googleSignIn = GoogleSignIn(
serverClientId: "TU_CLIENT_ID_DE_APLICACION_WEB.apps.googleusercontent.com",
scopes: ['email', 'profile'],
);
Future<String?> getGoogleAccessToken() async {
try {
// Forzar el cierre de sesión previo para limpiar estados en caché y evitar logins automáticos
if (await _googleSignIn.isSignedIn()) {
await _googleSignIn.signOut();
}
final GoogleSignInAccount? account = await _googleSignIn.signIn();
if (account == null) return null; // Flujo cancelado por el usuario
final GoogleSignInAuthentication auth = await account.authentication;
// Se prioriza el accessToken o el idToken de acuerdo a los requerimientos de validación del backend
return auth.accessToken ?? auth.idToken;
} catch (e) {
print("Error en Google Sign-In: \$e");
return null;
}
}
}
La página de login de ejemplo:
lib\pages\user\login_page.dart
class _LoginPageState extends State<LoginPage> {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final SocialAuthService _socialAuthService = SocialAuthService();
***
ElevatedButton(
text: "Google",
colors: [
const Color(0xFFEA4335),
const Color(0xFFDB4437)
],
onPressed: _loginWithGoogle,
),
***
void _loginWithGoogle() async {
setStateIfMounted(() {
socialLoading = true;
});
try {
final token = await _socialAuthService.getGoogleAccessToken();
if (token == null) {
setStateIfMounted(() {
socialLoading = false;
});
return;
}
final response = await _socialAuthService.authenticateOnBackend(
provider: 'google',
accessToken: token,
);
_handleSocialBackendResponse(response, 'google');
} catch (e) {
debugPrint("Error Google Auth flow: $e");
showToastMessage(context, LocaleKeys.errorGoogleLogin.tr());
setStateIfMounted(() {
socialLoading = false;
});
}
}Como puedes ver son dos pasos:
- getGoogleAccessToken, es 100% Google, para obtener el Token.
- authenticateOnBackend, es un método que gestiona la respuesta de tu Rest API que, dado el token de autorización de Google del paso uno, lo valida y devuelve TU usuario autenticado de TU app.
Future<Map<String, dynamic>> authenticateOnBackend({
required String provider,
required String accessToken,
}) async {
var url = Uri.https(baseUrlAPI, "/api/v1/auth/social");
if (!appApiUseHttps) {
url = Uri.http(baseUrlAPI, "/api/v1/auth/social");
}
try {
final response = await Api.post(
url,
body: {
'provider': provider,
'access_token': accessToken,
},
);
debugPrint("Response Status: ${response.statusCode}");
debugPrint("Response Body: ${response.body}");
if (response.statusCode == 200) {
return json.decode(response.body) as Map<String, dynamic>;
} else {
final Map<String, dynamic> errorData =
json.decode(response.body) as Map<String, dynamic>;
return {
'error': true,
'message':
errorData['message'] ?? 'Error de autenticación con el servidor.'
};
}
} catch (e) {
debugPrint("Error al conectar con el backend: $e");
return {
'error': true,
'message': 'No se pudo conectar con el servidor de Desarrollolibre.'
};
}
}3. Integración con el Backend
Opción A: Servidor Laravel con Laravel Socialite
Laravel Socialite simplifica la validación de tokens sociales mediante el método userFromToken(). A continuación, se muestra el controlador de Laravel que procesa la petición de la app móvil:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Laravel\Socialite\Facades\Socialite;
class SocialController extends Controller
{
public function handleSocialAuth(Request $request): JsonResponse
{
$request->validate([
'provider' => ['required', 'string', 'in:google,github'],
'access_token' => ['required', 'string'],
]);
$provider = $request->input('provider');
$token = $request->input('access_token');
// Para GitHub: si viene un código temporal (OAuth2 Web) en lugar del token final, lo intercambiamos
if ($provider === 'github' && !str_starts_with($token, 'gho_') && !str_starts_with($token, 'ghu_')) {
$response = \Illuminate\Support\Facades\Http::asJson()->post('https://github.com/login/oauth/access_token', [
'client_id' => config('services.github.client_id'),
'client_secret' => config('services.github.client_secret'),
'code' => $token,
]);
if ($response->successful()) {
$data = [];
parse_str($response->body(), $data);
$token = $data['access_token'] ?? $token;
}
}
try {
$socialUser = Socialite::driver($provider)->userFromToken($token);
} catch (\Exception $e) {
return response()->json(['message' => 'Token inválido', 'error' => $e->getMessage()], 401);
}
$user = User::where($provider . '_id', $socialUser->getId())->first();
if (!$user) {
$email = $socialUser->getEmail();
if (!$email) {
return response()->json([
'requires_email' => true,
'social_data' => [
'id' => $socialUser->getId(),
'name' => $socialUser->getName() ?? 'User',
'avatar' => $socialUser->getAvatar(),
'provider' => $provider,
]
], 200);
}
$user = User::create([
'name' => $socialUser->getName() ?? 'User',
'email' => $email,
$provider . '_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar(),
'password' => bcrypt(str_random(24)),
]);
}
return response()->json([
'access_token' => $user->createToken('mobile-app')->plainTextToken,
'token_type' => 'Bearer',
'user' => $user
], 200);
}
}Este método es el empleado desde Flutter en authenticateOnBackend().
Opción B: Proceso Manual en Backend (Python / Django)
Si no utilizas Laravel, puedes validar el token manualmente realizando una solicitud HTTPS directa a las APIs de validación del proveedor. A continuación, se muestra un ejemplo conceptual en Python (Django / Flask) para verificar tokens de Google y GitHub:
import requests
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
@csrf_exempt
def handle_social_auth(request):
if request.method == 'POST':
data = json.loads(request.body)
provider = data.get('provider')
token = data.get('access_token')
if provider == 'google':
# Validación de ID Token con la API de Google
response = requests.get(f'https://oauth2.googleapis.com/tokeninfo?id_token={token}')
if response.status_code != 200:
return JsonResponse({'message': 'Token de Google inválido'}, status=401)
user_info = response.json()
social_id = user_info.get('sub')
email = user_info.get('email')
name = user_info.get('name')
elif provider == 'github':
# Intercambio de código por access_token si aplica
if not token.startswith('gho_'):
res = requests.post(
'https://github.com/login/oauth/access_token',
headers={'Accept': 'application/json'},
data={
'client_id': 'TU_GITHUB_CLIENT_ID',
'client_secret': 'TU_GITHUB_CLIENT_SECRET',
'code': token
}
)
token = res.json().get('access_token')
# Consulta directa a la API de GitHub
headers = {'Authorization': f'token {token}'}
response = requests.get('https://api.github.com/user', headers=headers)
if response.status_code != 200:
return JsonResponse({'message': 'Token de GitHub inválido'}, status=401)
user_info = response.json()
social_id = str(user_info.get('id'))
name = user_info.get('name') or user_info.get('login')
email = user_info.get('email') # Puede requerir llamada adicional si es privado
# Aquí procedes a registrar o loguear al usuario en tu BD y emitir tu token JWT
return JsonResponse({'message': 'Autenticación exitosa', 'user': {'name': name, 'email': email}})