How to renew the id token using auth0 in SwiftUI app

532 Views Asked by At

I'm using Auth0 for login and logout in my iOS app. after the user logs in I get an id token which I use to make the further api calls in my app. we need to keep updating the token with auth0 as mentioned in their doc My function is as follows

struct UpdateToken {
let credentialsManager: CredentialsManager

init() {
    self.credentialsManager = CredentialsManager(authentication: Auth0.authentication())
}


func updateToken() {
    
    guard credentialsManager.canRenew() else {
        // Present login screen
        print("not renewing")
        return
    }
    Auth0
        .webAuth()
        .scope("openid profile offline_access")
    
        .audience("\(audience)/userinfo")
        .start {
            switch $0 {
            case .failure(let error):
                print("token update failed")
                break
                // Handle error
            case .success(let credentials):
                // Pass the credentials over to the Credentials Manager
                credentialsManager.store(credentials: credentials)
                UserDefaults.standard.set(credentials.idToken, forKey: "id_token")
                print("token updated")
                
            }
    }
}

}

it is printing not renewing in my console. I'm not sure what I am missing here.

the login function works perfectly fine

func login() {
    Auth0
        .webAuth()
        .start { result in
            // Handle the result of the authentication
            switch result {
            case .failure(let error):
                // If the authentication fails, print the error message
                print("Failed with: \(error)")
                
            case .success(let credentials):
                // If the authentication is successful, store the credentials and user information in UserDefaults
                self.userProfile = Profile.from(credentials.idToken)
                self.userIsAuthenticated = "1"
                print("Credentials: \(credentials)")
                
                // Store the ID token
                print("ID token: \(credentials.idToken)")
                UserDefaults.standard.set(credentials.idToken, forKey: "id_token")
                
                // Print and store the token type and access token
                print("token type: \(credentials.tokenType)")
                print("access token \(credentials.accessToken)")
                
                // Extract and store the user ID, name, and email from the user profile
                print("userID is \(userProfile.id)")
                let fullString = userProfile.id
                let parts = fullString.split(separator: "|")
                let desiredPart = String(parts[1])
                print(desiredPart)

                UserDefaults.standard.set(desiredPart, forKey: "userId")
                UserDefaults.standard.set(userProfile.name, forKey: "userName")
                UserDefaults.standard.set(userProfile.email, forKey: "userEmail")
                
            }
        }
}
2

There are 2 best solutions below

1
On

It sounds like canRenew() is unable to find any stored credentials - Try using credentialsManager.store on initial login similar to how you are in updateToken(). This way the credentials are stored in the keychain when a user logs in to begin with.

0
On

The reason why it is not renewing is because you didn't persist credentials in CredentialsManager. More less something like this

self.credentialsManager.store(credentials: withCredentials)

Please check my code and adapt it to your needs

protocol AuthServiceProtocol {
  func login(email: String, password: String) async throws -> Credentials
  func refreshToken(withRefreshToken: String) async throws -> Credentials
  func register(email: String, password: String) async throws -> DatabaseUser
  func resetPassword(email: String) async throws
  func changePassword(email: String, password: String) async throws
  func requestEmailVerificationCode(email: String) async throws
  func verifyEmailVerificationCode(email: String, code: String) async throws
}

AuthService:

final class AuthService: ObservableObject {
  @Published var profile: ProfileModel?
  
  enum AuthError: LocalizedError {
    case expiredAuth
    case missingRefreshToken
    case notAuthenticated
    
    var errorDescription: String? {
      switch self {
      case .expiredAuth: return "Auth expired"
      case .notAuthenticated: return "User not authenticated"
      case .missingRefreshToken: return "Missing refresh token"
      }
    }
  }
  
  
  private let credentialsManager = CredentialsManager(authentication: Auth0.authentication())
  private let defaults = UserDefaults.standard
  
  
  let authService: AuthServiceProtocol
  
  init(authService: AuthServiceProtocol) {
    self.authService = authService
  }
  
  func refreshToken() async throws {
    do {
      let refreshToken = try await getRefreshToken()
      let credentials = try await authService.refreshToken(withRefreshToken: refreshToken)
      
      loginWithAuthCredentials(credentials)
      
    } catch let error {
      throw error
    }
  }
  
  func getRefreshToken() async throws -> String {
    // check if user is persisted in the app loca credentials manager
    guard self.credentialsManager.canRenew() else {
      throw AuthError.notAuthenticated
    }
    
    return try await withCheckedThrowingContinuation { continuation in
      credentialsManager.credentials { result in
        switch result {
        case .success(let credentials):
          
          guard let refreshToken = credentials.refreshToken else {
            continuation.resume(throwing: AuthError.missingRefreshToken)
            return
          }
          
          continuation.resume(returning: refreshToken)
        case .failure(let error):
          continuation.resume(throwing: error)
        }
      }
    }
  }
  
  func getAuthenticatedUser() async throws -> ProfileModel {
    
    // check if user is persisted in the app loca credentials manager
    guard self.credentialsManager.canRenew() else {
      throw AuthError.notAuthenticated
    }
    
    return try await withCheckedThrowingContinuation { continuation in
      credentialsManager.credentials { result in
        switch result {
        case .success(let credentials):
          
          
          // throw authExpired and refresh token if credentials expires in 5h from now
          if credentials.expiresIn < Date().addingTimeInterval(3600 * 5) {
            continuation.resume(throwing: AuthError.expiredAuth)
            return
          }
          
          continuation.resume(returning: ProfileModel.from(credentials.idToken))
        case .failure(let error):
          continuation.resume(throwing: error)
        }
      }
    }
  }
  
  func loginWithAuthCredentials(_ withCredentials: Auth0.Credentials) {
    let _ = self.credentialsManager.store(credentials: withCredentials)
    
    DispatchQueue.main.async { [self] in
      defaults.setValue(withCredentials.idToken, forKey: "authToken")
      profile = ProfileModel.from(withCredentials.idToken)
        
    }
  }

  func logout() {
    let _ = self.credentialsManager.clear()
    defaults.removeObject(forKey: "authToken")
  }
  
}

and in the app, on the very first View you can attach something like this:

struct RootView: View {
  
  //  @StateObject var authentication = Authentication()
  
  @StateObject private var authState: AuthService = AuthService(authService: Auth0Service())
  @State private var showSignInView: Bool = false
  let persistenceController = PersistenceController.shared
  
  var body: some View {
    
    VStack {
      if !showSignInView && authState.profile != nil {
        MainView(
          authState: authState,
          showSignInView: $showSignInView
        )
          .environment(
            \.managedObjectContext, persistenceController.container.viewContext
          )
      }
    }
    .onAppear {
      Task {
        do {
          let authUser = try await authState.getAuthenticatedUser()
          authState.profile = authUser
          self.showSignInView = false
          
        } catch AuthService.AuthError.expiredAuth {
          
          do {
            try await authState.refreshToken()
          } catch {
            authState.logout()
            self.showSignInView = true
            authState.profile = .empty
          }
          
        } catch {
          authState.logout()
          self.showSignInView = true
          authState.profile = .empty
        }
      }
    }
    .fullScreenCover(isPresented: $showSignInView, content: {
      WelcomView(
        authState: authState,
        showSignInView: $showSignInView
      )
    })
  }
}

Also keep in mind that in order to make it works you need to enable "Refresh Token Rotation" in your Auth0 App

enter image description here