Let's write β

プログラミング中にできたことか、思ったこととか

Auth0のCredentialsManagerとOkhttp3のAuthenticator, Interceptorを組みあわせる

背景

今後新規開発するサービスでAuth0をIDaaSとして利用する可能性があったので 技術調査としてAuth0をAndroidでRetrofit + Okhttp3で利用する時にどのように組みあわせるのか調査していました。

ライブラリ

implementation "com.auth0.android:auth0:1.22.1"

Auth0のCredentialをSecureCredentialManagerに保存する

Auth0のライブラリには、SecureCredentialsManagerというAuth0の認証情報をセキュアに端末内で管理するのをサポートするためのクラスが付属しています。

auth0.com

このクラスに、ログインの結果取得できたCredentialsを保存しておく事で、有効期限が切れた時などに自動的にトークンをリフレッシュした結果を取得する事ができます:

たとえば以下はログイン成功時に保存するイメージ:

//じっさいはここらへんはDagger2等でSingletonでInjectすると良いです。
val auth0Account = Auth0(BuildConfig.AUTH0_CLIENT_ID, BuildConfig.AUTH0_DOMAIN).also {
  it.isOIDCConformant = true
}
val auth0AuthenticationAPIClient = AuthenticationAPIClient(auth0)
val auth0SharedPreferencesStorage = SharedPreferencesStorage(context)
val auth0SecureCredentialManager = SecureCredentialsManager(context, auth0AuthenticationAPIClient, auth0SharedPreferencesStorage)
WebAuthProvider.login(auth0Account)
            .withScheme("demo")
            .withAudience("https://${BuildConfig.AUTH0_DOMAIN}/userinfo")
            .start(requireActivity(), object : AuthCallback {
                override fun onSuccess(credentials: Credentials) {
                    //ここで保存しておく
                    auth0SecureCredentialsManager.saveCredentials(credentials)
                }

                override fun onFailure(dialog: Dialog) {
                    Timber.d("Failed to login")
                }

                override fun onFailure(exception: AuthenticationException?) {
                    exception?.let {
                        Timber.e(it)
                    }
                }
            })

SecureCredentialsManagerからトークンを取りだす

さて、このSecureCredentialsManagerにはgetCredentialsというメソッドが用意されており コールバックを通して有効期限が切れていた場合には自動的にリニューアルした結果のCredentialsを取得する事ができます。

auth0SecureCredentialManager.getCredentials(new BaseCallback<Credentials, CredentialsManagerException>() {
    @Override
    public void onSuccess(Credentials credentials) {
        //Use credentials
    }

    @Override
    public void onFailure(CredentialsManagerException error) {
        //No credentials were previously saved or they couldn't be refreshed
    }
});

Okhttp3のAuthenticatorやInterceptorとSecureCredentialsManagerを組みあわせたい

Okhttp3のAuthenticator

Okhttp3ではAuthenticatorというクラスを通して、サーバーから401が帰ってきた時等にリクエストに認証情報を付与する事ができます。

square.github.io

class AuthTokenAuthenticator : Authenticator {
    override fun authenticate(route: Route, response: Response): Request? {
        val token = TODO("トークンをリニューアルする")
        return response
                .request()
                .newBuilder()
                .removeHeader("Authorization")
                .addHeader("Authorization", "Bearer $token")
                .build()
    }
}

しかし、今回も問題はSecureCredentialsManagerはコールバックを通して非同期的に結果を返してくれる形式なので、 同期的にトークンをリフレッシュしたい今回の場面ではそのままでは使う事ができません。

SecureCredentialsManagerのコールバックをsuspend関数に変換する

そのため、今回は以下の記事を参考にgetCredentialsをラップしてCoroutine形式でトークンが取得できるような拡張関数を定義しました。

medium.com

suspend fun SecureCredentialsManager.getCredentials(): Credentials {
    return suspendCoroutine { cont ->
        getCredentials(object :
            BaseCallback<Credentials, CredentialsManagerException> {
            override fun onSuccess(payload: Credentials) {
                cont.resume(payload)
            }

            override fun onFailure(error: CredentialsManagerException) {
                cont.resumeWithException(error)
            }
        })
    }
}

AuthenticatorのrunBlockingにする

さて、無事SecureCredentialsManagerのgetCredentialsをsuspend関数に変換したので、authenticateの本体をrunBlockingで囲う事によって同期的に取得する事ができるようになりました。

そのため、以下のように呼びだしてやる事によってトークンをリフレッシュした結果を取得してリクエストを生成する事ができるようになります。

 override fun authenticate(route: Route, response: Response): Request? = runBlocking {
        try {
            val token = auth0SecureCredentialsManager.getCredentials().idToken ?: return@runBlocking null
            response
                .request()
                .newBuilder()
                .removeHeader("Authorization")
                .addHeader("Authorization", "Bearer $token")
                .build()
        } catch (e: Exception) {
            Timber.e(e)
            null
        }
    }

Interceptorでも同様に呼びだす

Interceptorについても、同様にrunBlockingしてやって付与する事ができます。

class AuthTokenHeaderInterceptor : Interceptor {
    @Inject
    lateinit var secureCredentialsManager: SecureCredentialsManager

    override fun intercept(chain: Interceptor.Chain): Response? = runBlocking {
        try {
            val token = secureCredentialsManager.getCredentials().idToken ?: return@runBlocking null
            val newRequest = chain.request().newBuilder()
                .addHeader("Authorization", "Bearer $token")
                .build()
            withContext(Dispatchers.IO) {
                chain.proceed(newRequest)
            }
        } catch (e: Exception) {
            Timber.e(e)
            null
        }
    }
}

このようにする事で無事に、サーバーサイドでもヘッダーに送られてきたJWTトークンをデコードして認証情報を確認する事ができました。

auth0.com