Skip to content
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

MOB-415: Add access to access token and save it in the keychain #11

Merged
merged 6 commits into from
Dec 19, 2016

Conversation

mats-claassen
Copy link
Contributor

Fixes #6

Adds access token management code according to IDme/ID.me-WebVerify-SDK-Android#9

Also adds SAMKeychain dependency for keychain access. Dependency is added as a pod dependency.


/// API Constants (Production)
#define IDME_WEB_VERIFY_GET_AUTH_URI @"https://api.id.me/oauth/authorize?client_id=%@&redirect_uri=%@&response_type=token&scope=%@"
#define IDME_WEB_VERIFY_GET_USER_PROFILE @"https://api.id.me/api/public/v2/%@.json?access_token=%@"

/// Data Constants
#define IDME_WEB_VERIFY_ACCESS_TOKEN_PARAM @"access_token"
#define IDME_WEB_VERIFY_EXPIRATION_PARAM @"expires_in"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep expires_in at the same column of access_token

[self clearWebViewCacheAndCookies];
}

return self;
}

+ (void)initializeWithClientID:(NSString *)clientID redirectURI:(NSString *)redirectURI {
[[IDmeWebVerify sharedInstance] setClientID:clientID];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to support changing the clientID after the initialize method was called? I think these properties should be readonly and here we should call a private constructor which pass the id and url

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently sharedInstance is initializing the instance. Should we require calling initialize... before calling sharedInstance and then calling the init from the initialize?

@param forceRefreshing Force the SDK to refresh the token and do not use the current one.
@param callback A block that returns an NSString object representing a valid access token or an NSError object.
*/
- (void)getAccessTokenWithScope:(NSString*)scope forceRefreshing:(BOOL)force result:(IDmeVerifyWebVerifyResults _Nonnull)callback;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add nullability indicator to scope

NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSUInteger statusCode = [httpResponse statusCode];
if ([data length]) {
NSDictionary *results = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass an error variable and check if everything was ok

[self.keychainData clean];
}

- (void)getAccessTokenWithScope:(NSString*)scope forceRefreshing:(BOOL)force result:(IDmeVerifyWebVerifyResults)callback{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nullability indicators are missing

@@ -343,4 +384,21 @@ - (NSString * _Nullable)clientID{

return (_clientID) ? _clientID : nil;
}

#pragma mark - Helpers
- (void)setExpirationDateWith:(NSNumber*)seconds{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add nullability indicator

self.keychainData.expirationDate = [NSDate dateWithTimeIntervalSinceNow:[seconds doubleValue]];
}

- (UIViewController*) topMostController
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add nullability indicator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, I'm not sure if we should use this here, it is related to the getAccessToken feature

return;
} else if (![scope isEqualToString:self.keychainData.scope]) {
[self logout];
[self verifyUserInViewController:[self topMostController] scope:scope withTokenResult:callback];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should show the login form automatically. This function getAccessToken is intended to just get a fresh token to make an API call, for example. Remember that this could be done in a background thread where the developer maybe don't want or can't show some UI. In this case I would call the callback with an error. This mostly must be defined in the issue discussion

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This depends on how scope change is handled. I would think that scope change would require a new login and should therefore not be called from a background thread. But yes it should be discussed in the issue.


NSError* error;
[SAMKeychain setPasswordData:dictionaryRep forService:[NSBundle mainBundle].bundleIdentifier account:IDME_KEYCHAIN_DATA_ACCOUNT error:&error];
NSLog(@"%@", error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if persist fails, shouldn't fail the entire sign in process?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure. Keychain might fail but the sign in succeeded. So the user will be able to use the app while he does not shut it down. He would have to sign in again after shutting down the app as the token has not been saved to the keychain. But it is better than make the sign in fail when it succeeded in reality.

-(void)loadFromKeychain{
NSString *error;
NSData* data = [SAMKeychain passwordDataForService:[NSBundle mainBundle].bundleIdentifier account:IDME_KEYCHAIN_DATA_ACCOUNT];
NSDictionary *dictionary = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:nil errorDescription:&error];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should send back this error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is internal and just called in the init

@param clientID The clientID provided by ID.me when registering the app at @b http://developer.id.me
@param redierectURI The redirectURI provided to ID.me when registering your app at @b http://developer.id.me
@param affiliationType The type of group verficiation that should be presented. Check the @c IDmeVerifyAffiliationType typedef for more details
@param scope The type of group verficiation that should be presented.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo in verification

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied it from the previous method 😱

@mats-claassen
Copy link
Contributor Author

PR yet depending on scope handling discussion

NSDate* expiration = self.keychainData.expirationDate;
NSDate* now = [[NSDate alloc] init];
if ([now compare:expiration] != NSOrderedAscending) {
// TODO: refresh token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mats-claassen Are you planning on adding refreshing tokens in this branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. I want to implement that in a separate branch linking to #7

@param webVerificationResults A block that returns an NSDictionary object and an NSError object. The verified user's profile is stored in an @c NSDictionary object as @c JSON data. If no data was returned, or an error occured, @c NSDictionary is @c nil and @c NSError returns an error code and localized description of the specific error that occured.
*/
- (void)getUserProfileWithResult:(IDmeVerifyWebVerifyResults _Nonnull)webVerificationResults;
- (void)getUserProfileWithScope:(NSString* _Nullable)scope andResult:(IDmeVerifyWebVerifyResults _Nonnull)webVerificationResults;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSUInteger statusCode = [httpResponse statusCode];
if ([data length]) {
NSError* error;
NSDictionary *results = (NSDictionary *)[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only needed if statusCode == 200, right? maybe you can move it to that if.


I'm not sure about the structure of this if else block. What would happen if statusCode == 401 and the response doesn't have a body? Currently it is returning the error IDmeWebVerifyErrorCodeVerificationDidFailToFetchUserProfile instead of doing the refresh process.

What do you think?


if (!latestScope) {
// no token has been requested yet
callback(nil, nil, nil);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't an error be returned in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens when there has never been a login. We could return an error IDmeWebVerifyErrorCodeNoSuchScope

-(NSDate* _Nullable)expirationDateForScope:(NSString* _Nonnull)scope;
-(NSString* _Nullable)refreshTokenForScope:(NSString* _Nonnull)scope;

-(void)setToken:(NSString * _Nonnull)accessToken expirationDate:(NSDate * _Nonnull)date
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move each parameter to a new line to improve readability

options:NSPropertyListMutableContainersAndLeaves
format:nil
error:&error];
self.tokensByScope = [NSMutableDictionary dictionaryWithDictionary:dictionary];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't you set latestScope at this moment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right

-(NSString* _Nullable)refreshTokenForScope:(NSString* _Nonnull)scope;

-(void)setToken:(NSString * _Nonnull)accessToken expirationDate:(NSDate * _Nonnull)date
refreshToken:(NSString * _Nullable)refreshToken forScope:(NSString * _Nonnull)scope;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refresh token must not be nullable, right? maybe add a todo to change it when we are implementing refresh feature

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is nullable as it will not be updated when the access token gets updated so passing nil will leave the old refresh token. We can change this if it is not clear. Or we should document it

@param webVerificationResults A block that returns an NSDictionary object and an NSError object. The verified user's profile is stored in an @c NSDictionary object as @c JSON data. If no data was returned, or an error occured, @c NSDictionary is @c nil and @c NSError returns an error code and localized description of the specific error that occured.
*/
- (void)getUserProfileWithResult:(IDmeVerifyWebVerifyResults _Nonnull)webVerificationResults;
- (void)getUserProfileWithScope:(NSString* _Nullable)scope andResult:(IDmeVerifyWebVerifyResults _Nonnull)webVerificationResults;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't use the same block type for getting the token and user profile due to them should have different argument list. When requesting the access token doesn't matter the profile parameter, and when requesting the user profile probably you don't want the access token.

What do you think? Maybe this is a change for a new PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I did just keep the block as it was in the previous version.


if (force) {
// TODO: refresh token
callback(nil, nil, nil);
callback(nil, nil);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

due to we won't support refresh token feature for a time, what do you think about sending back a not-implemented-like error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should remove the force for now.

@@ -204,16 +192,13 @@ - (void)getAccessTokenWithScope:(NSString* _Nullable)scope forceRefreshing:(BOOL
NSDate* now = [[NSDate alloc] init];
if ([now compare:expiration] != NSOrderedAscending) {
// TODO: refresh token
callback(nil, nil, nil);
callback(nil, nil);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and here return an unauthorized error?

@@ -119,12 +119,12 @@ - (void)verifyAction:(id)sender {
}

- (void)tokenTestAction:(id)sender {
[[IDmeWebVerify sharedInstance] getUserProfileWithResult:^(NSDictionary *userProfile, NSError *error, NSString *accessToken) {
[[IDmeWebVerify sharedInstance] getUserProfileWithScope: @"wallet" result:^(NSDictionary *userProfile, NSError *error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we use some test scope? do we want wallet to be public?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this should not be there

/// Keychain Constants
#define IDME_KEYCHAIN_DATA_ACCOUNT @"IDME_KEYCHAIN_DATA"
#define IDME_EXPIRATION_DATE @"IDME_EXPIRATION_DATE"
#define IDME_REFRESH_TOKEN @"IDME_REFRESH_TOKEN"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mats-claassen We should drop refresh token for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything is implemented so that a refresh token can be easily supported in the future. Do you want us to remove everything related to refresh tokens?

// IDmeWebVerifyKeychainData.m
// WebVerifySample
//
// Created by Mathias Claassen on 12/9/16.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mats-claassen We should be able to drop the "created by" since we're using git :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

@mats-claassen mats-claassen merged commit 063a7c9 into 4.x Dec 19, 2016
@mats-claassen mats-claassen deleted the MOB-415 branch December 19, 2016 18:17
@mats-claassen mats-claassen mentioned this pull request Mar 28, 2017
sdownie pushed a commit that referenced this pull request Mar 29, 2017
MOB-415: Add access to access token and save it in the keychain

Fixes #6

* Handle several scopes and their associated access tokens
* Separate the callback block for profile and token results.
* Throw appropriate errors when there is no valid token
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants