-
Notifications
You must be signed in to change notification settings - Fork 231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Credentials support NSSecureCoding, CredentialsManager Utility, KeyChain Storage #113
Conversation
Auth0/Authentication.swift
Outdated
guard let refreshToken = credentials.refreshToken else { | ||
return callback(AuthenticationError(string: "missing refresh_token", statusCode: 400), nil) | ||
} | ||
guard let expiresIn = credentials.expiresIn, expiresIn < Date(timeIntervalSinceNow: -60) else { return callback(nil, credentials) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does the -60
means? Now that I see this, it reminds me that some tokens might be deemed invalid wrongly depending on the device's clock. Remember the leeway thing we support in our jwt libs? Maybe we should open a method to customize it too for this case. It should not be an issue for most of the users but some of them might have it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the 60 although it should perhaps be longer based like 5 minutes. As it would be if a token was not renewed as it was close to expiry and then immediately expires when the user goes to use it a few seconds later.
I've not looked at this in the JWT Libs, although I feel if a user's clock is wrong this isn't really our issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The token should be used right after the user asks for it, so it's up to him if he takes too long and the server rejects it.. Maybe not 5 minutes, but 2 min already is too much. Remember that the problem here is the server, because we can still decode a jwt payload if needed even if expired.. Maybe it's best to check the server "allowed leeway" and pick a smaller value just to be sure. Anyway, it's not too hard to log in again 😛. I'll approve this pr and let you decide if a change it's needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should have a leeway eventually just in case to avoid issues with customers. Having 5 minutes or a big one is not that a big deal since the RS will properly validate the AT and reject it on API call
Auth0/Authentication.swift
Outdated
- parameter callback: callback with the user's credentials or the cause of the error. | ||
- important: This method only works for a refresh token obtained after auth with OAuth 2.0 API Authorization. | ||
*/ | ||
func renewExpired(_ credentials: Credentials, scope: String? = nil, callback: @escaping (Error?, Credentials?) -> Void) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sort of breaks all the convention of API with the request that the rest of the methods have. If we want to avoid that we can move this to a different type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also the naming is a bit odd, since it won't renew expired all the time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we add the keychain storing how the whole story will look like?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding naming, well renewExpired
implies only renew when expired, you will get the same credentials back if they have not expired.
Yes, I was conflicted over where to put it. I put it here as it requires Authentication
so rather than passing an Authentication
instance it, but agree the breaking of convention makes it not quite feel right from an API purity point of view.
I ran originally ran through a few scenarios, for example I liked the idea of the renew method in Credentials
or in a Storage
utility class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public class Storage: A0SimpleKeychain {
public func archive(object: NSSecureCoding, forKey key: String) -> Bool {
return self.setData(NSKeyedArchiver.archivedData(withRootObject: object), forKey: key)
}
public func unarchive(objectWithKey key: String) -> Any? {
guard let data = self.data(forKey:key) else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: data)
}
}
If we want to avoid adding SimpleKeyChain
this can become a protocol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thought:
let storedCredentials = storage.unarchive("credentials") as? Credentials
// Class method in Credentials
Credentials.renewExpired(credentials, authentication: authentication) { error, credentials in }
You could of course add a 1 call wrapper as well to unarchive and renew from storage, which people would use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The inheritance there is not needed and tbh it exposes way too much info and API. Also you could have achieved the same thing with an extension.
I'd say we iterate the idea a bit more with a separate entity to handle the Auth state.
If I read it renewExpired
seems to be imply that it might be called when I know the token is expired not to ensure I have a fresh/valid/nonexpired token
Auth0/Authentication.swift
Outdated
*/ | ||
func renewExpired(_ credentials: Credentials, scope: String? = nil, callback: @escaping (Error?, Credentials?) -> Void) { | ||
guard let refreshToken = credentials.refreshToken else { | ||
return callback(AuthenticationError(string: "missing refresh_token", statusCode: 400), nil) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use a different type of error for this case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to avoid creating another error type. WebAuthError
is closer but as it deals with missing token cases or can create a new error type but it's pretty much one case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok but put status code 0 not 400
@hzalaz after todays talks, I've added Turned this into a Credentials utility for easy, store, retrieve, renew. Sort of thing be nice to see in the Centralized QS. I can update tests and add docheaders in morning, wanted to check it was the right direction to take. Not 100% sure on name though. |
be9822a
to
3eb9eb4
Compare
I am still not sure on |
f0c3ca6
to
476d3ac
Compare
Auth0/CredentialsManager.swift
Outdated
/// Retrieve stored credentials instance from keychain | ||
/// | ||
/// - Returns: Optional Credentials instance | ||
public func retrieve() -> Credentials? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets remove this one in favor of the next one
Auth0/CredentialsManager.swift
Outdated
/// | ||
/// - Parameter credentials: credentials instance to store | ||
/// - Returns: Bool outcome of success | ||
public func store(_ credentials: Credentials) -> Bool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets not use _
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind either way, however isn't it clear inside a CredentialsManager
that has a param type of Credentials
that it is going to store a credentials instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
more for the flow of the api manager.store(credentials: result)
then in the future we could have manager.store(accessToken: token, expiresIn: date)
to avoid passing all the credential object.
Auth0/CredentialsManager.swift
Outdated
/// - scope: scopes to request for the new tokens. By default is nil which will ask for the same ones requested during original Auth | ||
/// - callback: callback with the user's credentials or the cause of the error. | ||
/// - Important: This method only works for a refresh token obtained after auth with OAuth 2.0 API Authorization. | ||
public func retrieveCredentialsAutoRenew(withScope scope: String? = nil, callback: @escaping (CredentialsManagerError?, Credentials?) -> Void) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just call it either credentials(withScope:, callback:)
or withCredentials(scope:, callback)
the idea is to use this one to try to get credentials to be used. If it fails the user should login otherwise it will always yield a valid token
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thx, sometimes naming is the hardest part.
|
||
import Foundation | ||
|
||
public enum CredentialsManagerError: Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure about this list of errors, few of them are actionable for the developer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tend to add them for logging, could argue that expiresIn and refreshToken could be one guard and one error e.g. just .noRefreshToken
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be 3 cases
- Failed to refresh (api call or no expires In)
- No credentials (no creds)
- No refresh token (only raised when trying to renew)
|
||
import Foundation | ||
|
||
public enum CredentialsManagerError: Error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be 3 cases
- Failed to refresh (api call or no expires In)
- No credentials (no creds)
- No refresh token (only raised when trying to renew)
Added CredentialsManager convenience utility
c56857e
to
00903d6
Compare
Update Tests
00903d6
to
5cceb8f
Compare
Although I was very tempted to add SimpleKeyChain to this repo, I felt we should leave it up to the user. NS*Coding is how you generally expect to serialize information in iOS. I don't feel we need to add any convenience wrappers here to
NSKeyedArchiver/NSKeyedUnArchiver
as they are essentially one liners and is yet another public API.