Recently I migrated the use of the Lock v1 SDK to the new Lock-Swift SDK and was left wrestling with issues for days. I hope this post will help anybody also having a difficult time, and this should serve in addition to the provided Migration guide which I found to be currently lacking.
The problems seem to largely stem from a messy move towards OIDC conformance. Until such a time as a system is moved to using accessToken
instead of idToken
, getting things to work can prove to be messy.
I'm taking for granted that you are using the new SDK, and have the Log In / Sign Up view displaying when appropriate in your application. After following the guide that they provide on their website I was left with:
Lock
.classic()
.withOptions {
$0.oidcConformant = false
}
.withStyle {
…
}
.onAuth { [unowned self] credentials in
…
}
.onSignUp {
…
}
.present(from: self)
My first issue was that Lock wasn't providing the refreshToken
; easily fixed by adding the required scope:
.withOptions {
$0.oidcConformant = false
$0.scope = "openid offline_access"
}
Still not getting the refresh token… Oh wait, there's a secret key that they want which is absent from the iOS documentation:
.withOptions {
$0.oidcConformant = false
$0.scope = "openid offline_access"
if let deviceID = UIDevice.current.identifierForVendor?.uuidString {
$0.parameters = ["device": deviceID]
}
}
We have lift off! Oh no… Facebook isn't working.
Well, this was actually my fault. With some digging the documentation states:
The Authorization Code flow with PKCE can only be used for Clients whose type is Native in the Dashboard.
The guide actually tells you to set the client type to Native
as well… whoops.
That's it, right?
Well, no.
Sign up isn't working.
Unfortunately I still don't know what about our flow mean that the onSignUp { … }
callback doesn't work, but it doesn't. Therefore, it's time for a bit of dirty work:
.onAuth { credentials in
guard let accessToken = credentials.idToken,
let refreshToken = credentials.refreshToken else { return }
// ATTEMPT TO SIGN IN
viewModel.signIn(with: accessToken, refreshToken: refreshToken) { error in
guard let signInError = error as? SignInError else { return }
switch signInError {
case .shouldShowSignUp:
// HERE I NOW KNOW THAT I NEED TO USE THE PROVIDED CREDENTIALS TO CREATE AN ACCOUNT
Auth0.authentication().userInfo(token: accessToken)
.start { result in
switch result {
case .failure(let error):
print("Error fetching user information: \(error)")
case .success(let profile):
/// YAY - NOW WE USE THE NEW PROFILE TO SIGN UP WITH MY CLIENT
…
}
}
}
})
…
}
With that done, we're golden!
Celebration
Release to TestFlight
Next Day
"Hey James, everything is broken now"
Doesn't take a genius to realise that when no calls to the backend are working, it's because none of them are authorized… Seems the refreshing of the idToken
isn't working. Probably should have checked that…
The refresh code:
Auth0
.authentication()
.renew(withRefreshToken: refreshToken, scope: nil)
.start { [weak self] result in
switch result {
case .failure(_):
/// THE REFRESH FAILED
case .success(let credentials):
/// CONGRATS! Here's your new token:
let newIDToken = credentials.idToken
}
}
Well, this was working, but the newIDToken
would always be nil
. That's definitely not what I wanted.
Maybe I need to add the scopes that I used before?
.renew(withRefreshToken: refreshToken, scope: "openid offline_access")
That didn't work, and I'm already out of ideas.
Fortunately I work with an amazing backend developer, and all too quickly I dumped this problem on his shoulders. With a lot of back and forth, umming and ahhing, and scratching of heads, we stumbled upon the solution.
Auth0
.authentication()
.delegation(withParameters: ["refresh_token": refreshToken])
.start { [weak self] result in
switch result {
case .success(let data):
guard let idToken = data["id_token"] as? String else {
/// No ID token in the received data
return
}
// save your idToken
case .failure(let error):
/// FAIL
}
}
Rather than renew(withRefreshToken: scope:)
I needed delegation(withParameters:)
to which I will pass a random key with the refreshToken
as the value. If you're wondering how this is at all intuitive, it's not. What is less intuitive is the supposed data
you receive and the untyped key that you use to retrieve the new idToken
.
With that then, I was finished.
What should have been an easy migration turned into a nightmare of searching for answers to unacknowledged problems. I suppose it's always good to be reminded that even the simple things can be a complete pain in the arse.