こんにちは、株式会社Pentagonでアプリを開発している難波です。
前回、Amplify CLI x Amazon Cognito を用いた認証基盤構築について調査しましたが、弊社のようにデザイン品質にこだわりを持たせる場合、カスタマイズに制限がかかる点がネックであることがわかりました。
そのため、デザインカスタマイズ性が高い基盤構築を行えるよう、再調査を行いました。
【こんな人に読んで欲しい】
- AWSでの認証機能実装が求められているFlutterアプリ開発エンジニア
- AWSでのSNS認証が求められているFlutterアプリ開発エンジニア
- デザインにこだわりのある画面が求められるFlutterアプリ開発エンジニア
【この記事を読むメリット】
- Flutter x AWS の環境で認証機能が実現できる
- 認証基盤を独自開発しなくてよくなるため、開発工数が削減できる
- デザインをカスタマイズできるログイン画面が実装できる
【結論】
前回 とは異なり、AWSが提供する Amplify CLI を用いずに認証基盤を構築することができました。
また、Amplify UI を用いないことで、デザインへの訴求が必要となるログインUIにも対応できることがわかりました。
本記事を読み、デザイン自由度の高い認証機能をFlutterアプリへ導入していきましょう!
前提条件
本記事は Flutter x Cognito の構築を対象とし、Eメール認証およびソーシャルログイン(Googleアカウント)の実装までをゴールとします。
本記事はFlutterアプリに ソーシャルログイン機能実現にフォーカスするため、以下は対応済みの前提として説明を進めます。
- Flutter開発環境構築済みであること
- AWSアカウント登録済みであること
- Googleアカウント登録済みであること
- 本記事ではiOSのみ対応
「Amazon Cognitoの環境構築手順」ならびに「ソーシャルログインに必要なGoogleの設定」については公式デベロッパーガイドに従います。
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-identity-provider.html
アプリ構築
Cognito との認証連携を行うため、pubspec.yaml に以下を追加します。
pubspec.yaml
dependencies:
amplify_auth_cognito: ^1.0.1
amplify_flutter: ^1.0.1
flutterアプリケーションのルートに amplifyconfiguration.dart ファイルを新規追加します。
ソーシャルログインに必要な内容について定義します。
amplifyconfiguration.dart
const amplifyconfig = '''{
"auth": {
"plugins": {
"awsCognitoAuthPlugin": {
"CognitoUserPool": {
"Default": {
"PoolId": "ユーザープールID",
"AppClientId": "アプリケーションクライアントID",
"Region": "リージョン"
}
},
"Auth": {
"Default": {
"authenticationFlowType": "USER_SRP_AUTH",
"socialProviders": ["GOOGLE"],
"usernameAttributes": [],
"signupAttributes": [
"email"
],
"passwordProtectionSettings": {
"passwordPolicyMinLength": 8,
"passwordPolicyCharacters": []
},
"mfaConfiguration": "OFF",
"mfaTypes": [
"SMS"
],
"verificationMechanisms": [
"EMAIL"
],
"OAuth": {
"WebDomain": "アプリケーションクライアントのホストされたUIのドメイン",
"AppClientId": "アプリケーションクライアントID",
"SignInRedirectURI": "許可されているコールバックURL",
"SignOutRedirectURI": "許可されているサインアウトURL",
"Scopes": [
"email",
"openid"
]
}
}
}
}
}
}
}''';
アプリケーションを実装します。
記事投稿用のソースコードにつき、下記の状態となっております。参考にされる際、その点ご留意ください。
- 正常系以外の動作ではエラーが発生します
- ファイル分割をしていません
- リファクタリングしていません
main.dart
import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'package:flutter/material.dart';
import 'amplifyconfiguration.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_configureAmplify();
}
void _configureAmplify() async {
final auth = AmplifyAuthCognito();
await Amplify.addPlugin(auth);
try {
await Amplify.configure(amplifyconfig);
} on AmplifyAlreadyConfiguredException catch (e) {
safePrint(e);
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.grey,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final tabBarViews = [
const LoginScreen(),
const SignUpScreen(),
];
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabBarViews.length,
child: Scaffold(
appBar: AppBar(
title: const Text('HOME'),
bottom: const TabBar(
tabs: [
Tab(text: 'ログイン'),
Tab(text: 'サインアップ'),
],
),
),
body: TabBarView(children: tabBarViews),
),
);
}
}
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailTextController = TextEditingController();
final _passwordTextController = TextEditingController();
bool get isValid =>
_emailTextController.text.isNotEmpty &&
_passwordTextController.text.isNotEmpty;
void clear() {
_emailTextController.text = '';
_passwordTextController.text = '';
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: _emailTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('email'),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('password'),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: isValid
? () {
_signIn(
context,
signIn: Amplify.Auth.signIn(
username: _emailTextController.text,
password: _passwordTextController.text,
),
);
}
: null,
child: const Text('ログイン'),
),
const Divider(thickness: 2.0),
ElevatedButton(
onPressed: () {
_signIn(
context,
signIn: Amplify.Auth.signInWithWebUI(
provider: AuthProvider.google,
),
);
},
child: const Text('Google アカウント'),
),
],
),
);
}
Future<void> _signIn(
BuildContext context, {
required Future<SignInResult> signIn,
}) async {
try {
final result = await signIn;
if (result.isSignedIn) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('ログインに成功しました!'),
),
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const TopScreen(),
),
);
}
} catch (e) {
safePrint(e);
}
}
}
class SignUpScreen extends StatefulWidget {
const SignUpScreen({super.key});
@override
State<SignUpScreen> createState() => _SignUpScreenState();
}
class _SignUpScreenState extends State<SignUpScreen> {
final _emailTextController = TextEditingController();
final _passwordTextController = TextEditingController();
final _codeTextController = TextEditingController();
bool get isValid =>
_emailTextController.text.isNotEmpty &&
_passwordTextController.text.isNotEmpty;
int _step = 0;
@override
void dispose() {
super.dispose();
_emailTextController.dispose();
_passwordTextController.dispose();
_codeTextController.dispose();
}
@override
Widget build(BuildContext context) {
final steps = [
_signUpStep(context),
_codeStep(context),
];
return Container(
padding: const EdgeInsets.all(16.0),
child: steps[_step],
);
}
Widget _signUpStep(BuildContext context) {
return Column(
children: [
TextFormField(
controller: _emailTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('email'),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('password'),
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: isValid
? () async {
try {
final result = await Amplify.Auth.signUp(
username: _emailTextController.text,
password: _passwordTextController.text,
);
switch (result.nextStep.signUpStep) {
case AuthSignUpStep.confirmSignUp:
setState(() => _step++);
break;
case AuthSignUpStep.done:
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomeScreen(),
),
);
break;
}
} catch (e) {
safePrint(e);
}
}
: null,
child: const Text('サインアップ'),
),
],
);
}
Widget _codeStep(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextFormField(
controller: _codeTextController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('code'),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
final result = await Amplify.Auth.confirmSignUp(
username: _emailTextController.text,
confirmationCode: _codeTextController.text,
);
if (result.isSignUpComplete) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('認証に成功しました!'),
),
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomeScreen(),
),
);
}
},
child: const Text('認証'),
),
],
),
);
}
}
class TopScreen extends StatelessWidget {
const TopScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('トップページ'),
),
body: Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: Column(
children: [
const SizedBox(height: 16),
const Text('ここはログイン後のトップページです'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () async {
try {
await Amplify.Auth.signOut();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('ログアウトしました!'),
),
);
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const HomeScreen(),
),
);
} on AuthException catch (e) {
safePrint(e);
}
},
child: const Text('ログアウト'),
),
],
),
),
);
}
}
動作イメージ
「Google アカウント」を押します。
許可ダイアログの「続ける」を押します。
(初回のみGoogle認可画面が表示されます)
まとめ
FlutterアプリからAmazon Cognitoを利用することで、ソーシャルログイン(Google認証)を実現することができます。
また、Flutter標準Widgetのみを用いているため、バックエンド構築にAWSを利用した上で、デザイン自由度の高いアプリ開発ができることがわかりました。
本記事の内容をもとに、デザイン性の高いUIを認証フローに適用していきましょう!
おまけ
Flutterに関する知識を深めたい方には、『Flutterの特徴・メリット・デメリットを徹底解説』という記事がおすすめです。
この記事では、Flutter アプリ開発の基本から、flutter とは何か、そして実際のflutter アプリ 事例を通じて、その将来性やメリット、デメリットまで詳しく解説しています。
Flutterを使ったアプリ開発に興味がある方、またはその潜在的な可能性を理解したい方にとって、必見の内容となっています。
ぜひ一度ご覧ください。