Help us understand the problem. What is going on with this article?

Alamofire5.2のAuthenticationInterceptorでトークンリフレッシュを試す

AuthenticationInterceptor

先月Alamofire 5.2がリリースされました。
https://github.com/Alamofire/Alamofire/releases/tag/5.2.0

新機能の1つとして、「AuthenticationInterceptor」があります。
(Release)
https://github.com/Alamofire/Alamofire/releases/tag/5.2.0
(PR)
https://github.com/Alamofire/Alamofire/pull/3164

クレデンシャルを使用したリクエストのAdaptとRetryを容易にするために導入されたとされています。
AuthenticationInterceptorは、AdaptとRetryの処理におけるスレッドやキューイングの管理を意識せずに実装するための仕組みのようです。

経緯

導入の発端?となったのはこのissueのようです。
https://github.com/Alamofire/Alamofire/issues/3086

今までAdvanced Usageに記載されていたOAuthのトークンリフレッシュ処理のexampleが突然削除されたのはなぜか?という趣旨のissueです。

従来Alamofireで行われていたトークンリフレッシュの実装はこんな感じでした。(上記issueより抜粋)

  // MARK: - RequestRetrier

    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        lock.lock() ; defer { lock.unlock() }

        if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {
            requestsToRetry.append(completion)

            if !isRefreshing {
                refreshTokens { [weak self] succeeded, accessToken, refreshToken in
                    guard let strongSelf = self else { return }

                    strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

                    if let accessToken = accessToken, let refreshToken = refreshToken {
                        strongSelf.accessToken = accessToken
                        strongSelf.refreshToken = refreshToken
                    }

                    strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
                    strongSelf.requestsToRetry.removeAll()
                }
            }
        } else {
            completion(false, 0.0)
        }
    }

RequestRetrierを用いる方法ですが、上記のようなコードが今まではAlamofireのAdvanced Usageには記載がありました。
それが最近突如削除されたので、既存のサンプルコードが何かまずい動作をしていたのではないかと心配になった開発者の方々がいたようですね。
私もRequestRetrierを実際に利用していましたが、「lock.lock()処理がサンプルに入っているけど、入れても他に影響ないかな?」みたいなことを考えた記憶があります。

Alamofireの開発サイドとしては、どうやら掲載していたサンプルコードは本番で動く想定のものではなかったとのことです。しかしその意図と反して本番プロジェクトにも利用されることが増えてしまったため、Advanced Usageから削除したとコメントされています。実際、ネット上で見るRequestRetrierの実装の紹介の多くが上記のような実装になっていますよね。

こういった状況を踏まえ、スレッドの管理やlock処理について意識した実装をする必要のない仕組みの導入に至ったようです。

使い方

クレデンシャル情報をプロパティに持つstructを定義します。(公式のサンプルから抜粋)

struct OAuthCredential: AuthenticationCredential {
    let accessToken: String
    let refreshToken: String
    let userID: String
    let expiration: Date

    // Require refresh if within 5 minutes of expiration
    var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration }
}

次にAuthenticatorクラスを実装します。
applyメソッドでは、クレデンシャル情報の適用(headerにトークン付与)、refreshメソッドではリフレッシュ処理を行います。

class OAuthAuthenticator: Authenticator {
    func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
        urlRequest.headers.add(.authorization(bearerToken: credential.accessToken))
    }

    func refresh(_ credential: OAuthCredential,
                 for session: Session,
                 completion: @escaping (Result<OAuthCredential, Error>) -> Void) {

        // トークンリフレッシュ処理
        requestToRefreshToken()
        // 新しいクレデンシャルの作成
        completion(.success(newCredential))
    }

    func didRequest(_ urlRequest: URLRequest,
                    with response: HTTPURLResponse,
                    failDueToAuthenticationError error: Error) -> Bool {
        return response.statusCode == 401
    }

    func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
        let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
        return urlRequest.headers["Authorization"] == bearerToken
    }
}

上記のようにrefreshメソッド内でトークンリフレッシュ処理を行い、新しいクレデンシャル情報をcompletion(.success)に渡してあげると、それ以降の通信は新しいクレデンシャルをapplyメソッドで適用してくれるようになります。

使い方はこんな感じです。 (公式のAdvancedUsageより抜粋)

// Generally load from keychain if it exists
let credential = OAuthCredential(accessToken: "a0",
                                 refreshToken: "r0",
                                 userID: "u0",
                                 expiration: Date(timeIntervalSinceNow: 60 * 60))

// Create the interceptor
let authenticator = OAuthAuthenticator()
let interceptor = AuthenticationInterceptor(authenticator: authenticator,
                                            credential: credential)

// Execute requests with the interceptor
let session = Session()
let urlRequest = URLRequest(url: URL(string: "https://api.example.com/example/user")!)
session.request(urlRequest, interceptor: interceptor)

手軽にトークンリフレッシュ機能を実装することが可能でした。

尚、Authenticatorクラスの「didRequest」「isRequest」メソッドですが、下記のような定義がコメントで書かれているものの、手元でこのメソッドに到達するケースを見つけることができませんでした。

    func didRequest(_ urlRequest: URLRequest,
                    with response: HTTPURLResponse,
                    failDueToAuthenticationError error: Error) -> Bool {
        // If authentication server CANNOT invalidate credentials, return `false`
        return false

        // If authentication server CAN invalidate credentials, then inspect the response matching against what the
        // authentication server returns as an authentication failure. This is generally a 401 along with a custom
        // header value.
        // return response.statusCode == 401
    }

    func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
        // If authentication server CANNOT invalidate credentials, return `true`
        return true

        // If authentication server CAN invalidate credentials, then compare the "Authorization" header value in the
        // `URLRequest` against the Bearer token generated with the access token of the `Credential`.
        // let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
        // return urlRequest.headers["Authorization"] == bearerToken
    }

issueでも質問をしてみていますが、引き続き調査をしつつ判明したことがあったら追記をしていきます。

以上、AuthenticationInterceptorを使ってみた際のメモでした。

maKunugi
アプリエンジニアやスクラムマスターをしています。 個人開発で音声アシスタントや対話システム関連のサービスを開発・運用しています。
globis
グロービスは 1992 年の創業以来、社会人を対象とした MBA、人材育成の領域で Ed-Tech サービスを提供し、現在は日本 No.1 の実績があります。これらの資産と、さらに IT や AI を活用することで、アジア No.1 を目指しています。
http://www.globis.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした