この記事は #AsanaTogetherJP Advent Calendar 2021 と、Flutter Advent Calendar 2021 を
同時に提出してしまおうという 邪な 記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
ここまでお約束
Asanaのアドベントカレンダーを眺めていた際に、Asanaの方に「Developer向けの記事があまりないので、ぜひお願いします」
と言われたので調子に乗ってエントリーしました。
で、何をしようかなーと考えたときに、ちょうど最近趣味でFlutterを触り始めたので、
FlutterでAsanaをもっと便利に使えるアプリなんかを作ったら便利かなと思ったのがきっかけです。
Dart/Flutterについてはまだまだ勉強中ですので、あまり高度な内容は期待しないでください(とりあえず認証してAPIを呼び出せるようにすることが目標)
この記事で実現できるようになること
- Asanaを使ったモバイルアプリにおけるOAuth認証のフローを理解できる
- FlutterにおいてAsanaでOAuthを実装する際のヒントがわかる
実行環境/リソースについて
以下のバージョンで動作を確認しています
- OS: Windows11 Pro
- Dart: 2.14.4
- Flutter: 2.5.3
参照したリソース
何を実現したいか
OAuth認証でAsanaのアクセストークンを取得して、APIコールしたい
※OAuthについての説明はGoogleで検索してもらうのが良いかと思いますが、簡易的に今回利用するフローを掲載しておきます
- ① ユーザ認証をコール&アプリケーションの接続を許可
- FlutterのアプリからOAuthの認証URLを開き、Asanaの認証(ID/PWなど)を行ったのち、アプリケーションの接続を「許可」します
- ② リダイレクト
- 認証に成功すると、リダイレクトURLに指定したURLにリダイレクトされます
- リダイレクト時にURLにクエリパラメータでcodeが付与されています。このコードとクライアントID, シークレットを利用してBearerトークンを取得しますので取っておきます。
- ③ Bearerトークンを取得
- APIコールに利用するBearerトークンを取得します。
-
トークン交換エンドポイント
https://app.asana.com/-/oauth_token
にPOSTリクエストを行うことで取得できます
- ④ Bearerトークンを使用してAPIをコール
-
③
で取得したBearerトークンを使用してAPIコールをします - 利用可能なAPIはたくさんありますが、今回は一番簡単な
https://app.asana.com/api/1.0/users/me
をコールしてみます - そのほかのAPIについては公式ドキュメントを参照してください(プランによっては利用不可能なAPIもあります)
-
準備する
AsanaやFlutterのアプリで準備することを書いておきます
Asanaで準備するもの
AsanaのマイアプリのクライアントID, クライアントシークレットを取得します
AsanaのDeveloper Consoleを開いて、新しいアプリを作成します。
「アプリを作成」すると、クライアントID、クライアントシークレットが取得できるので、メモします
また、「リダイレクトURL」の部分には、「http://localhost:8080/
」を設定しておきます。(ここがミソ)
Flutterアプリで準備すること
webview_flutterを追加する
アプリ内でAsanaの画面を表示するために、「webview_flutter」を追加します。
名前の通り、アプリ内でWebViewを利用するためのWidgetです
Androidの場合の作業
API Level 28以降は、http通信がデフォルトで無効化されているため、AndroidManifestなどで明示的に有効化する必要があります
https://developer.android.com/training/articles/security-config#CleartextTrafficPermitted
AndroidManifest.xmlに「android:usesCleartextTraffic="true"」を追加すると無効化されているhttp通信が行えるようになりますが、
検証要素で使用しているだけですので上記の対策をすることをお勧めします。
実装する
暫定完成版のコードはこちらに記述しております。
ポイントだけ触れていきます
①ユーザ認証をコール&アプリケーションの接続を許可
ログイン/許可するときは、WebViewを使用して、認証エンドポイント https://app.asana.com/-/oauth_authorize
を開きます。
body: Container(
padding: const EdgeInsets.all(10),
child: _isBusy
? const WebView(
javascriptMode: JavascriptMode.unrestricted,
initialUrl:
// initialUrlに、認証エンドポイントのURLとClientId, redirect_uriなどを指定し、WebView内でAsanaの認証画面を開きます
'https://app.asana.com/-/oauth_authorize?response_type=code&client_id=【ClientId】&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2F',
)
: logined
? Column(
children: [
Image.network(response!.data.photo['image_128x128'])
],
)
: Text(token),
),
② リダイレクト
Asanaの認証に成功すると、リダイレクト先に指定されたアドレスに自動的にリダイレクトされます。
OAuthの設定でリダイレクト先をdeep linkなどにできないので、localhostにWebServerを立ててリダイレクトを受け付けるようにしました。
start() async {
server = await HttpServer.bind(
InternetAddress.loopbackIPv4, redirectServerPort,
shared: true);
server!.listen((request) async {
final uri = request.uri;
// WebViewにレスポンスを返す
request.response
..statusCode = 200
..headers.set('Content-Type', ContentType.html.mimeType);
await request.response.close();
// uriからcode/errorを取得
final code = uri.queryParameters['code'];
final error = uri.queryParameters['error'];
if (code != null) {
// 結果をStreamに書き込む
_controller.add(code);
} else if (error != null) {
_controller.add('');
_controller.addError(error);
}
});
}
③ Bearerトークンを取得
Codeが取れたら、次はTokenを取得します。
Tokenはトークンエンドポイント https://app.asana.com/-/oauth_token
へPOSTリクエストを送ることで取得できます。
Future<TokenResponse> get token async {
// Streamからcodeを取得
final tokenCode = await code;
// パラメータを設定し、POSTリクエストを送信する
final response = await http.post(tokenEndpoint, body: {
'grant_type': 'authorization_code', // 固定
'client_id': clientId, // client Idを指定
'client_secret': clientSecret, // client secretを指定
'redirect_uri': 'http://localhost:8080/', // redirect uriを指定
'code': tokenCode // リダイレクトパラメータから受けとったCodeを取得する
});
// Tokenをいい感じにParseしてくれる処理を書いている。(要はBearerトークンをとったりその他のパラメータを渡したりしている)
return TokenResponse.fromMap(json.decode(response.body));
}
完成品
完成した画面の動きを見てみましょう。
デザインも何も考えられていないので微妙ですが、やりたいことはできています!
(ユーザのアイコンに指定している画像を取得・表示できました!)
最後に
AsanaのOAuthとFlutterを使用した認証でAPIをコールできるようになりました。
作った仕組みを使用して、Asanaのオリジナルブラウザを作ってみたいと思っています。
実は、プライベートでもAsanaを家族で利用しており、共有の買い物リストやToDoリストとして使っていますが、
家計簿などにも使えないかな~などと企んでおります😎
なお、API・OAuth利用時は、利用規約をよく確認し、
用法・用量を守って 正しく利用しましょう。