Saltar al contenido principal
Requisitos previos: Antes de comenzar, asegúrate de tener instalado lo siguiente:
  • Flutter 3.0 o una versión posterior configurada para web
  • Dart 2.17 o una versión posterior
  • Conocimientos básicos de la CLI de Flutter
Verifica la instalación: flutter --versionSi no tienes una aplicación web de Flutter, crea una: flutter create --platforms=web my_app

Primeros pasos

Esta guía de inicio rápido muestra cómo añadir la autenticación de Auth0 a una aplicación web de Flutter. Crearás una aplicación segura de una sola página con funciones de inicio de sesión, cierre de sesión y perfil de usuario mediante el SDK de Auth0 para Flutter.
1

Crear un nuevo proyecto

Cree una nueva aplicación web de Flutter para esta guía de inicio rápido:
flutter create --platforms=web auth0_flutter_web
Abre el proyecto:
cd auth0_flutter_web
2

Instala el SDK de Auth0 para Flutter

Agrega el SDK de Auth0 para Flutter a tu proyecto con la CLI de Flutter:
flutter pub add auth0_flutter
El SDK requiere que la biblioteca Auth0 SPA JS esté cargada en su aplicación web. Agregue la siguiente etiqueta <script> a su archivo web/index.html, antes de la etiqueta de cierre </body>:
web/index.html
<!DOCTYPE html>
<html>
<head>
  <!-- ... contenido existente del head ... -->
</head>
<body>
  <!-- ... contenido existente del body ... -->

  <!-- Añadir esto antes de la etiqueta de cierre del body -->
  <script src="https://cdn.auth0.com/js/auth0-spa-js/2.9/auth0-spa-js.production.js" defer></script>
</body>
</html>
El script de Auth0 SPA JS es necesario para que el SDK de Flutter Web funcione. Sin él, la autenticación no funcionará.
3

Configura tu aplicación de Auth0

A continuación, debes crear una nueva aplicación en tu inquilino de Auth0.Puedes hacerlo automáticamente ejecutando un comando de la CLI o manualmente desde el Dashboard:
Ejecuta el siguiente comando de shell en el directorio raíz de tu proyecto para crear una aplicación en Auth0:macOS / Linux:
AUTH0_APP_NAME="My Flutter Web App" && \
auth0 apps create -n "${AUTH0_APP_NAME}" -t spa \
  --callbacks http://localhost:3000 \
  --logout-urls http://localhost:3000 \
  --origins http://localhost:3000 \
  --json
Windows (PowerShell):
$appName = "My Flutter Web App"
auth0 apps create -n $appName -t spa `
  --callbacks http://localhost:3000 `
  --logout-urls http://localhost:3000 `
  --origins http://localhost:3000 `
  --json
Copia los valores de domain y client_id de la salida. Los usarás en el siguiente paso.
Si todavía no has instalado Auth0 CLI, ejecuta:
brew tap auth0/auth0-cli && brew install auth0
Luego, autentícate con auth0 login.
4

Configura el SDK

Cree una instancia de la clase Auth0Web con los valores de dominio e ID de cliente de Auth0.Cree un nuevo archivo: lib/auth0_service.dart
lib/auth0_service.dart
import 'package:auth0_flutter/auth0_flutter_web.dart';

class Auth0Service {
  static final Auth0Service _instance = Auth0Service._internal();
  late final Auth0Web auth0Web;

  factory Auth0Service() {
    return _instance;
  }

  Auth0Service._internal() {
    auth0Web = Auth0Web(
      'YOUR_AUTH0_DOMAIN',        // Reemplaza con tu dominio de Auth0
      'YOUR_AUTH0_CLIENT_ID',     // Reemplaza con tu ID de cliente
      cacheLocation: CacheLocation.localStorage, // Mantener sesiones activas
    );
  }
}
Reemplaza YOUR_AUTH0_DOMAIN por el dominio de tu inquilino de Auth0 (por ejemplo, dev-abc123.us.auth0.com) y YOUR_AUTH0_CLIENT_ID por el ID de cliente de tu aplicación en el panel.
Configurar cacheLocation: CacheLocation.localStorage habilita sesiones persistentes entre recargas de página.
5

Crear la vista principal

Reemplaza el contenido de lib/main.dart con el siguiente código:
lib/main.dart
import 'package:flutter/material.dart';
import 'package:auth0_flutter/auth0_flutter_web.dart';
import 'auth0_service.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Auth0 Flutter Web',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MainView(),
    );
  }
}

class MainView extends StatefulWidget {
  const MainView({super.key});

  @override
  State<MainView> createState() => _MainViewState();
}

class _MainViewState extends State<MainView> {
  final auth0Service = Auth0Service();
  Credentials? _credentials;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _handleAuthCallback();
  }

  Future<void> _handleAuthCallback() async {
    try {
      final credentials = await auth0Service.auth0Web.onLoad();
      setState(() {
        _credentials = credentials;
        _isLoading = false;
      });
    } catch (e) {
      print('Error handling auth callback: $e');
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _login() async {
    await auth0Service.auth0Web.loginWithRedirect(
      redirectUrl: 'http://localhost:3000',
    );
  }

  Future<void> _logout() async {
    await auth0Service.auth0Web.logout(
      returnToUrl: 'http://localhost:3000',
    );
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }

    return Scaffold(
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFF667eea), Color(0xFF764ba2)],
          ),
        ),
        child: Center(
          child: Card(
            elevation: 8,
            margin: const EdgeInsets.all(24),
            child: Container(
              constraints: const BoxConstraints(maxWidth: 500),
              padding: const EdgeInsets.all(48),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Icon(
                    Icons.security,
                    size: 64,
                    color: Color(0xFF667eea),
                  ),
                  const SizedBox(height: 24),
                  Text(
                    'Auth0 Flutter Web',
                    style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 16),
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 16,
                      vertical: 12,
                    ),
                    decoration: BoxDecoration(
                      color: _credentials != null
                          ? Colors.green.shade50
                          : Colors.red.shade50,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Row(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Icon(
                          _credentials != null ? Icons.check_circle : Icons.cancel,
                          color: _credentials != null ? Colors.green : Colors.red,
                        ),
                        const SizedBox(width: 8),
                        Text(
                          _credentials != null
                              ? 'You are logged in'
                              : 'You are logged out',
                          style: TextStyle(
                            fontWeight: FontWeight.w600,
                            color: _credentials != null
                                ? Colors.green.shade900
                                : Colors.red.shade900,
                          ),
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 32),
                  if (_credentials == null)
                    ElevatedButton.icon(
                      onPressed: _login,
                      icon: const Icon(Icons.login),
                      label: const Text('Log In'),
                      style: ElevatedButton.styleFrom(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 32,
                          vertical: 16,
                        ),
                        backgroundColor: const Color(0xFF667eea),
                        foregroundColor: Colors.white,
                      ),
                    )
                  else
                    Column(
                      children: [
                        if (_credentials!.user.pictureUrl != null)
                          CircleAvatar(
                            radius: 50,
                            backgroundImage: NetworkImage(
                              _credentials!.user.pictureUrl!.toString(),
                            ),
                          ),
                        const SizedBox(height: 16),
                        Text(
                          _credentials!.user.name ?? 'User',
                          style: Theme.of(context).textTheme.headlineSmall,
                        ),
                        const SizedBox(height: 8),
                        Text(
                          _credentials!.user.email ?? '',
                          style: Theme.of(context).textTheme.bodyMedium?.copyWith(
                            color: Colors.grey.shade600,
                          ),
                        ),
                        const SizedBox(height: 24),
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            ElevatedButton.icon(
                              onPressed: () {
                                Navigator.push(
                                  context,
                                  MaterialPageRoute(
                                    builder: (context) => ProfileView(
                                      credentials: _credentials!,
                                    ),
                                  ),
                                );
                              },
                              icon: const Icon(Icons.person),
                              label: const Text('View Profile'),
                              style: ElevatedButton.styleFrom(
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 24,
                                  vertical: 12,
                                ),
                              ),
                            ),
                            const SizedBox(width: 16),
                            ElevatedButton.icon(
                              onPressed: _logout,
                              icon: const Icon(Icons.logout),
                              label: const Text('Log Out'),
                              style: ElevatedButton.styleFrom(
                                padding: const EdgeInsets.symmetric(
                                  horizontal: 24,
                                  vertical: 12,
                                ),
                                backgroundColor: Colors.red,
                                foregroundColor: Colors.white,
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class ProfileView extends StatelessWidget {
  final Credentials credentials;

  const ProfileView({super.key, required this.credentials});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Profile'),
        backgroundColor: const Color(0xFF667eea),
        foregroundColor: Colors.white,
      ),
      body: Container(
        decoration: const BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Color(0xFF667eea), Color(0xFF764ba2)],
          ),
        ),
        child: Center(
          child: Card(
            elevation: 8,
            margin: const EdgeInsets.all(24),
            child: Container(
              constraints: const BoxConstraints(maxWidth: 600),
              padding: const EdgeInsets.all(48),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Center(
                    child: Column(
                      children: [
                        if (credentials.user.pictureUrl != null)
                          CircleAvatar(
                            radius: 60,
                            backgroundImage: NetworkImage(
                              credentials.user.pictureUrl!.toString(),
                            ),
                          ),
                        const SizedBox(height: 16),
                        Text(
                          credentials.user.name ?? 'User',
                          style: Theme.of(context).textTheme.headlineMedium,
                        ),
                      ],
                    ),
                  ),
                  const SizedBox(height: 32),
                  const Text(
                    'Profile Information',
                    style: TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const Divider(height: 24),
                  _buildInfoRow('Email', credentials.user.email ?? 'N/A'),
                  _buildInfoRow('Name', credentials.user.name ?? 'N/A'),
                  _buildInfoRow('Nickname', credentials.user.nickname ?? 'N/A'),
                  _buildInfoRow('User ID', credentials.user.sub),
                  const SizedBox(height: 24),
                  const Text(
                    'Raw User Object',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 12),
                  Container(
                    padding: const EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.grey.shade100,
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: SingleChildScrollView(
                      scrollDirection: Axis.horizontal,
                      child: SelectableText(
                        credentials.user.toString(),
                        style: const TextStyle(
                          fontFamily: 'monospace',
                          fontSize: 12,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 120,
            child: Text(
              '$label:',
              style: const TextStyle(
                fontWeight: FontWeight.w600,
                color: Colors.grey,
              ),
            ),
          ),
          Expanded(
            child: SelectableText(
              value,
              style: const TextStyle(fontWeight: FontWeight.w500),
            ),
          ),
        ],
      ),
    );
  }
}
Puntos clave:
  • Se invoca onLoad() en initState() para gestionar la callback de autenticación
  • loginWithRedirect() redirige a los usuarios a la página de Universal Login de Auth0
  • logout() cierra la sesión y redirige a tu aplicación
  • Se accede a la información del perfil de usuario mediante credentials.user
6

Ejecuta tu aplicación

Ejecute la aplicación web de Flutter en el puerto 3000:
flutter run -d chrome --web-port 3000
Flutter 3.24.0 y versiones posteriores admiten la compilación a WASM para mejorar el rendimiento:
flutter run -d chrome --web-port 3000 --wasm
ComprobaciónAhora deberías tener una página de inicio de sesión de Auth0 completamente funcional ejecutándose en http://localhost:3000. Al:
  1. Hacer clic en “Log In” - se te redirige a la página de Universal Login de Auth0
  2. Completar la autenticación - se te redirige de vuelta a tu aplicación
  3. Hacer clic en “View Profile” - ves la información de tu usuario
  4. Hacer clic en “Log Out” - cierras sesión tanto en tu aplicación como en Auth0

Uso avanzado

Configura el SDK para solicitar un token de acceso para llamar a API protegidas:
lib/auth0_service.dart
Auth0Service._internal() {
  auth0Web = Auth0Web(
    'YOUR_AUTH0_DOMAIN',
    'YOUR_AUTH0_CLIENT_ID',
    cacheLocation: CacheLocation.localStorage,
  );
}

Future<String?> getAccessToken({String? audience}) async {
  try {
    final token = await auth0Web.getTokenSilently(
      audience: audience ?? 'YOUR_API_IDENTIFIER',
    );
    return token;
  } catch (e) {
    print('Error al obtener el token de acceso: $e');
    return null;
  }
}
Usa el token de acceso para llamar a tu API:
Future<void> callProtectedApi() async {
  final accessToken = await Auth0Service().getAccessToken();

  if (accessToken != null) {
    final response = await http.get(
      Uri.parse('https://your-api.example.com/protected'),
      headers: {
        'Authorization': 'Bearer $accessToken',
      },
    );

    print('Respuesta de la API: ${response.body}');
  }
}
Pasa parámetros adicionales al flujo de inicio de sesión:
Future<void> _loginWithGoogle() async {
  await auth0Service.auth0Web.loginWithRedirect(
    redirectUrl: 'http://localhost:3000',
    authorizationParams: AuthorizationParams(
      connection: 'google-oauth2', // Forzar el inicio de sesión con Google
      screen_hint: 'signup',       // Mostrar la pantalla de registro
    ),
  );
}

Future<void> _loginWithCustomScope() async {
  await auth0Service.auth0Web.loginWithRedirect(
    redirectUrl: 'http://localhost:3000',
    authorizationParams: AuthorizationParams(
      scope: 'openid profile email read:messages',
      audience: 'https://your-api.example.com',
    ),
  );
}
Implementa un manejo de errores adecuado para los fallos de autenticación:
Future<void> _handleAuthCallback() async {
  try {
    final credentials = await auth0Service.auth0Web.onLoad();
    setState(() {
      _credentials = credentials;
      _isLoading = false;
    });
  } on Auth0Exception catch (e) {
    // Gestionar errores específicos de Auth0
    print('Error de Auth0: ${e.message}');
    _showErrorDialog(e.message);
    setState(() {
      _isLoading = false;
    });
  } catch (e) {
    // Gestionar otros errores
    print('Error: $e');
    _showErrorDialog('Se produjo un error inesperado');
    setState(() {
      _isLoading = false;
    });
  }
}

void _showErrorDialog(String message) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Error de autenticación'),
      content: Text(message),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('Aceptar'),
        ),
      ],
    ),
  );
}
Comprueba si el usuario ya está autenticado sin mostrar la página de inicio de sesión:
Future<bool> checkAuthentication() async {
  try {
    final credentials = await auth0Service.auth0Web.onLoad();
    return credentials != null;
  } catch (e) {
    return false;
  }
}

Future<void> silentLogin() async {
  try {
    // Intentar obtener un token de forma silenciosa
    final token = await auth0Service.auth0Web.getTokenSilently();
    if (token != null) {
      // El usuario está autenticado
      print('El usuario está autenticado');
    }
  } catch (e) {
    // El usuario necesita iniciar sesión
    print('El usuario necesita iniciar sesión');
  }
}

Solución de problemas

Error de “Callback URL mismatch”

Problema: La URL de callback no coincide con la configurada en Auth0.Solución: Asegúrate de que la URL de callback en tu código coincida exactamente con la configurada en Auth0 Dashboard:
  1. Ve a Auth0 Dashboard → Applications → Your App → Settings
  2. Verifica que Allowed Callback URLs incluya http://localhost:3000
  3. La URL debe coincidir exactamente (sin barras diagonales finales, a menos que también las incluyas en el código)

La autenticación no funciona

Problema: El botón de inicio de sesión no hace nada o la autenticación falla.Solución: Verifica que el script de Auth0 SPA JS se cargue en web/index.html:
<script src="https://cdn.auth0.com/js/auth0-spa-js/2.9/auth0-spa-js.production.js" defer></script>
Este script debe estar presente antes de la etiqueta de cierre </body>.

El usuario cierra sesión después de recargar la página

Problema: La sesión del usuario no se mantiene entre recargas de la página.Soluciones:
  1. Asegúrate de que Allowed Web Origins incluya http://localhost:3000 en Auth0 Dashboard
  2. Usa cacheLocation: CacheLocation.localStorage al crear una instancia de Auth0Web
  3. Verifica que se llame a onLoad() en el initState() de tu widget

Error de “Invalid state”

Problema: El estado no coincide durante el callback de autenticación.Soluciones:
  1. Borra la caché del navegador y el almacenamiento local
  2. Asegúrate de no abrir varias pestañas durante el inicio de sesión
  3. Verifica que la URL de callback sea correcta

Errores de CORS en la consola del navegador

Problema: Errores de uso compartido de recursos entre orígenes cruzados.Solución:
  1. Agrega http://localhost:3000 a Allowed Web Origins en Auth0 Dashboard
  2. Asegúrate de que la aplicación se esté ejecutando en el puerto 3000 (para que coincida con tu configuración)

Próximos pasos

Ahora que la autenticación ya funciona, puede explorar:

Recursos