ローソンデジタルイノベーション(LDI)でPMを担当している畑沢です。
先日、下記記事にて在宅勤務報告システムについてご紹介しました。
techblog.ldi.co.jp
今回は、Firebase Authentication × Microsoft Entra ID × Microsoft Graphを利用した認証処理と、実装中にちょっと困ったことをご紹介します。
FirebaseとMicrosoft Entra IDの連携
まず、コンソール上でFirebaseとMicrosoft Entra IDを連携します。
具体的な手順は下記ドキュメントに記載されているため詳細は割愛しますが、大きくは以下の流れになります。
- Firebaseコンソール上でAuthenticationを有効化し、リダイレクトURLをコピー
- Microsoft Entra IDでアプリ登録を行い、リダイレクトURLを設定しつつ、クライアントIDとシークレットを取得
- Firebaseに戻り、AuthenticationでクライアントIDとシークレットを設定
Flutterでログイン処理を実装
こちらも公式のドキュメントがありますが、参考までに実装イメージを載せておきます。
import 'package:firebase_auth/firebase_auth.dart'; class FirebaseAuthService { Future<String> login() async { final microsoftProvider = MicrosoftAuthProvider(); microsoftProvider.addScope("User.Read"); // 認証処理 final content = await FirebaseAuth.instance.signInWithPopup(microsoftProvider); final credential = content.credential; if (credential != null && credential.accessToken != null) { return credential.accessToken; } return ""; } }
認証後にaccessTokenを取得しているのは、後のMicrosoft Graphからデータを取得する際に利用するためです。
Microsoft Graphからユーザ情報を取得
認証時に取得したaccessTokenとMicrosoft Graphを利用して、ユーザーの属性情報を取得します。こちらも実装イメージを載せておきます。
import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:http/http.dart'; class UserInfoService { static const String _microsoftGraphDomain = 'graph.microsoft.com'; static const Map<String, dynamic> _queryUserAttrs = { '\$select': 'id,employeeId,displayName,surname,givenName,jobTitle,department' }; /// ユーザー情報取得 Future<UserInfo> getUserInfo(String accessToken) async { if (accessToken.isNotEmpty) { // ユーザー情報 final response = await _callApi(accessToken, 'v1.0/me', _queryUserAttrs); final data = jsonDecode(response.body) as Map<String, dynamic>; return UserInfo( userId: data["id"] ?? "", employeeId: data["employeeId"] ?? "", department: data["department"] ?? "", displayName: data["displayName"] ?? "", firstName: data["givenName"] ?? "", lastName: data["surname"] ?? "", jobTitle: data["jobTitle"] ?? ""); } return UserInfo.notLoginUser(); } /// API呼び出し処理 Future<Response> _callApi( String accessToken, String path, Map<String, dynamic>? query) async { var url = Uri.https(_microsoftGraphDomain, path, query); return http.get(url, headers: {'Authorization': 'Bearer ${accessToken}'}); } }
accessTokenをHTTPリクエストヘッダーに指定してユーザー情報を取得します。
困ったこと
システムの要件として、在宅勤務申請を上長が承認するというものがありました。
そのため、ログイン者が承認すべき社員を特定する必要があり、Microsoft GraphのdirectReportsを利用して情報取得することにしました。
directReports を一覧表示する - Microsoft Graph v1.0 | Microsoft Learn
/// メンバー情報取得 Future<List<UserInfo>> getDirectReports(String accessToken, String userId) async { if (accessToken.isNotEmpty) { // ユーザー情報 final response = await _callApi(accessToken, 'v1.0/users/$userId/directReports', _queryUserAttrs); final data = jsonDecode(response.body) as Map<String, dynamic>; final List<dynamic> list = data["value"]; final directReports = List<UserInfo>.generate( list.length, (index) => UserInfo( userId: list[index]["id"] ?? "", staffNumber: list[index]["employeeId"] ?? "", department: list[index]["department"] ?? "", displayName: list[index]["displayName"] ?? "", firstName: list[index]["givenName"] ?? "", lastName: list[index]["surname"] ?? "", jobTitle: list[index]["jobTitle"] ?? "", directReports: List<UserInfo>.empty())); return directReports; } return List<UserInfo>.empty(); }
この際、困ったことが2点発生しました。
- 自身以外では、基本属性しか取得できない($selectの指定が効かない)
- 自身のdirectReportsは取得できるが、自身以外のdirectReportsが取得できない(403 Forbiddenとなる)
自身以外のdirectReportsを取得することについての補足ですが、上長はレポートライン上にいる全メンバーのデータを参照できるという要件もあり、自身のdirectReportsで取得したメンバーだけでなく、その先にいるメンバー情報も取得するため、自身以外のdirectReportsを取得する必要がありました。
解決方法
こちらの事象はMicrosoft Entra IDで登録したアプリの「APIのアクセス許可」で、Microsoft Graphに対してUser.Read.Allのアクセス許可を行うことで解決しました。
Microsoft Graph のアクセス許可のリファレンス - Microsoft Graph | Microsoft Learn
感想
運用が煩雑になるため、アカウント管理を独自に行うことは極力避けたいという想いがあり、Microsoftアカウントと連携ができないかというところからスタートしました。
簡単にできないかなと期待はしていたのですが、想像以上にFirebaseとMicrosoftとの連携が簡単で驚きました。
Microsoft Graphからのユーザー情報取得もシンプルだったのですが、アクセス許可周りがきめ細やかになっているため、仕組みを理解するために時間を要しました。(今使っているもの以外の理解は不十分です…)
この辺りをしっかり抑えておけば、セキュリティが盤石なシステム構築が比較的簡単に構築できそうだと感じました。
さいごに
今後も様々な記事を投稿する予定なので、興味がある方は是非「読者になる」をお願いします!