メインコンテンツへスキップ
前提条件: 開始する前に、以下がインストールされていることを確認してください。インストールを確認するには、次を実行します: flutter --versionFlutter Web アプリがない場合は、次のコマンドで作成します: flutter create --platforms=web my_app

はじめに

このクイックスタートでは、Flutter Web アプリケーションに Auth0 認証を追加する方法を説明します。Auth0 Flutter SDK を使用して、ログイン、ログアウト、ユーザープロファイル機能を備えたセキュアなシングルページアプリケーションを構築します。
1

新しいプロジェクトを作成

このクイックスタート用に、新しい Flutter Web アプリケーションを作成します。
flutter create --platforms=web auth0_flutter_web
プロジェクトを開く:
cd auth0_flutter_web
2

Auth0 Flutter SDKをインストールする

Flutter CLI を使用して、Auth0 Flutter SDK をプロジェクトに追加します。
flutter pub add auth0_flutter
SDK を使用するには、Web アプリケーションで Auth0 SPA JS ライブラリを読み込む必要があります。終了の </body> タグの前に、次の <script> タグを web/index.html ファイルに追加します。
web/index.html
<!DOCTYPE html>
<html>
<head>
  <!-- ... 既存のheadコンテンツ ... -->
</head>
<body>
  <!-- ... 既存のbodyコンテンツ ... -->

  <!-- bodyの閉じタグの前に追加してください -->
  <script src="https://cdn.auth0.com/js/auth0-spa-js/2.9/auth0-spa-js.production.js" defer></script>
</body>
</html>
Flutter Web SDK を機能させるには、Auth0 SPA JS スクリプトが必要です。これがないと、認証は動作しません。
3

Auth0アプリを設定する

次に、Auth0テナントに新しいアプリケーションを作成する必要があります。これは、CLI コマンドを実行して自動で行うことも、Auth0 Dashboard から手動で行うこともできます。
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
出力から ドメインclient_id の値をコピーします。これらは次の手順で使用します。
まだ Auth0 CLI をインストールしていない場合は、次を実行します。
brew tap auth0/auth0-cli && brew install auth0
その後、auth0 login でログインします。
4

SDK を設定する

Auth0 の ドメインクライアントID の値を使用して、Auth0Web クラスのインスタンスを作成します。新しいファイル 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',        // Auth0ドメインに置き換えてください
      'YOUR_AUTH0_CLIENT_ID',     // クライアントIDに置き換えてください
      cacheLocation: CacheLocation.localStorage, // セッションを永続化する
    );
  }
}
YOUR_AUTH0_DOMAIN は Auth0 テナントのドメイン (例: dev-abc123.us.auth0.com) に、YOUR_AUTH0_CLIENT_ID はダッシュボードに表示されるアプリケーションのクライアントIDに置き換えてください。
cacheLocation: CacheLocation.localStorage を設定すると、ページを再読み込みしてもセッションが維持されます。
5

メインビューを作成

lib/main.dart の内容を以下のコードに置き換えてください。
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('メール', credentials.user.email ?? 'N/A'),
                  _buildInfoRow('名前', credentials.user.name ?? 'N/A'),
                  _buildInfoRow('ニックネーム', credentials.user.nickname ?? 'N/A'),
                  _buildInfoRow('ユーザー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),
            ),
          ),
        ],
      ),
    );
  }
}
重要なポイント:
  • 認証コールバックを処理するため、initState() 内で onLoad() が呼び出されます
  • loginWithRedirect() はユーザーを Auth0 の Universal Login ページへリダイレクトします
  • logout() はセッションをクリアし、アプリに戻るようリダイレクトします
  • ユーザープロファイル情報には credentials.user からアクセスできます
6

アプリを実行する

Flutter Web アプリケーションをポート 3000 で起動します:
flutter run -d chrome --web-port 3000
Flutter 3.24.0 以降では、パフォーマンス向上のために WASM コンパイルをサポートしています。
flutter run -d chrome --web-port 3000 --wasm
チェックポイントこれで、http://localhost:3000 で Auth0 のログインページが完全に動作するようになっているはずです。次の操作を行うと、以下のようになります。
  1. 「Log In」をクリックすると、Auth0 の Universal Login ページにリダイレクトされます
  2. 認証を完了すると、アプリにリダイレクトされます
  3. 「View Profile」をクリックすると、ユーザー情報が表示されます
  4. 「Log Out」をクリックすると、アプリと Auth0 の両方からログアウトします

高度な使用

保護されたAPIを呼び出すためのアクセストークンを要求するように SDK を設定します。
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 getting access token: $e');
    return null;
  }
}
アクセストークンを使用して 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('API Response: ${response.body}');
  }
}
ログインフローに追加のパラメーターを渡します。
Future<void> _loginWithGoogle() async {
  await auth0Service.auth0Web.loginWithRedirect(
    redirectUrl: 'http://localhost:3000',
    authorizationParams: AuthorizationParams(
      connection: 'google-oauth2', // Google ログインを強制
      screen_hint: 'signup',       // サインアップ画面を表示
    ),
  );
}

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',
    ),
  );
}
認証失敗に備えて適切なエラー処理を実装します。
Future<void> _handleAuthCallback() async {
  try {
    final credentials = await auth0Service.auth0Web.onLoad();
    setState(() {
      _credentials = credentials;
      _isLoading = false;
    });
  } on Auth0Exception catch (e) {
    // Auth0 固有のエラーを処理
    print('Auth0 Error: ${e.message}');
    _showErrorDialog(e.message);
    setState(() {
      _isLoading = false;
    });
  } catch (e) {
    // その他のエラーを処理
    print('Error: $e');
    _showErrorDialog('An unexpected error occurred');
    setState(() {
      _isLoading = false;
    });
  }
}

void _showErrorDialog(String message) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Authentication Error'),
      content: Text(message),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('OK'),
        ),
      ],
    ),
  );
}
ログインページを表示せずに、ユーザーがすでに認証されているかどうかを確認します。
Future<bool> checkAuthentication() async {
  try {
    final credentials = await auth0Service.auth0Web.onLoad();
    return credentials != null;
  } catch (e) {
    return false;
  }
}

Future<void> silentLogin() async {
  try {
    // サイレントでトークンの取得を試行
    final token = await auth0Service.auth0Web.getTokenSilently();
    if (token != null) {
      // ユーザーは認証済み
      print('User is authenticated');
    }
  } catch (e) {
    // ユーザーはログインが必要
    print('User needs to log in');
  }
}

トラブルシューティング

「Callback URL mismatch」エラー

問題: コールバックURLが Auth0 に設定されているURLと一致していません。解決方法: コード内のコールバックURLが Auth0 Dashboard の設定と完全に一致していることを確認してください。
  1. Auth0 Dashboard → Applications → Your App → Settings に移動します
  2. Allowed Callback URLshttp://localhost:3000 が含まれていることを確認します
  3. URL は完全に一致している必要があります (コード側で指定していない限り、末尾のスラッシュは付けないでください)

認証が機能しない

問題: ログインボタンをクリックしても何も起こらない、または認証に失敗します。解決方法: web/index.html で Auth0 SPA JS スクリプトが読み込まれていることを確認してください。
<script src="https://cdn.auth0.com/js/auth0-spa-js/2.9/auth0-spa-js.production.js" defer></script>
このスクリプトは、閉じタグ </body> の前に配置する必要があります。

ページを更新するとユーザーがログアウトされる

問題: ページを再読み込みするとユーザーセッションが維持されません。解決方法:
  1. Auth0 Dashboard の Allowed Web Originshttp://localhost:3000 が含まれていることを確認します
  2. Auth0Web インスタンスの作成時に cacheLocation: CacheLocation.localStorage を使用します
  3. ウィジェットの initState()onLoad() が呼び出されていることを確認します

「Invalid state」エラー

問題: 認証コールバック中に state が一致していません。解決方法:
  1. ブラウザのキャッシュとローカルストレージを削除します
  2. ログイン中に複数のタブを開いていないことを確認します
  3. コールバックURLが正しいことを確認します

ブラウザコンソールで CORS エラーが発生する

問題: Cross-Origin Resource Sharing (CORS) エラーが発生しています。解決方法:
  1. Auth0 Dashboard の Allowed Web Originshttp://localhost:3000 を追加します
  2. ポート 3000 で実行していることを確認します (設定と一致している必要があります)

次のステップ

認証が動作するようになったら、次の項目も確認してください。

リソース