Gestion du token pour l'authentification Directus

Bonjour à tous, @mbritto, @Mrt1,

Suite au coatching, j’ai tenté de bosser la gestion des tokens pour l’authentification des requêtes Directus. J’ai choisi de stocker l’accessToken et le refreshToken et de les réinitialiser à vide après un certain temps. Mais j’ai l’impression que mon timer ne se met. jamais en route pour remettre à vide mes tokens. Est-ce que vous voyez un problème sur mon code ?

func login() async throws {
        var loginPath: String
        var json: [String : String]
        
        if refreshToken.isEmpty {
            loginPath = "https://api.pickfungi.com/auth/login"
            json = ["email": "[email protected]", "password": "XXXXX"]
        } else {
            loginPath = "https://api.pickfungi.com/auth/refresh"
            json = ["refresh_token": refreshToken]
        }
        
        guard let loginUrl = URL(string: loginPath) else {
            throw ShowError.URLError
        }
        var urlRequest = URLRequest(url: loginUrl)
        urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        urlRequest.httpMethod = "POST"
        
        let jsonData = try? JSONSerialization.data(withJSONObject: json, options: [])
        urlRequest.httpBody = jsonData
        
        let (data, response) = try await URLSession.shared.data(for: urlRequest)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else { throw ShowError.httpStatusCodeError }
        
        guard let downloadedLoginResponse = try? JSONDecoder().decode(LoginResponse.self, from: data) else {
            throw ShowError.dataError
        }
        
        accessToken = downloadedLoginResponse.data.access_token
        refreshToken = downloadedLoginResponse.data.refresh_token
        
        Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { timer in
            self.accessToken = ""
        }

        Timer.scheduledTimer(withTimeInterval: 20.0, repeats: false) { timer in
            self.refreshToken = ""
        }
    }

En plus de ce problème là, je viens de percuter que je créé un RemoteDataProvider par viewModel.
En gros, View1 appelle ViewModel1 qui utilise une instance d’un objet Manager qui appelle une instance d’un RemoteDataProvider. Et View2 appelle ViewModel2 qui utilise une autre instance de l’objet Manager qui appelle une autre instance d’un RemoteDataProvider. Comme je stocke mon accessToken et mon refreshToken dans mon RemoteDataProvider, je relance une connexion login/mot de passe pour chacune des vues …
Est-ce normal de créer une instance pour chaque vue ? Doit-on remonter le stockage des tokens ailleurs pour que tous les appels puissent en profiter ?

Merci de votre aide !

Pour le token, je te conseille d’éviter les timers. Il vaut mieux que tu stockes ton token, ton token refresh et la date d’expiration dans 3 variables.
Puis, à chaque fois que tu t’apprêtes à lancer une requête tu commences par vérifier la date d’expiration :

  • si elle est dans le futur alors tu peux faire ta requête avec ton token
  • si elle est dans le passé alors tu utilises ton token refresh pour regenerer un nouveau token avant de faire ta requete

Non, il faudrait que tu n’aies qu’une seule instance de ton RemoteDataProvider sinon tu va recréer sans arrêt des tokens alors que les précédents sont encore valides

Hello @mbritto,

J’ai corrigé mon code en définissant une date d’expiration à laquelle j’ajoute 14m et 50sec à chaque login/refresh :

expireDate = Date().addingTimeInterval(890)

Et je teste avant chaque requête nécessitant une authentification si ma date d’expiration est plus vieille que la date système :

if expireDate < Date() {
      try await login()
}

Il me reste plus qu’à régler mes problèmes d’instance :slight_smile:

Quand je lance mon appli et ma première vue, je crée un ViewModel :

@StateObject var viewModel = ViewModel()

Et dans mon ViewModel, je crée un manager :

private var manager = Manager()

Dans mon manager, je crée également un remoteDataProvider :

var request = RemoteDataProvider()

Si je navigue vers un autre écran, comment puis-je accéder à mon manager crée dans ma première vue ? Aujourd’hui, j’en recrée un …

Merci pour ton aide !!

1 « J'aime »

Dans l’idéal, il te faudrait un objet qui chapeaute ta navigation, une sorte de routeur qui crée chacun des écrans et leur fait passer ce dont ils ont besoin. C’est assez complexe à mettre en place et c’est justement l’objet du cours avancé d’architecture et de navigation que je viens de sortir en Dart/Flutter.
Il faudra faire une version iOS/Swift de ce cours, mais avec la WWDC qui a lieu dans 1 mois et demi ça fait un peu tard pour sortir un cours Swift/SwiftUI avec un risque de changements importants.

La solution la plus rapide et la plus simple pour toi serait le singleton pour ton RemoteDataProvider

Je vais laisser en l’état pour l’instant. Mon appli n’est pas prête de sortir, il me reste beaucoup de boulot. Et je verrai par la suite si tu proposes un cours dans ce sens :slight_smile:

1 « J'aime »

Salut @Tazooou,

Tu peux declarer ton RemoteDataProvider() dans ton App et la passer a une vue contenant toutes les autres : « MainScreen » par example (ou tout autre vue contenant toutes les vues où tu en a besoin, comme a expliqué @mbritto).

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            MainScreen(dataProvider: RemoteDataProvider())
        }
    }
}
struct MainScreen<T>: View where T: RemoteDataProviderProtocol {
    
    @ObservedObject private var dataProvider: T
    
    init(dataProvider: T) {
        self.dataProvider = dataProvider
    }
    
    var body: some View {
        VStack {
            if dataProvider.isLoggedIn {
                HomeScreen(dataProvider: dataProvider)
            } else {
                LoginScreen(dataProvider: dataProvider)
            }
        }
    }
}

Puis dans tes écrans descendants, récupérer le DataProvider, en le passant par le onAppear, et ajouter son propre ViewModel :

struct HomeScreen<T>: View where T: RemoteDataProviderProtocol {
    
    @ObservedObject var dataProvider: T
    @StateObject private var viewModel = HomeViewModel()
    
    var body: some View {
        Text("Home Screen")
            .onAppear {
                self.viewModel.dataProvider = self.dataProvider as? RemoteDataProviderProtocol
            }
    }
}

Le protocole doit conformer à ObservableObject :

protocol RemoteDataProviderProtocol: ObservableObject {
    var isLoggedIn: String { get }
    func login() -> Void
}

Happy Coding ! :wink:

1 « J'aime »

Merci @cedric06nice. Je vais regarder ça de près :slight_smile:

1 « J'aime »

@Tazooou et @mbritto Petite question sur cette explication qui me bloque dans la compréhension :

  • Je comprends que le token à une durée d’expiration.
  • Je pensais jusqu’à présent que le refreshToken avait cette même durée d’expiration mais du coup je doute :
    Le refreshToken a-t-il une durée d’expiration ou il a une durée de vie illimitée ?

Hello @Xababa_Dalabama,

Désolé pour le délai de réponse. Non, le refresh Token n’a pas la même durée d’expiration mais il en a une tout de même !
Elle est plus longue que le token puisque tu es censé l’utiliser moins souvent dans tes requêtes.
Par défaut, je crois que c’est 7 jours mais tu peux tout à fait le paramétrer toi même sur ton serveur.
Je vais essayer de te retrouver le sujet qui traitait de ça :slight_smile:

1 « J'aime »

Le bout de doc qui parle de ça :

refresh_token chaîne
Le jeton qui peut être utilisé pour récupérer un nouveau jeton d’accès via /auth/refresh. Remarque : si vous avez utilisé cookie le mode dans la requête, le jeton d’actualisation ne sera pas renvoyé dans le JSON.

Date d’expiration

Le délai d’expiration du jeton peut être configuré via la ACCESS_TOKEN_TTL variable d’environnement .

2 « J'aime »
ACCESS_TOKEN_TTL La durée pendant laquelle le jeton d’accès est valide. 15m
REFRESH_TOKEN_TTL La durée de validité du jeton d’actualisation, ainsi que la durée pendant laquelle les utilisateurs restent connectés à l’application. 7d
1 « J'aime »

Merci @Tazooou pour l’info bien utile :smiley: Bon du coup il faut bien conserver le mp ou demander à l’utilisateur de se reco :stuck_out_tongue: moi qui croyait en avoir finit avec ce Login xD
En tout cas merci pour les éléments ca m’aide beaucoup !

1 « J'aime »