メインコンテンツまでスキップ

あなたの Flutter アプリケーションに認証 (Authentication) を追加する

このチュートリアルでは、Logto を Flutter アプリケーションに統合する方法を紹介します。

ヒント:
  • SDK パッケージは pub.dev と Logto の GitHub リポジトリ で利用可能です。
  • サンプルプロジェクトは Flutter material を使用して構築されています。pub.dev と私たちの GitHub リポジトリ で見つけることができます。
  • SDK は Android と iOS プラットフォームのみと互換性があります。
  • SDK v1.x は Dart 2.x と互換性があります。SDK v2.x の場合、Dart のバージョンを 3.x 以上に更新する必要があります。

前提条件

  • Logto Cloud アカウントまたは セルフホスト Logto
  • Logto ネイティブアプリケーションが作成されていること。
  • Flutter または Dart の開発環境。

インストール

logto_dart_sdk package を pub パッケージマネージャーを使用して直接インストールできます。 プロジェクトのルートで次のコマンドを実行してください:

flutter pub get logto_dart_sdk

モジュール

logto_dart_sdk には 2 つの主要なモジュールが含まれています:

  • logto_core.dart このコアモジュールは、Logto SDK の基本的な機能とインターフェースを提供します。

  • logto_client.dart このクライアントモジュールは、Logto サーバーと対話するための高レベルの Logto クライアントクラスを提供します。

依存関係と設定

この SDK には以下の依存関係があり、一部は追加の設定が必要です:

flutter_secure_storage

flutter_secure_storage を使用して、クロスプラットフォームの永続的な安全なトークンストレージを実装しています。

  • iOS では Keychain が使用されます
  • Android では AES 暗号化が使用されます。

Android バージョンの設定

プロジェクトの android/app/build.gradle ファイルで android:minSdkVersion を 18 に設定します。

build.gradle
  android {
...

defaultConfig {
...
minSdkVersion 18
...
}
}

自動バックアップの無効化

デフォルトでは、Android は Google Drive にデータを自動的にバックアップする場合があります。これにより、例外 java.security.InvalidKeyException:Failed to unwrap key が発生する可能性があります。

これを避けるために、アプリの自動バックアップを無効にするか、FlutterSecureStorage から sharedprefs を除外することができます。

  1. 自動バックアップを無効にするには、アプリのマニフェストファイルに移動し、android:allowBackupandroid:fullBackupContent 属性を false に設定します。

    AndroidManifest.xml
    <manifest ... >
    ...
    <application
    android:allowBackup="false"
    android:fullBackupContent="false"
    ...
    >
    ...
    </application>
    </manifest>

  2. FlutterSecureStorage から sharedprefs を除外します。

    アプリの android:fullBackupContent を無効にするのではなく保持する必要がある場合は、バックアップから sharedprefs ディレクトリを除外できます。 詳細は Android ドキュメント を参照してください。

    AndroidManifest.xml ファイルで、次の例に示すように <application> 要素に android:fullBackupContent 属性を追加します。この属性は、バックアップルールを含む XML ファイルを指します。

    AndroidManifest.xml
    <application ...
    android:fullBackupContent="@xml/backup_rules">
    </application>

    res/xml/ ディレクトリに @xml/backup_rules という名前の XML ファイルを作成します。このファイルに <include><exclude> 要素を使用してルールを追加します。次のサンプルは、device.xml を除くすべての共有設定をバックアップします:

    @xml/backup_rules
    <?xml version="1.0" encoding="utf-8"?>
    <full-backup-content>
    <exclude domain="sharedpref" path="FlutterSecureStorage"/>
    </full-backup-content>

詳細については flutter_secure_storage を確認してください。

flutter_web_auth

flutter_web_auth は Logto の flutter SDK の背後で使用されています。ユーザーを認証 (Authentication) するために、その webview ベースのインターフェースに依存しています。

注記:

このプラグインは iOS 12+ および macOS 10.15+ では ASWebAuthenticationSession を、iOS 11 では SFAuthenticationSession を、Android では Chrome Custom Tabs を使用し、Web では新しいウィンドウを開きます。

Android でのコールバック URL の登録

Logto のサインインウェブページからのコールバック URL をキャプチャするために、AndroidManifest.xml ファイルにサインイン redirectUri を登録する必要があります。

AndroidManifest.xml
<activity android:name="com.linusu.flutter_web_auth.CallbackActivity" android:exported="true">
<intent-filter android:label="flutter_web_auth">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="io.logto"/>
</intent-filter>
</activity>

http.dart

SDK がネットワークリクエストを行う必要があるため、HTTP クライアントを SDK に渡す必要があります。デフォルトの http.Clienthttp.dart から使用するか、カスタム設定で独自の http.Client を作成できます。


import 'package:http/http.dart' as http;

統合

LogtoClient を初期化する

logto_dart_sdk パッケージをインポートし、アプリケーションのルートで LogtoClient インスタンスを初期化します。

lib/main.dart
import 'package:logto_dart_sdk/logto_dart_sdk.dart';
import 'package:http/http.dart' as http;

void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);


Widget build(BuildContext context) {
return const MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(title: 'Logto Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;


State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
late LogtoClient logtoClient;

void render() {
// state change
}

// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>"
);

void _init() {
logtoClient = LogtoClient(
config: logtoConfig,
httpClient: http.Client(), // Optional http client
);
render();
}


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

// ...
}

サインインを実装する

詳細に入る前に、エンドユーザーの体験について簡単に説明します。サインインプロセスは次のように簡略化できます:

  1. あなたのアプリがサインインメソッドを呼び出します。
  2. ユーザーは Logto のサインインページにリダイレクトされます。ネイティブアプリの場合、システムブラウザが開かれます。
  3. ユーザーがサインインし、あなたのアプリにリダイレクトされます(リダイレクト URI として設定されています)。

リダイレクトベースのサインインについて

  1. この認証 (Authentication) プロセスは OpenID Connect (OIDC) プロトコルに従い、Logto はユーザーのサインインを保護するために厳格なセキュリティ対策を講じています。
  2. 複数のアプリがある場合、同じアイデンティティプロバイダー (Logto) を使用できます。ユーザーがあるアプリにサインインすると、Logto は別のアプリにアクセスした際に自動的にサインインプロセスを完了します。

リダイレクトベースのサインインの理論と利点について詳しく知るには、Logto サインイン体験の説明を参照してください。


始める前に、アプリケーションのために管理コンソールでリダイレクト URI を追加する必要があります。

Logto コンソールのアプリケーション詳細ページに切り替えましょう。リダイレクト URI io.logto://callback を追加し、「変更を保存」をクリックします。

Logto コンソールのリダイレクト URI
  • iOS の場合、ASWebAuthenticationSession クラスがリダイレクト URI を登録しているかどうかに関係なくリッスンするため、リダイレクト URI スキームは実際には重要ではありません。
  • Android の場合、リダイレクト URI スキームは AndroidManifest.xml ファイルに登録する必要があります。

リダイレクト URI が設定された後、ページにサインインボタンを追加し、logtoClient.signIn API を呼び出して Logto サインインフローを起動します:

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
// ...
final redirectUri = 'io.logto://callback';


Widget build(BuildContext context) {
// ...

Widget signInButton = TextButton(
onPressed: () async {
await logtoClient.signIn(redirectUri);
render();
},
child: const Text('Sign In'),
);

return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
signInButton,
],
),
),
);
}
}

サインアウトを実装する

次に、メインページにサインアウトボタンを追加して、ユーザーがアプリケーションからサインアウトできるようにします。

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
// ...


Widget build(BuildContext context) {
// ...

Widget signOutButton = TextButton(
onPressed: () async {
await logtoClient.signOut();
render();
},
child: const Text('Sign Out'),
);

return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
signInButton,
signOutButton,
],
),
),
);
}
}

認証 (Authentication) ステータスを処理する

Logto SDK は、認証 (Authentication) ステータスを確認するための非同期メソッドを提供しています。このメソッドは logtoClient.isAuthenticated です。このメソッドはブール値を返し、ユーザーが認証 (Authentication) されている場合は true、そうでない場合は false です。

この例では、認証 (Authentication) ステータスに基づいてサインインボタンとサインアウトボタンを条件付きでレンダリングします。では、状態変更を処理するために Widget の render メソッドを更新しましょう:

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
// ...
bool? isAuthenticated = false;

void render() {
setState(() async {
isAuthenticated = await logtoClient.isAuthenticated;
});
}


Widget build(BuildContext context) {
// ...

return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
isAuthenticated == true ? signOutButton : signInButton,
],
),
),
);
}
}

チェックポイント: アプリケーションをテストする

これで、アプリケーションをテストできます:

  1. アプリケーションを実行すると、サインインボタンが表示されます。
  2. サインインボタンをクリックすると、SDK がサインインプロセスを初期化し、Logto のサインインページにリダイレクトされます。
  3. サインインすると、アプリケーションに戻り、サインアウトボタンが表示されます。
  4. サインアウトボタンをクリックしてローカルストレージをクリアし、サインアウトします。

ユーザー情報を取得する

ユーザー情報を表示する

ユーザーの情報を表示するには、logtoClient.idTokenClaims ゲッターを使用できます。例えば、Flutter アプリで:

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
// ...


Widget build(BuildContext context) {
// ...

Widget getUserInfoButton = TextButton(
onPressed: () async {
final userClaims = await logtoClient.idTokenClaims;
print(userInfo);
},
child: const Text('Get user info'),
);

return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
isAuthenticated == true ? signOutButton : signInButton,
isAuthenticated == true ? getUserInfoButton : const SizedBox.shrink(),
],
),
),
);
}
}

追加のクレームをリクエストする

client.idTokenClaims から返されるオブジェクトに一部のユーザー情報が欠けていることがあります。これは、OAuth 2.0 と OpenID Connect (OIDC) が最小特権の原則 (PoLP) に従うように設計されており、Logto はこれらの標準に基づいて構築されているためです。

デフォルトでは、限られたクレーム (Claim) が返されます。より多くの情報が必要な場合は、追加のスコープ (Scope) をリクエストして、より多くのクレーム (Claim) にアクセスできます。

備考:

「クレーム (Claim)」はサブジェクトについての主張であり、「スコープ (Scope)」はクレーム (Claim) のグループです。現在のケースでは、クレーム (Claim) はユーザーに関する情報の一部です。

スコープ - クレーム (Claim) 関係の非規範的な例を示します:

ヒント:

「sub」クレーム (Claim) は「サブジェクト (Subject)」を意味し、ユーザーの一意の識別子(つまり、ユーザー ID)です。

Logto SDK は常に 3 つのスコープ (Scope) をリクエストします:openidprofile、および offline_access

追加のスコープをリクエストするには、スコープを LogtoConfig オブジェクトに渡すことができます。例えば:

lib/main.dart
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: ["email", "phone"],
);

SDK パッケージには、事前定義されたスコープを使用するのに役立つ組み込みの LogtoUserScope 列挙型も提供されています。

// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScope.email.value, LogtoUserScope.phone.value],
);

ネットワークリクエストが必要なクレーム (Claims)

ID トークンの肥大化を防ぐために、一部のクレーム (Claims) は取得するためにネットワークリクエストが必要です。例えば、custom_data クレームはスコープで要求されてもユーザーオブジェクトに含まれません。これらのクレームにアクセスするには、 logtoClient.getUserInfo() メソッドを使用できます

lib/main.dart
class _MyHomePageState extends State<MyHomePage> {
// ...


Widget build(BuildContext context) {
// ...

Widget getUserInfoButton = TextButton(
onPressed: () async {
final userInfo = await logtoClient.getUserInfo();
print(userInfo);
},
child: const Text('Get user info'),
);

return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText('My Demo App'),
isAuthenticated == true ? signOutButton : signInButton,
isAuthenticated == true ? getUserInfoButton : const SizedBox.shrink(),
],
),
),
);
}
}
このメソッドは、userinfo エンドポイントにリクエストを送信してユーザー情報を取得します。利用可能なスコープとクレームについて詳しくは、スコープとクレームのセクションを参照してください。

スコープとクレーム

Logto は OIDC の スコープとクレームの規約 を使用して、ID トークンおよび OIDC userinfo エンドポイント からユーザー情報を取得するためのスコープとクレームを定義します。「スコープ」と「クレーム」は、OAuth 2.0 および OpenID Connect (OIDC) 仕様からの用語です。

サポートされているスコープと対応するクレーム (Claims) のリストはこちらです:

openid

クレーム名タイプ説明ユーザー情報が必要か?
substringユーザーの一意の識別子いいえ

profile

クレーム名タイプ説明ユーザー情報が必要か?
namestringユーザーのフルネームいいえ
usernamestringユーザーのユーザー名いいえ
picturestringエンドユーザーのプロフィール写真の URL。この URL は、画像を含む Web ページではなく、画像ファイル(例えば PNG、JPEG、または GIF 画像ファイル)を指す必要があります。この URL は、エンドユーザーを説明する際に表示するのに適したプロフィール写真を特に参照するべきであり、エンドユーザーが撮影した任意の写真を参照するべきではありません。いいえ
created_atnumberエンドユーザーが作成された時間。時間は Unix エポック(1970-01-01T00:00:00Z)からのミリ秒数で表されます。いいえ
updated_atnumberエンドユーザーの情報が最後に更新された時間。時間は Unix エポック(1970-01-01T00:00:00Z)からのミリ秒数で表されます。いいえ

その他の 標準クレーム には、family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfo、および locale が含まれ、ユーザー情報エンドポイントを要求することなく profile スコープに含まれます。上記のクレームと異なる点は、これらのクレームは値が空でない場合にのみ返されることであり、上記のクレームは値が空の場合に null を返します。

注記:

標準クレームとは異なり、created_atupdated_at クレームは秒ではなくミリ秒を使用しています。

email

クレーム名タイプ説明ユーザー情報が必要か?
emailstringユーザーのメールアドレスいいえ
email_verifiedbooleanメールアドレスが確認済みかどうかいいえ

phone

クレーム名タイプ説明ユーザー情報が必要か?
phone_numberstringユーザーの電話番号いいえ
phone_number_verifiedboolean電話番号が確認済みかどうかいいえ

address

住所クレームの詳細については、OpenID Connect Core 1.0 を参照してください。

custom_data

クレーム名タイプ説明ユーザー情報が必要か?
custom_dataobjectユーザーのカスタムデータはい

identities

クレーム名タイプ説明ユーザー情報が必要か?
identitiesobjectユーザーのリンクされたアイデンティティはい
sso_identitiesarrayユーザーのリンクされた SSO アイデンティティはい

urn:logto:scope:organizations

クレーム名タイプ説明ユーザー情報が必要か?
organizationsstring[]ユーザーが所属する組織の IDいいえ
organization_dataobject[]ユーザーが所属する組織のデータはい

urn:logto:scope:organization_roles

クレーム名タイプ説明ユーザー情報が必要か?
organization_rolesstring[]ユーザーが所属する組織のロールで、<organization_id>:<role_name> の形式いいえ

パフォーマンスとデータサイズを考慮して、「ユーザー情報が必要か?」が「はい」の場合、クレームは ID トークンに表示されず、ユーザー情報エンドポイント のレスポンスで返されます。

API リソースと組織

まず 🔐 ロールベースのアクセス制御 (RBAC) を読むことをお勧めします。これにより、Logto の RBAC の基本概念と API リソースを適切に設定する方法を理解できます。

Logto クライアントを設定する

API リソースを設定したら、アプリで Logto を設定する際にそれらを追加できます:

lib/main.dart
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
// Add your API resources
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
);

各 API リソースには独自の権限 (スコープ) があります。

例えば、https://shopping.your-app.com/api リソースには shopping:readshopping:write の権限があり、https://store.your-app.com/api リソースには store:readstore:write の権限があります。

これらの権限を要求するには、アプリで Logto を設定する際にそれらを追加できます:

lib/main.dart
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
// Add your API resources' scopes
scopes: ["shopping:read", "shopping:write", "store:read", "store:write"]
);

スコープが API リソースとは別に定義されていることに気付くかもしれません。これは、OAuth 2.0 のリソースインジケーター が、リクエストの最終的なスコープはすべてのターゲットサービスでのすべてのスコープの直積になると指定しているためです。

したがって、上記のケースでは、Logto での定義からスコープを簡略化できます。両方の API リソースは、プレフィックスなしで readwrite スコープを持つことができます。その後、Logto の設定では:

lib/main.dart
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
resources: ["https://shopping.your-app.com/api", "https://store.your-app.com/api"],
// すべてのリソースで共有されるスコープ
scopes: ["read", "write"]
);

各 API リソースは、readwrite の両方のスコープを要求します。

注記:

API リソースで定義されていないスコープを要求しても問題ありません。例えば、API リソースに email スコープが利用できなくても、email スコープを要求できます。利用できないスコープは安全に無視されます。

サインインが成功すると、Logto はユーザーのロールに応じて適切なスコープを API リソースに発行します。

API リソースのアクセス トークンを取得する

特定の API リソースのアクセス トークンを取得するには、getAccessToken メソッドを使用できます:

lib/main.dart
Future<AccessToken?> getAccessToken(String resource) async {
var token = await logtoClient.getAccessToken(resource: resource);

return token;
}

このメソッドは、ユーザーが関連する権限を持っている場合に API リソースにアクセスするために使用できる JWT アクセス トークンを返します。現在キャッシュされているアクセス トークンが期限切れの場合、このメソッドは自動的にリフレッシュ トークンを使用して新しいアクセス トークンを取得しようとします。

組織のためのアクセス トークンを取得する

API リソースと同様に、組織のためのアクセス トークンを要求することもできます。これは、API リソース スコープではなく、組織スコープを使用して定義されたリソースにアクセスする必要がある場合に便利です。

組織 (Organization) が初めての場合は、🏢 組織 (マルチテナンシー) を読んで始めてください。

Logto クライアントを設定する際に、LogtoUserScope.Organizations スコープを追加する必要があります:

lib/main.dart
// LogtoConfig
final logtoConfig = const LogtoConfig(
endpoint: "<your-logto-endpoint>",
appId: "<your-app-id>",
scopes: [LogtoUserScopes.organizations.value]
);

ユーザーがサインインしたら、ユーザーのための組織トークンを取得できます:

// ユーザーに対する有効な組織 ID は、ID トークンのクレーム `organizations` にあります。
Future<AccessToken?> getOrganizationAccessToken(String organizationId) async {
var token = await logtoClient.getOrganizationToken(organizationId);

return token;
}

さらなる読み物

エンドユーザーフロー:認証 (Authentication) フロー、アカウントフロー、組織フロー コネクターを設定する あなたの API を保護する