背景
今後新規開発するサービスでAuth0をIDaaSとして利用する可能性があったので 技術調査としてAuth0をAndroidでRetrofit + Okhttp3で利用する時にどのように組みあわせるのか調査していました。
ライブラリ
implementation "com.auth0.android:auth0:1.22.1"
Auth0のCredentialをSecureCredentialManagerに保存する
Auth0のライブラリには、SecureCredentialsManager
というAuth0の認証情報をセキュアに端末内で管理するのをサポートするためのクラスが付属しています。
このクラスに、ログインの結果取得できた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が帰ってきた時等にリクエストに認証情報を付与する事ができます。
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形式でトークンが取得できるような拡張関数を定義しました。
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トークンをデコードして認証情報を確認する事ができました。