🆔

Stripe Identity で Flutter アプリ上の本人確認を簡単にセキュアに実装

2024/12/03に公開

この記事は、Flutter 大学アドベントカレンダー 20243日目の記事です。


こんにちは!本業ではJavaで、個人事業でFlutterのアプリを作っている「とむ」です。(Threads, Wantedly

今回は、Flutterアプリで本人確認機能を実装する方法について、Stripe Identityを使った簡単かつセキュアな実装方法をご紹介します。

この記事を読むと、Flutterアプリでユーザーに本人確認をお願いしたい時に、Stripe が私たちに代わって確認データを適切に保存・削除してくれるため、法令遵守の負荷を軽減することができます。

技術系のブログは初めて書くので、もっとこうした方が良いなどアドバイスがあったらぜひコメント頂けますと幸いです!

Stripe Identity の特徴と選んだ理由

Stripe Identity は、Stripeが提供している本人確認サービスです。日本では2024年7月18日から一般公開されました。ユーザーが身分証明書や顔写真を撮影・アップロードする機能を提供し、その内容をStripe側でチェック、安全に管理・削除まで行なってくれます。

https://stripe.com/jp/identity

公式ホームページにたくさんの特徴が紹介されていますが、特に個人的にいいなと思った点は以下の通りです。

  1. 高機能な本人確認機能を簡単に導入できるので、低コードで済む
  2. Stripeが確認データを保存するため、セキュリティ面で任せられる
  3. 100カ国以上の政府発行のIDと自撮り写真の照合にも対応でき、なりすまし防止に効果的
  4. 機械学習を使ったIDと第三者記録のマッチングで、本人確認の精度を高められる
  5. 料金体系がちゃんと提示されているので、導入しやすい

コンプライアンス面の負担を考えると、ユーザーの個人情報をできるかぎり管理したくないため、その負担を Stripe側に任せられる Stripe Identity はとても魅力的でした。

TRUSTDOCK や iTrust といった日本でも有名な本人確認サービスがあります。しかし、料金体系がホームページ上で掲載されていない点や、自分だけで導入が完結しないため、導入のハードルが高い印象を受けておりました。(どちらかというと、大手法人がターゲットなのかもしれませんが)

一方で、Stripe Identity は料金も確認1回あたり300円(12月3日時点)と明記されており、コストも算出しやすく、ドキュメントも豊富のため、個人でアプリを作ってセキュリティ面を担保するのにも適しているなと思い、導入しました。(しかも、最初の50回までは無料!)

Flutter での実装

ここからは、実際の実装内容とその画面を紹介します。

前提

- flutter: 3.19.6
- Dart: 3.3.4
- flutter_stripe: ^11.1.0
- cloud_functions: ^5.1.0

用意するファイル

  1. submitIdentification.dart:本人確認開始ボタンを表示する画面
  2. stripe.ts:Stripe Identity API を呼び出すCloud Functions

クライアントサイド(submitIdentification.dart)

submitIdentification.dart
import 'package:flutter/material.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:url_launcher/url_launcher.dart';

class SubmitIdentification extends StatefulWidget {
  
  _SubmitIdentificationState createState() => _SubmitIdentificationState();
}

class _SubmitIdentificationState extends State<SubmitIdentification> {
  bool isVerified = false;

  
  void initState() {
    super.initState();
    getVerificationStatus();
  }

  void getVerificationStatus() async {
    // 本人確認状態を取得するロジックを実装しています。
    if (本人確認済みの場合) {
        isVerified = true;
    } 
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('本人確認'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "本人確認のためのお手続きをお願いします。",
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _startVerification,
              child: Text(
                isVerified ? "本人確認書類を更新する" : "本人確認を開始する",
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _startVerification() async {
    if (isVerified) {
        // ここは任意です
        // すでに本人確認を実施済みの場合、
        // 新しい本人情報を登録するかどうかを
        // AlertDialog で確認しています。
        // Yesの場合、処理を進めます。
    }
    try {
      // Stripe Identity API をリクエストします。
      final HttpsCallable callable = FirebaseFunctions.instance.httpsCallable('createVerificationSession');

      // 本人確認書類の検証URLを取得します。
      final result = await callable.call();
      final String verificationUrl = result.data['url'];

      // verificationUrlが空でなく、かつURLを開くことができる場合、
      // launchUrlを使用してURLを開きます。そうでない場合は例外を投げています。
      if (await canLaunch(verificationUrl)) {
        await launch(verificationUrl);
      } else {
        throw 'Could not launch $verificationUrl';
      }
    } catch (e) {
      print('エラーが発生しました: $e');
      // エラーハンドリングを実装
    }
  }
}

サーバーサイド(stripe.ts)

stripe.ts
export const createVerificationSession = functions.https.onCall(async (data, context) => {
  try {
    // Stripe Identityの createVerificationSession を呼び出して、認証セッションを作成します。
    const verificationSession = await stripe.identity.verificationSessions.create({
      type: 'document',
    });

    // 認証セッションのための一時キー(エフェメラルキー)を作成します。
    const ephemeralKey = await stripe.ephemeralKeys.create(
      { verification_session: verificationSession.id },
      { apiVersion: '${利用しているAPIのバージョン}' }
    );

    // 認証セッションID、一時キーのシークレット、認証セッションのURLをクライアントに返します。
    return {
      verificationSessionId: verificationSession.id,
      ephemeralKeySecret: ephemeralKey.secret,
      url: verificationSession.url
    };
  } catch (error) {
    console.error(`認証セッションの作成中にエラーが発生しました: ${error}`);
    throw new functions.https.HttpsError('internal', `認証セッションの作成中にエラーが発生しました: ${error}`);
  }
});

実際の見え方

上記の実装により、開始ボタンを押すだけでStripe Identityの本人確認プロセスが起動し、本人確認書類の撮影から提出までをスムーズに実行できます。

stripe identity matenu app.gif
メープルマニア君は政府公式の本人確認書類ではないため、無事認証されませんでした。

まだ実装途中ですが、なりすまし防止のために顔認証を挟むこともできるため、今後追加していく予定です。
セキュリティの専門の方にもレビューをいただいて、必要に応じてより強固にして、ユーザーが安心して使えるアプリに仕上げたいなと思います。

所感

Flutterアプリ開発歴1年目の私でも、思っていたよりも簡単に実装できました!

実際の書類を提出する操作も、正しい書類でないと不正な書類として認識されなかったり、文字が読み取れなかったり、画像が鮮明ではないこともリアルタイムで表示されるため、不精緻な写真が送られることを減らすことができます。

前述の通り、検証1回につき300円かかるので、本人確認がうまくいかないと支出が増えるので、Stripe側で精度を高めてくれるのはとてもありがたいです。

何よりユーザーの個人情報を自身で管理することでのセキュリティリスクを抑えられるので、運用の負担も減らせるの高ポイントです。

さらに、導入検討前からコストが明確であることは収支計画を立てやすいのも、利点だなと感じました。

とはいえ、まだユーザーの利便性を高める余地はあるので、Ionic Japan User Group 代表の榊原さんが紹介しているStripe Identityを実装する上での4つの注意事項を参考に磨き上げたいと思っています。

余談

僕が Stripe Identity を知った時は、まだ日本で一般公開されておらず、招待制でした。

こんな個人のアプリ開発者は門前払いだろうけど、ダメもとで問い合わせたら、プロダクトチームの方の心意気で招待してくださり、以降、Stripe 大好きになりました!(ちょろい)

今では一般公開され、誰でも使えるようになったので、ぜひ Stripe Identity を試してみてください!

(願わくば、Stripe Connect アカウントの本人確認書類の収集にもStripe Identityを埋め込む機能が使えるようになればいいなあ。。。)

Discussion