Refresh token

Refresh token – Android

Hello, c’est Nicolas, développeur Android pour Webwag. Aujourd’hui nous allons parler de la mise en place d’un service de refresh token en Kotlin, grâce à la bibliothèque Retrofit combinée au client OkHttp.

Cas d’utilisation du refresh token

Lorsqu’une application permet à un utilisateur de se connecter, celui-ci s’attend à conserver sa connexion lorsqu’elle est relancée. Pour cela, il est obligatoire de stocker une information qui identifie cet utilisateur côté serveur : un token.

Ainsi, lors de la première connexion, le serveur génère la plupart du temps un token unique par utilisateur. Pour la sécurité des données, ce token devient invalide au bout d’un certain temps (1h, 3 jours, 1 semaine, … c’est le serveur qui choisit), et c’est là que doit intervenir un service qui permet de rafraîchir ce token côté application.

La manière « simple » de générer un nouveau token serait de renvoyer une requête d’authentification en ayant au préalable stocké les identifiants de connexion (login / mot de passe) dans les données privées de l’application (via les SharedPreferences).

Malheureusement, cette zone de stockage n’est pas sécurisée, et il devient facile pour n’importe quel hacker d’accéder à ces informations. Fort heureusement, la plupart des serveurs possèdent un service de refresh token qui nécessite uniquement l’ancien token devenu invalide, et qui en génère un nouveau.

Mise en place

Nous allons utiliser Retrofit avec le client OkHttp pour implémenter le refresh token.

implementation "com.squareup.retrofit2:retrofit:2.4.0"
implementation "com.squareup.retrofit2:converter-moshi:2.4.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.4.0"
implementation "com.squareup.moshi:moshi-adapters:1.7.0"
implementation "com.squareup.moshi:moshi-kotlin:1.7.0"

Pour créer un service, nous avons besoin de plusieurs éléments :
– Une interface service qui contient les différentes requêtes
– Un converter pour parser les objets (dans notre cas Moshi)
– Un client réseau (dans notre cas OkHttp)
– Un objet Retrofit.Builder pour instancier notre service

Nous allons créer ici trois services :
– un pour les requêtes qui ne nécessitent pas d’être identifié
– un autre une fois connecté
– et enfin le dernier pour le refresh token uniquement.

Vu que les trois services se basent sur le même serveur, nous allons factoriser une partie de l’initialisation de ces services.

 

Socle commun aux trois services

Converter

Tout d’abord, le converter permettant de parser les données :

fun provideConverterFactory(): MoshiConverterFactory {
    val moshiBuilder = Moshi.Builder().add(KotlinJsonAdapterFactory())
    return MoshiConverterFactory.create(moshiBuilder.build())
}

J’utilise personnellement Moshi plutôt que GSON ou d’autres car Moshi est plus moderne dans son utilisation, et plus performant. Je vous invite à lire les différents commentaires de ce thread Reddit pour avoir un meilleur feedback.

 

Client HTTP

Ensuite, nous allons créer le socle du client http :

fun provideClientBuilder(): OkHttpClient.Builder {
    val loggingInterceptor = HttpLoggingInterceptor().apply {
        level =
            if(BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BASIC }
            else { HttpLoggingInterceptor.Level.NONE }
    }
	
    val apiKeyInterceptor = Interceptor {
        val newRequest = it
                            .request()
                            .newBuilder()
                            .addHeader("x-api-key", "abcdef")
                            .build()

        it.proceed(newRequest)
    }

    return OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(60, TimeUnit.SECONDS)
            .addInterceptor(loggingInterceptor)
            .addInterceptor(apiKeyInterceptor)
}

loggingInterceptor nous permet d’afficher des informations lors d’une requête dans le Logcat si on est en debug et apiKeyInterceptor signe les requêtes envoyées au serveur en ajoutant l’ApiKey dans les headers.

 

Retrofit.Builder

Enfin, nous pouvons créer notre Retrofit.Builder de base :

fun provideRetrofitBuilder() : Retrofit.Builder {
    val converter = provideConverterFactory()

    return Retrofit.Builder()
            .baseUrl("https://mybaseurl.com")
            .addConverterFactory(converter)
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
}

 

LoggedOutService

Service utilisé lorsque l’utilisateur n’est pas connecté

interface LoggedOutService{

    @FormUrlEncoded
    @POST("api/login")
    fun login(@Field("username") username: String, @Field("password") password: String) : Call<Login>

}
LoggedInService

Service utilisé pour avoir / mettre à jour les données de l’utilisateur

interface LoggedInService{

    @FormUrlEncoded
    @POST("api/user")
    fun getUserData() : Call<User>

}
AuthenticateService

Service utilisé pour rafraîchir le token uniquement

interface AuthenticateService{

    @FormUrlEncoded
    @POST("api/user/token")
    fun refreshToken(@Field("refreshToken") refreshToken: String) : Call<Login>

}

La classe Login, qui est utilisée dans LoggedOutService et AuthenticateService est définie ainsi :

data class Login(val token: String, val refreshToken: String)

token est à passer en header du service authentifié, et refreshToken est à passer en paramètre pour la méthode refreshToken() de AuthenticateService.

 

Création des services non-authentifiés : LoggedOutService  & AuthenticateService

Création du client HTTP

Nous pouvons directement construire le client depuis le Builder car il n’y a aucun élément à ajouter.

fun provideClient(): OkHttpClient = provideClientBuilder().build()

Implémentation des services

Nous pouvons maintenant générer les deux services non-authentifiés :

fun provideLoggedOutService() : LoggedOutService {
    val builder = provideRetrofitBuilder()
    val client = provideClient()

    return builder
            .client(client)
            .build()
            .create(LoggedOutService::class.java)
}

fun provideAuthenticateService() : AuthenticateService {
    val builder = provideRetrofitBuilder()
    val client = provideClient()

    return builder
            .client(client)
            .build()
            .create(AuthenticateService::class.java)
}

Création du service authentifié : LoggedInService

Ajout du token dans les headers

Les requêtes de ce service doivent être authentifiées car elles concernent uniquement l’utilisateur connecté. Pour cela, nous allons utiliser le token récupéré via le login et le passer en header grâce à un interceptor.

fun provideBearerInterceptor(): Interceptor = Interceptor {
    val newRequest = it
                    .request()
                    .newBuilder()
                    .addHeader("Authorization", "Bearer ${PrefManager.login.token}")
                    .build()

    it.proceed(newRequest)
}

Libre à vous de récupérer le token comme vous le souhaitez. De mon côté, après chaque login / refreshToken, je sauvegarde mon objet Login dans les SharedPreferences via un PrefManager, que je peux ensuite récupérer facilement dans mon interceptor.

object PrefManager {
    var login : Login
    get() {
        val token = Prefs.getString("token", "")
        val refreshToken = Prefs.getString("refreshToken", "")

        return Login(token, refreshToken)
    }
    set(value) {
        Prefs.putString("token", value.token)
        Prefs.putString("refreshToken", value.refreshToken)
    }
}

Prefs est un wrapper de SharedPreferences que vous pouvez découvrir ici : https://github.com/Pixplicity/EasyPrefs

 

Ajout de l’authenticator pour rafraîchir le token

Nous allons également ajouter un Authenticator qui sera appelé automatiquement dès qu’une requête renverra une erreur 401 – unauthorized.

class TokenAuthenticator(private val service : AuthenticateService) : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request? {
        if(responseCount(response) >= 2) {
            return null
        }

        val refreshTokenCall = service.refreshToken(PrefManager.login.refreshToken)
        val refreshResponse = refreshTokenCall.execute()

        if(refreshResponse.isSuccessful) {
            PrefManager.login = refreshResponse.body()

            return response.request()
                    .newBuilder()
                    .header("Authorization", "Bearer ${PrefManager.login.token}")
                    .build()
        }

        return null
    }

    private fun responseCount(response: Response) : Int {
        var count = 1
        var res = response.priorResponse()
        while(res != null) {
            count++
            res = res.priorResponse()
        }

        return count
    }

}

La méthode authenticate sera donc invoquée lorsque le token aura expiré. Afin de le rafraîchir, nous allons donc utiliser notre AuthenticateService. C’est directement le thread de la requête qui appelle la méthode authenticate. Ainsi, il n’y aura pas de soucis de blocage de la UI.

Si la requête fonctionne, nous mettons à jour les informations sauvegardées dans le PrefManager, et nous reprenons la requête initiale. À l’inverse, si celle-ci échoue, nous renvoyons null.

Enfin, la méthode responseCount permer de compter le nombre de fois où nous essayons de rafraîchir le token pour cette requête. Si le refreshToken échoue 2 fois ou plus, nous n’essayons plus de rafraîchir le token, pour éviter de partir en boucle infinie.

 

Création du client HTTP
fun provideAuthorizedClient(): OkHttpClient {
    val bearerInterceptor = provideBearerInterceptor()
    val authenticateService = provideAuthenticateService() // voir plus bas
    val authenticator = TokenAuthenticator(authenticateService)

    return provideClientBuilder()
            .authenticator(authenticator)
            .addInterceptor(bearerInterceptor)
            .build()
}
Implémentation du service

Après avoir créé le client HTTP, nous pouvons facilement générer le service associé :

fun provideLoggedInService() : LoggedInService {
    val builder = provideRetrofitBuilder()
    val client = provideAuthorizedClient()

    return builder
            .client(client)
            .build()
            .create(LoggedInService::class.java)
}

Conclusion

Ainsi, grâce à la puissance combinée de Retrofit et OkHttp, le refresh token devient un jeu d’enfant. Malgré tout, il reste encore des cas à traiter. En effet, si la requête refreshToken échoue pour une raison quelconque, nous aurons une erreur 401 – unauthorized. Pour régler ce problème, l’utilisateur est déconnecté et il doit se reconnecter via la méthode classique (login / mot de passe).

À noter : Nous aurions pu regrouper LoggedOutService et AuthenticateService. Mais je trouve plus clair d’utiliser un service qui est dédié au refresh token car il ne doit avoir aucune dépendance. Ce vu qu’il est créé pour LoggedInService. Il n’aurait pas été logique que LoggedInService dépende de LoggedOutService .

Articles liés
Récupération de la signature d’un client dans une application
DataBinding pour les applications Windows
Mon avis sur les animations lottie pour Android
À la découverte du RecyclerView

Laissez votre commentaire

Votre commentaire*

Votre Nom*
Votre site internet

six − deux =