Iganinのブログ

日頃の開発で学んだ知見を中心に記事を書いています。

【Flutter】graphql_flutterでの認証処理について

graphql_flutter【Flutter】graphql_flutterのクライアントにTimeoutを設定するにて記載した通り、 Flutterでgraphqlを扱う際には第一候補として使用を検討するライブラリかと思います。

本稿では、graphql_flutterを使用した認証処理について記載します。

TL;DR

  • AuthLinkを使用する
  • JWTのような有効期限つきの認証トークンを使用する場合は、AuthLinkの初期化時の引数であるgetTokenに更新処理を渡しておく

開発環境

下記環境を使用しています。

[] Flutter (Channel stable, 1.22.2, on Mac OS X 10.15.6 19G2021, locale ja-JP)
 
[] Android toolchain - develop for Android devices (Android SDK version 30.0.0)
[] Xcode - develop for iOS and macOS (Xcode 11.5)
[] Android Studio (version 4.0)
[] VS Code (version 1.49.1)

graphql_flutter: "3.1.0"

Linkについて

GraphQL Serverとの通信において、通信を行うまでに行いたい処理や、通信結果に対して行いたい処理をLinkに記述していきます。 通信先のURLを指定したり、使用するClientを定義したり、認証処理を定義したり、エラーハンドリングを定義したりできます。 GraphQLのClientでは下記のようにlinkをつなげて定義できます。

static final Link link = authLink.concat(httpLink).concat(websocketLink);

以下の図がわかりやすいですが、mutationやqueryを実行する際に,operationが各Linkを伝っていき、 必要に応じてlinkでoperationに処理を行います。例えば認証用のheaderを付与したりなどです。 最終的にGraphQL Serverとのやりとりを行ったあとは、返却値がoperationが伝うのとは逆順で伝わっていき、 最終的に呼び出し元に返却されます。

f:id:Iganin:20210402110703p:plainApollo Link OverViewより

LinkについてはApolloのLinkに関するDocumentが参考になります。

AuthLinkについて

AuthLinkは上記のLinkのうち認証の仕組みを導入するために使用されるLinkです。 下記のように定義して使用します。

  static final AuthLink authLink = AuthLink(getToken: () => _token);

実際のコードは下記のようになっています。 onListen内の処理が肝心な部分となっていて、Streamが流れてきた都度 getTokenを実行し、 tokenがnullでない場合にHeaderにデフォルトではAuthorizationキーに対して認証情報を付与しています。 ここで重要なのはcontrollerのonListenのタイミングで都度getTokenが呼び出されると言う点です。 そのためgetTokenに関数を使用し、都度その時の最新のaccessTokenを取得するようにできます。

class AuthLink extends Link {
  AuthLink({@required this.getToken, this.headerKey = 'Authorization'})
      : super(
          request: (Operation operation, [NextLink forward]) {
            StreamController<FetchResult> controller;

            Future<void> onListen() async {
              try {
                final String token = await getToken();
                if (token != null) {
                  operation.setContext(<String, Map<String, String>>{
                    'headers': <String, String>{headerKey: token}
                  });
                }
              } catch (error) {
                controller.addError(error);
              }

              await controller.addStream(forward(operation));
              await controller.close();
            }

            controller = StreamController<FetchResult>(onListen: onListen);

            return controller.stream;
          },
        );

  GetToken getToken;
  String headerKey;
}

AuthLink内にJWTの更新処理を実装

上記までの情報を前提にJWTのような有効期限を有する認証トークンを使用した認証処理を考えてみます。 今回は、RefreshTokenとAccessTokenを使用し、RefreshTokenによるアクセストークンの更新の都度、RefreshTokenも合わせて更新されるようなパターンを考えます。 RefreshToken、AccessTokenの更新をどこでやるかですが、これらに対する関心はAPI Clientに閉じると考えられますので、API Clientクラス内に記述します。

流れのみを記述すると下記のようになります。

final httpLink = HttpLink(uri: '$apiUrl');  //apiのurlを入れる
final authLink = AuthLink(getToken: () async => await _getAccessToken());
final link = authLink.concat(httpLink);
final _graphQLClient = GraphQLClient(link: link, cache: InMemoryCache());

String _accessToken; // ひとまずインスタンスに持たせていますが、SecureStorageに持たせるのもよいと思います。

Future<String> _getAccessToken() async {
  final accessToken = _accessToken;
  if (accessToken == null) {
    return null;
  }
  
  // アクセストークンの有効期限を確認します。
  // APIからExpireDateが返却されていた場合はそちらの値を、
  // そうでない場合はAccessTokenから有効期限を取得します。
  // jaguar_jwtなど使用すると楽ができると思います。
  final accessTokenExpireDate = await _getAccessTokenExpireDate();
  final currentDateTime = DateTime.now();
      
  if (accessTokenExpireDate.isBefore(currentDateTime)) {
    // refreshTokenを用いてaccessTokenとrefreshTokenの更新を実施します。
    // refreshTokenはsecureStorageに格納しておいて、取得すると良いかと思います。
    final refreshToken = await _getRefreshToken();
    if (refreshToken == null) return null;
    
    final newAccessToken = await authorize(refreshToken: refreshToken);
    return 'Bearer $newAccessToken';
  } else {
    return 'Bearer $accessToken';
  }
}

上記において、authorize(refreshToken: refreshToken)ではAPIに用意されたrefreshTokenを用いたaccessTokenの更新処理を行います。 返却値に応じて下記が必要になるかと思います。

  • 返却値のaccessTokenの保持処理
  • 返却値のrefreshTokenの保存処理

まとめ

graphql_flutterでの認証処理に関して記載しました。 基本的にはAuthLinkを使用することで問題なく実装を進めることができるかと思います。

参考