0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】Amazon Cognitoを用いたシンプルなログイン実装

Last updated at Posted at 2025-03-21

今回実装してみた内容

モバイルアプリ(Flutter)x Amazon Cognito x ソーシャルプロバイダー(Apple, Google)
ざっくり図で表すとこんな感じ
スクリーンショット 2025-03-21 12.43.36.png

モバイルアプリ x Amazon Cognito x ソーシャルプロバイダー認証 には大きく3つの実装方法があるかなと思います。

  • ネイティブSDK+Cognito連携:アプリからネイティブの認証機能を用いて、トークンをCognitoに渡して認証
  • Hosted UI:マネージドログインを使用した認証
  • AWS Amplify:AWS Amplifyを利用した認証
方法 カスタマイズ性 実装の容易さ ソーシャルログイン連携 セキュリティ
ネイティブSDK+Cognito連携 ◎(自由自在) △(やや複雑) 手動で実装(自由度高) アプリ実装に依存
Hosted UI △(限られる) ◎(超簡単) AWSマネージドで簡単 AWSが管理
AWS Amplify ○(ある程度可) ◎(簡単) Hosted UI / ネイティブ 両方対応 AWSが管理

→今回はHosted UIを使用した認証の実装をしました

スマホで、以下のような画面(マネージドログイン)に遷移し、認証することを目指します。

Simulator Screenshot - iPhone 15 Pro - 2025-03-21 at 13.29.29.png

お願い

  • いつでも見返せるようにいいねをお願いします🙇
  • 間違っているポイント、質問などございましたら、お気軽にコメントしてください。
  • 手順が多く、長編となっております。

Amazon Cognitoを用いたシンプルなログイン実装方法

目次

  1. AWS側
    1. ユーザープールを作成
    2. ソーシャルプロバイダーとカスタムプロバイダーの設定
      1. Google
      2. Apple
    3. アプリケーションクライアントの設定
    4. おまけ
      1. 認証方法のカスタマイズ
      2. マネージドログイン画面のカスタマイズ
      3. メッセージテンプレート作成
  2. アプリ側
    1. 必要な設定
      1. iOS側
      2. Android側
    2. ライブラリ
    3. configクラス
    4. 認証処理をまとめたクラス
      1. 認証処理
      2. その他の便利な関数
  3. まとめ

はじめに

Amazon Cognitoは、ウェブおよびモバイルアプリケーションの認証、認可、ユーザー管理を提供するAWSのサービスです。このサービスを使用することで、ユーザー登録やサインイン機能を簡単に実装でき、さらにGoogle、Apple、Facebookなどのソーシャルアイデンティティプロバイダーと連携することもできます。

AWS側

ユーザープールを作成

  1. AWSにログインし、Cognitoの画面に移動します。

  2. 右上の「ユーザープールを作成」ボタンをクリックします。
    スクリーンショット 2025-03-20 18.05.54.png

  3. 以下のように記入し、ユーザープールを作成します。
    スクリーンショット 2025-03-23 14.31.39.png
    項目の解説

項目 解説
アプリケーションタイプ 開発するアプリケーションのタイプ
アプリケーションに名前を付ける アプリの名前
サインイン識別子のオプション ユーザーがログインする際に使用する識別子
サインアップのための必須属性 ユーザーが登録時に入力する必要がある情報

注意
サインイン識別子のオプションとサインアップのための必須属性は後から変更不可です。

ソーシャルプロバイダーとカスタムプロバイダーの設定

  1. 各ソーシャルプロバイダー側で必要な設定を行い、クライアントIDなどを取得します。ここの説明は公式が丁寧にされているので、省きます。
    公式ページはこちら
    スクリーンショット 2025-03-20 19.04.45.png

Google

  1. 右上のボタンからアイデンティティプロバイダーを追加します。
    スクリーンショット 2025-03-20 18.58.36.png

  2. 以下の項目を埋めます。
    スクリーンショット 2025-03-23 14.42.52.png

項目の解説

項目 解説
クライアントID Google API Consoleで取得したクライアントID
クライアントシークレット Google API Consoleで取得したクライアントシークレット
許可されたスコープ Googleから取得したい情報(例:email profile openid)
Googleとユーザープール間で属性をマッピング GoogleとCognitoのユーザー属性を対応付ける設定

Apple

  1. 右上のボタンからアイデンティティプロバイダーを追加します。
    スクリーンショット 2025-03-20 18.58.36.png

  2. 以下の項目を埋めます。
    スクリーンショット 2025-03-23 14.45.24.png

項目の解説

項目 解説
AppleサービスID Apple DeveloperでService ID登録時に設定したID
チームID Apple DeveloperのチームID
キーID Apple Developerで生成されたKey ID
プライベートキー Apple Developerで生成された.p8ファイルをアップロード
許可されたスコープ Appleから取得したい要素
Appleとユーザープール間で属性をマッピング AppleとCognitoのユーザー属性を対応付ける設定

アプリケーションクライアントの設定

  1. アプリを選択します。
    スクリーンショット 2025-03-23 14.55.56.png

  2. ログインページの中の編集を押下します。
    スクリーンショット 2025-03-20 19.42.59.png

  3. 以下の項目を埋めます。
    スクリーンショット 2025-03-20 20.43.42.png

項目の解説

項目 解説
許可されているコールバックURL サインイン後にリダイレクトされるURL(例:com.example.app://callback)
許可されているサインアウトURL ログアウト後にリダイレクトされるURL(例:com.example.app://callback)
ID プロパイダー 使用するIDプロバイダー(例:Google、Apple)
OpenID Connectedのスコープ OpenID Connectで要求するスコープ

※OpenID Connectedのスコープ:openidは必須です。IDトークンを取得するために必要なスコープ。これが無いと「認証(ログイン)」が動作しないため。

※許可されているサインアウトURLはコールバックURLと同じで問題ないです

おまけ(ここの部分はやらなくても問題ないです)

認証方法のカスタマイズ

以下のページで認証方法をお好みにカスタマイズします。
私はパスワード要件を緩くしました。
スクリーンショット 2025-03-20 18.26.56.png

マネージドログイン画面のカスタマイズ

右上のボタンでマネージドログイン画面をオシャレにしましょう。
スクリーンショット 2025-03-20 18.40.12.png

このように自由にカスタムできます。
スクリーンショット 2025-03-20 18.53.22.png

メッセージテンプレート作成

認証時に送られてくるメールをオシャレにしましょう!
スクリーンショット 2025-03-20 21.11.09.png

メッセージテンプレートを設定します。
スクリーンショット 2025-03-20 20.53.50.png

E メールメッセージではhtmlを設定できます。
スクリーンショット 2025-03-20 20.55.09.png

E メールメッセージの例

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>認証コードのご案内</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      background-color: #f5f5f5;
      color: #333333;
      margin: 0;
      padding: 0;
    }
    .container {
      background-color: #ffffff;
      max-width: 600px;
      margin: 40px auto;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 6px rgba(0,0,0,0.1);
    }
    .header {
      text-align: center;
      padding-bottom: 20px;
      border-bottom: 1px solid #eeeeee;
    }
    .header h1 {
      color: #333333;
      font-size: 24px;
    }
    .content {
      margin: 20px 0;
      font-size: 16px;
      line-height: 1.6;
    }
    .code {
      font-size: 32px;
      font-weight: bold;
      color: #4CAF50;
      text-align: center;
      margin: 20px 0;
      letter-spacing: 4px;
    }
    .footer {
      font-size: 12px;
      color: #999999;
      text-align: center;
      border-top: 1px solid #eeeeee;
      padding-top: 20px;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>「アプリ名」へようこそ!</h1>
    </div>
    <div class="content">
      <p>以下の認証コードをアプリ内で入力してください。</p>
      <div class="code">{####}</div>
      <p>認証コードの有効期限にご注意ください。</p>
    </div>
    <div class="footer">
      ※このメールアドレスは送信専用となっております。<br>
      ご返信いただいてもお答えできませんのでご了承ください。
    </div>
  </div>
</body>
</html>

アプリ側

以下の実装をします。

  • OAuthを使ったソーシャルログイン
    • Google認証によるログイン
    • Apple IDによるログイン
  • 認証情報の安全な管理
    • トークンの安全な保存(flutter_secure_storage使用)
    • アクセストークンの有効期限管理
    • リフレッシュトークンによる自動再認証
  • ユーザー情報の取得
    • メールアドレス、ニックネームなどのプロフィール情報の取得
    • OAuthプロバイダーからのユーザー情報の取得
  • セッション管理
    • ログイン状態の確認機能
    • 適切なセッション終了(ログアウト)処理

必要な設定

OAuthフローを使ったCognito認証とログアウトを正しく機能させるためには、iOS(Info.plist)とAndroid(AndroidManifest.xml)の両方に適切な設定が必要です。

iOS側

<!-- Info.plist -->
..

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>com.example.app</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.example.app</string>
    </array>
  </dict>
</array>

..

※CFBundleURLSchemesの値は、CognitoConfig.redirectUriで指定しているURLスキーム部分(com.example.app)と一致させる必要があります。

Android側

<!-- AndroidManifest.xml -->
..
      
      <!-- OAuth用のディープリンク設定 -->
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
          android:scheme="com.example.app"
          android:host="callback" />
      </intent-filter>

..

ライブラリ

以下のライブラリを使って実装しました。

flutter_secure_storage: ^9.0.0
flutter_appauth: ^9.0.0
http: ^1.1.0

configクラス

適当な場所に以下を定義します。(例:my_app/core/config/cognito_config.dart)

class CognitoConfig {
  static const String clientId = '';
  static const String hostedUiDomain = ''; // Cognitoのドメイン https://を抜いて
  static const String redirectUri = '';  // CognitoのリダイレクトURI
} 

clientIdはここ
スクリーンショット 2025-03-20 20.10.53.png

hostedUiDomainはここ(※https//を抜いて)
スクリーンショット 2025-03-20 20.13.27.png

redirectUriはここ
スクリーンショット 2025-03-20 20.15.23.png

認証処理をまとめたクラス

適当な場所に以下を定義します。(例:my_app/services/auth_service.dart)

認証処理

import 'package:my_app/core/config/cognito_config.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_appauth/flutter_appauth.dart';

class Result {
  final bool success;
  final String message;

  Result({
    required this.success,
    this.message = '',
  });
}

class AuthService {
  static final _storage = FlutterSecureStorage();
  static final _appAuth = FlutterAppAuth();

  // ============================
  // = 認証に関わるコード =
  // ============================
  static Future<Result> authenticateWithCognito() async {
    try {
      final result = await _appAuth.authorizeAndExchangeCode(
        AuthorizationTokenRequest(
          CognitoConfig.clientId,
          CognitoConfig.redirectUri,
          serviceConfiguration: AuthorizationServiceConfiguration(
            authorizationEndpoint: 'https://${CognitoConfig.hostedUiDomain}/oauth2/authorize',
            tokenEndpoint: 'https://${CognitoConfig.hostedUiDomain}/oauth2/token',
          ),
          scopes: ['openid', 'email', 'profile'],
          additionalParameters: {
            'lang': 'ja',
          },
        ),
      );
      
      await _persistTokens({
        'id_token': result.idToken,
        'access_token': result.accessToken,
        'refresh_token': result.refreshToken,
      });
      return Result(success: true, message: 'ログインしました');
    } catch (e) {
      print('認証エラー: $e');
      return Result(success: false, message: 'ログインに失敗しました');
    }
  }

  // ============================
  // = トークン保存に関わるコード =
  // ============================
  static Future<void> _persistTokens(Map<String, dynamic> tokens) async {
    await _storage.write(key: 'id_token', value: tokens['id_token']);
    await _storage.write(key: 'access_token', value: tokens['access_token']);
    await _storage.write(key: 'refresh_token', value: tokens['refresh_token']);
  }
  
  ..
}

その他の便利な関数

..

  // ===========================
  // = サインアウトに関わるコード =
  // ===========================
  static Future<Result> signOut() async {
    try {
      // 1. OAuthセッションのクリア
      try {
        final tokens = await getTokens();
        final idToken = tokens['id_token'];
        
        if (idToken != null) {
          // AppAuthのセッションをクリア
          await _appAuth.endSession(
            EndSessionRequest(
              idTokenHint: idToken,
              postLogoutRedirectUrl: CognitoConfig.redirectUri,
              serviceConfiguration: AuthorizationServiceConfiguration(
                authorizationEndpoint: 'https://${CognitoConfig.hostedUiDomain}/oauth2/authorize',
                tokenEndpoint: 'https://${CognitoConfig.hostedUiDomain}/oauth2/token',
                endSessionEndpoint: 'https://${CognitoConfig.hostedUiDomain}/logout',
              ),
              additionalParameters: {
                'client_id': CognitoConfig.clientId,
                'logout_uri': CognitoConfig.redirectUri,
              },
            ),
          );
          print('OAuthセッションをクリアしました');
        }
      } catch (e) {
        print('OAuthセッションクリアエラー: $e');
        // エラーが発生してもログアウト処理は続行
      }
      
      // 2. 保存されたセッション情報を削除
      await _storage.deleteAll();
      print('ログアウト完了: ストレージをクリア');
      return Result(success: true, message: 'サインアウトに成功しました');
      
    } catch (e) {
      print('サインアウトエラー: $e');
      // エラーが発生してもストレージは削除を試みる
      await _storage.deleteAll();
      return Result(success: false, message: 'サインアウトに失敗しました');
    }
  }


  // ============================
  // = ログイン状態確認に関わるコード =
  // ============================ 
  static Future<Result> isSignin() async {
    try {
      final tokens = await getTokens();
      final accessToken = tokens['access_token'];
      
      if (accessToken == null) {
        return Result(success: false, message: 'ログインが必要です');
      }

      try {
        // アクセストークンでユーザー情報を取得
        await getUserInfo(accessToken);
        return Result(success: true, message: 'ログイン中');
      } catch (e) {
        // アクセストークンが無効な場合、リフレッシュを試みる
        final refreshToken = tokens['refresh_token'];
        if (refreshToken != null) {
          try {
            final newTokens = await refreshTokens(refreshToken);
            await _persistTokens(newTokens);
            return Result(success: true, message: 'トークンを更新しました');
          } catch (e) {
            await signOut(); // リフレッシュに失敗した場合はログアウト
            return Result(success: false, message: '再ログインが必要です');
          }
        }
      }
      
      return Result(success: false, message: 'ログインが必要です');
    } catch (e) {
      return Result(success: false, message: 'エラーが発生しました');
    }
  }

  static Future<Map<String, String?>> getTokens() async {
    return {
      'id_token': await _storage.read(key: 'id_token'),
      'access_token': await _storage.read(key: 'access_token'),
      'refresh_token': await _storage.read(key: 'refresh_token'),
    };
  }

  static Future<Map<String, dynamic>> refreshTokens(String refreshToken) async {
    try {
      // FlutterAppAuthを使用してトークンをリフレッシュ
      final TokenResponse? response = await _appAuth.token(
        TokenRequest(
          CognitoConfig.clientId,
          CognitoConfig.redirectUri,
          refreshToken: refreshToken,
          grantType: 'refresh_token',
          serviceConfiguration: AuthorizationServiceConfiguration(
            authorizationEndpoint: 'https://${CognitoConfig.hostedUiDomain}/oauth2/authorize',
            tokenEndpoint: 'https://${CognitoConfig.hostedUiDomain}/oauth2/token',
          ),
        ),
      );
      
      if (response != null && response.accessToken != null) {
        // 更新されたトークン情報を返す
        return {
          'id_token': response.idToken,
          'access_token': response.accessToken,
          'refresh_token': response.refreshToken ?? refreshToken, // 新しいリフレッシュトークンがなければ古いものを使用
        };
      }
      
      throw Exception('トークンの更新に失敗しました: レスポンスが空です');
    } catch (e) {
      print('トークン更新エラー: $e');
      throw Exception('トークンの更新に失敗しました: $e');
    }
  }

  static Future<Map<String, dynamic>> getUserInfo(String accessToken) async {
    final response = await http.get(
      Uri.parse('https://${CognitoConfig.hostedUiDomain}/oauth2/userInfo'),
      headers: {
        'Authorization': 'Bearer $accessToken',
      },
    );

    if (response.statusCode == 200) {
      return json.decode(response.body);
    }
    throw Exception('ユーザー情報の取得に失敗しました');
  }

..

認証の呼び出し先

以下のように適当な画面で呼び出します。

import 'package:my_app/services/auth_service.dart';

..
..

PrimaryButton(
  text: '認証する',
  onPressed: () async {
    final result = await AuthService.authenticateWithCognito();
    if (result.success) {
      print("認証成功")
    } else {
      print("認証失敗")
    }
  },
)

..

ここまで実装すると、以下の認証が表示されるようになるはずです。

Simulator Screenshot - iPhone 15 Pro - 2025-03-21 at 13.29.29.png

まとめ

Amazon Cognitoのマネージドログインを活用することで、セキュアかつスムーズなログイン体験を簡単にアプリに導入できました。
複雑なOAuth2.0認証の処理はAWSに任せつつ、ユーザー管理やトークン取得をシンプルに実装可能でした。ぜひ、いいね👍をお願いします。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?