【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の通信をどのように行うか、operationの結果をどのように使用するかなどを記述する仕組みです。 通信先のURLを指定したり、使用するClientを定義したり、認証処理を定義したりできます。 GraphQLのClientでは下記のようにlinkをつなげて定義できます。

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

ここで、通信時の処理は結合したlinkの前の方から行われていきます。 実際のLinkの定義を見ると理解が深まります。 重要な部分のみ抜粋すると下記のようになります。

Link {
  Link concat(Link next) => _concat(this, next);
}

Link _concat(
  Link first,
  Link second,
) {
  return Link(request: (
    Operation operation, [
    NextLink forward,
  ]) {
    return first.request(operation, (Operation op) {
      return second.request(op, forward);
    });
  });
}

concat(link)で行っていることは、1番目のlinkで処理した結果を2番目のlinkに引き渡すという処理をまとめた新たなlinkを作成するということです。 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を使用することで問題なく実装を進めることができるかと思います。

参考