Azure Active Directory Refresh Token for iOS client - ios

I have a standard iOS app from QuickStart downloaded from my portal like so:
Its a very basic iOS app that communicates with the Azure backend where you can push and pull data.
log in, log out
If you are to turn on authentication in your portal, you will need to log in. You do so like this:
[client loginWithProvider:#"windowsazureactivedirectory"
controller:controller
animated:YES
completion:^(MSUser *user, NSError *error) {
// save data into your keychain
// invoke various custom APIs
// be able to pull and push data to your Easy Table
}];
Log out is also simple:
[client logoutWithCompletion:^(NSError * _Nullable error) {
if(!error) {
[self clearCredentials];
complete(nil);
} else {
DDLogError(#"%#", [error debugDescription]);
complete(error);
}
}];
Refreshing the Token
My problem is the token. I understand that when using Active Directory, the token they give you lasts for 1 hour only. If you were to get a Refresh token, you need to use it within 14 days. If you do use it within 14 days, it will be good up to 90 days after that.
Hence, I waited an hour, and when I try using the client to pull data or invoke an API, it would give me an 401 error:
Error Domain=com.Microsoft.MicrosoftAzureMobile.ErrorDomain Code=-1301 "The server returned an error." UserInfo={com.Microsoft.MicrosoftAzureMobile.ErrorResponseKey= { URL: https://xxxx-xxxxxx.azurewebsites.net/api/getUserProfileData } { status code: 401, headers {
"Content-Length" = 0;
Date = "Tue, 25 Oct 2016 09:18:57 GMT";
Server = "Microsoft-IIS/8.0";
"Set-Cookie" = "ARRAffinity=743d3a9e04f3c081f27c2f2c3af95d099f93dc60f14553bd4423b0abc62cfd33;Path=/;Domain=xxxx-xxxxxx.azurewebsites.net";
"Www-Authenticate" = "Bearer realm=\"xxxx-xxxxxx.azurewebsites.net\"";
"X-Powered-By" = "ASP.NET";
} }, NSLocalizedDescription=The server returned an error., com.Microsoft.MicrosoftAzureMobile.ErrorRequestKey= { URL: https://xxxx-xxxxxx.azurewebsites.net/api/getUserProfileData }}
The log for my custom node script that the client hits, gives 401.71 unauthorized error:
2016-10-25T09:18:55 PID[35372] Warning JWT validation failed: IDX10223: Lifetime validation failed. The token is expired.
ValidTo: '10/25/2016 09:12:31'
Current time: '10/25/2016 09:18:55'..
2016-10-25T09:18:55 PID[35372] Information Sending response: 401.71 Unauthorized
If I were to do a pull down on my table to grab data:
com.Microsoft.MicrosoftAzureMobile.ErrorRequestKey= { URL: https://xxx-xxxxx.azurewebsites.net/tables/TodoItem?$skip=0&$filter=(updatedAt%20ge%20datetimeoffset'2016-10-20T02%3A33%3A25.607Z')&$orderby=updatedAt%20asc&__includeDeleted=true&$top=50 }}
code - -1301
domain - com.Microsoft.MicrosoftAzureMobile.ErrorDomain
userInfo - {
NSLocalizedDescription = "The server returned an error.";
"com.Microsoft.MicrosoftAzureMobile.ErrorRequestKey" = " { URL: https://xxxx-xxxxxx.azurewebsites.net/tables/TodoItem?$skip=0&$filter=(updatedAt%20ge%20datetimeoffset'2016-10-20T02%3A33%3A25.607Z')&$orderby=updatedAt%20asc&__includeDeleted=true&$top=50 }";
Microsoft Azure has a small article on refreshing tokens here:
https://azure.microsoft.com/en-us/blog/mobile-apps-easy-authentication-refresh-token-support/
I would use refreshUserWithCompletion: method, but it keeps giving me 403 error.
Hence, if anyone have successfully refreshed their token on an iOS app, please comment.
thank you!
UPDATE: REFRESH WORKS, TOKEN STILL EXPIRES IN 1 HOUR
After following the links, my refreshUserWithCompletion method works, and returns a valid MSUser object with token. However, the issue is that this token is still only good for 1 hour. I'm pasting my refresh token code below. Please suggest, thanks!
if(!client.currentUser) {
[client loginWithProvider:#"windowsazureactivedirectory"
controller:controller
animated:YES
completion:^(MSUser * user, NSError *error) {
DDLogInfo(#"INITIAL TOKEN, userId: %#, token: %#", user.userId, user.mobileServiceAuthenticationToken);
if(!error && user) {
[client refreshUserWithCompletion:^(MSUser * _Nullable user, NSError * _Nullable error) {
// user object now has new token for us to use.
// I'm assuming this is the refresh token.
[self saveUserIntoKeyChain: user];
// I can just load the userId and token from the keychain for future uses, and they all work.
// The only problem is that this token is only good for 1 hour.
}];
}
}];
}

AAD requires configuration for refresh. See my blog on the subject: https://shellmonger.com/2016/04/13/30-days-of-zumo-v2-azure-mobile-apps-day-7-refresh-tokens/
If you haven't configured refresh, it isn't available. Once you do configure refresh, then the NEXT time you log in, you will get a refresh token that can be re-used.

Related

How to do Access Token Request in Fitbit API Integration - iOS?

I did OAuth2 by using OAuth2 - https://github.com/trongdth/OAuth2-for-iOS ,
I successfully logged in and got response
{
"kOAuth_AccessToken" = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0NDIzODY2NzEsInNjb3BlcyI6Indsb2Mgd3BybyB3bnV0IHdzbGUgd3NldCB3d2VpIHdociB3YWN0IHdzb2MiLCJzdWIiOiIzRFJQQzYiLCJhdWQiOiIyMjlROVEiLCJpc3MiOiJGaXRiaXQiLCJ0eXAiOiJhY2Nlc3NfdG9rZW4iLCJpYXQiOjE0NDIzODMwNzF9.5vTYvUAuvMflnOw_7cc1nZoighhtUx4RU26-Q7SewzQ";
"kOAuth_AuthorizeURL" = "https://www.fitbit.com/oauth2/authorize";
"kOAuth_Callback" = "http://oauth-callback/fitbit";
"kOAuth_ClientId" = string;
"kOAuth_ExpiredDate" = "";
"kOAuth_RefreshToken" = 97ad2a073f40f21974c8d27cdb74523047f39e98cd2adcfd6f6cc3eb92522d53;
"kOAuth_Scope" = "activity heartrate location nutrition profile settings sleep social weight";
"kOAuth_Secret" = string;
"kOAuth_TokenURL" = "https://api.fitbit.com/oauth2/token";
}
Here - how can I get code URI parameter value in the callback URI?
According to Fitbit doc's ---
Access Token Request:-
When a user authorizes your application in the Authorization Code Grant flow, your application must exchange the authorization code for an access token. The code is only valid for 10 minutes.
Authorization Header:-
The Authorization header should be set to Basic followed by a space and a Base64 encoded string of your application's client id and secret concatenated with a colon.
I did this by making use of AFNetworking lib, Please have a look on code (header)-
-(AFHTTPSessionManager *)getSessionManager {
if (!self.sessionManager){
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.HTTPAdditionalHeaders = nil;
NSURL *url = [NSURL URLWithString:FITBIT_BASE_URL];
self.sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:url sessionConfiguration:sessionConfiguration];
}
// [self.sessionManager.requestSerializer setValue:Appdelegate.fitbitAuthorizationString forHTTPHeaderField:#"Authorization"];
[self.sessionManager.requestSerializer setAuthorizationHeaderFieldWithUsername:Appdelegate.fitbitOAuthClientId password:Appdelegate.fitbitOAuthClientSecret];
self.sessionManager.responseSerializer = [JSONResponseSerializerWithData serializer];
self.sessionManager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:#"text/html"];
return self.sessionManager;
}
and requested token access - code (parameter's) is
NSDictionary *tempParamsDict = #{#"client_id":[JsonUtil stringFromKey:#"kOAuth_ClientId" inDictionary:dictResponse], #"grant_type":#"authorization_code", #"redirect_uri":[JsonUtil stringFromKey:#"kOAuth_Callback" inDictionary:dictResponse], #"code":#""};
My request is
[dP postJsonContentWithUrl:#"oauth2/token" parameters:tempParamsDict completion:^(BOOL success, id responseObject, NSError *error) {
// NSLog(#"%#", responseObject);
if (success) {
NSLog(#"Response: %#", responseObject);
}
else {
NSLog(#"%#", error.localizedDescription);
/*
You got 404 response ,The 404 or Not Found error message is a HTTP standard response code indicating that the client was able to communicate with the server, but the server could not find what was requested.
Make sure that you have valid url, try to put your link in the browser and see if it is correct, if the url you requested have special headers
*/
}
}];
I am getting - Request failed: not found (404) error.
What have I missed and How can I proceed further to get token and access Fitbit APIs?
Update
That was happening due to access code so I tried to access code, now I am getting this error
description of error->_userInfo:
{
NSErrorFailingURLKey = "https://api.fitbit.com/oauth2/token.json";
NSErrorFailingURLStringKey = "https://api.fitbit.com/oauth2/token.json";
NSLocalizedDescription = cancelled;
}
Thanks a lot in advance
Instead of NSURLSession use NSURLConnection to request data from Fitbit APIs.

How to use Authentication again after it was granted prevously

-(void)getGoogleCalendar{
GTLServiceCalendar *calendarService = self.calendarService;
GTLQueryCalendar *calendarListQuery = [GTLQueryCalendar queryForCalendarListList];
[calendarService executeQuery:calendarListQuery completionHandler:^(GTLServiceTicket *ticket, GTLCalendarCalendarList * object, NSError *error) {
for (GTLCalendarCalendarListEntry *calendar in object.items) {
[self getEventsForTheGivenCalendar:calendar.identifier];
}
}];
}
For the First Time after the Login it works perfect but when app starts after that it gives error
Error Domain=com.google.GTLJSONRPCErrorDomain Code=401 "The operation
couldn’t be completed. (Login Required)" UserInfo=0x79eb4f90
{error=Login Required, GTLStructuredError=GTLErrorObject 0x79fa9270:
{message:"Login Required" code:401 data:[1]},
NSLocalizedFailureReason=(Login Required)}
I am using GTMOAuth2Authentication authentication for the login and storing the user details. If the user details are not nil it doesn't ask for login and then the error occurs while fetching the calendar.
How to fetch GTLCalendar every time the app runs?
You should set the service authorizer in order to be able to make queries.
calendarService.authorizer = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KEYCHAINITEMNAME
clientID:CLIENTID
clientSecret:CLIENTSECRET];

Google OAuth Login Error: Invalid credentials

I have an iPad application which allows users to login to their Gmail account(s) using OAuth2. Thus far, the login process and email fetching is successful. However, when the app is closed and then re-opened after a (long) period of time, an error is produced "invalid credentials,' even though previous logins with the same credentials were successful.
Login Flow:
1) User logs in to gmail using OAuth 2.
2) User email address and oAuthToken provided by the GTMOAuth2Authentication object are saved to core data for future logins.
3) IMAP Session is created using saved email address and OAuthToken.
Here is the relevant code
Google Login
- (void)gmailOAuthLogin
{
NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];
GTMOAuth2ViewControllerTouch *googleSignInController =
[[GTMOAuth2ViewControllerTouch alloc] initWithScope:GmailScope clientID:GmailAppClientID clientSecret:GmailClientSecret keychainItemName:KeychainItemName completionHandler:^(GTMOAuth2ViewControllerTouch *googleSignInController, GTMOAuth2Authentication *auth, NSError *error){
if (error != nil) {
//handle error
} else {
[[ModelManager sharedInstance] authenticateWithEmailAddress:[auth userEmail]
oAuthToken:[auth accessToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {
//create IMAP session using above arguments
} failure:^(NSError *error) {
//handle error
}];
}
}];
[self presentGoogleSignInController:googleSignInController];
}
Create IMAP Session Using MailCore2
- (void)authenticateWithEmailAddress:(NSString *)emailAddress password:(NSString *)password oAuthToken:(NSString *)oAuthToken imapHostname:(NSString *)imapHostname imapPort:(NSInteger)imapPort smtpHostname:(NSString *)smtpHostname smtpPort:(NSInteger)smtpPort success:(void (^)())success failure:(void (^)(NSError *))failure
{
self.imapSession = [[MCOIMAPSession alloc] init];
self.imapSession.hostname = imapHostname;
self.imapSession.port = imapPort;
self.imapSession.username = emailAddress;
self.imapSession.connectionType = MCOConnectionTypeTLS;
self.imapSession.password = nil;
self.imapSession.OAuth2Token = oAuthToken;
self.imapSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :
self.imapSession.authType;
[self.imapSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type,
NSData * data){
NSLog(#"MCOIMAPSession: [%i] %#", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
self.smtpSession = [[MCOSMTPSession alloc] init];
self.smtpSession.hostname = smtpHostname;
self.smtpSession.port = smtpPort;
self.smtpSession.username = emailAddress;
self.smtpSession.connectionType = MCOConnectionTypeTLS;
self.smtpSession.password = nil;
self.smtpSession.OAuth2Token = oAuthToken;
self.smtpSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :
self.smtpSession.authType;
[self.smtpSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type, NSData * data){
NSLog(#"MCOSMTPSession: [%i] %#", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
[[self.imapSession checkAccountOperation] start:^(NSError *error) {
if (nil == error) {
success();
} else {
failure(error); //FAILS WITH INVALID CREDENTIALS ERROR
}
}];
}
Once again, the above code works fine, unless the application has not been used in some time. I was not sure if I needed to refresh the OAuthToken or not, so I tried doing the following on launch of the application:
GTMOAuth2Authentication *auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KeychainItemName clientID:GmailAppClientID clientSecret:GmailClientSecret];
BOOL canAuthorize = [auth canAuthorize]; //returns YES
NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];
[[ModelManager sharedDefaultInstance] authenticateWithEmailAddress:[auth userEmail] oAuthToken:[auth refreshToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {
//create IMAP session
} failure:^(NSError *error) {
NSLog(#"failure %#", error);
}];
But I still get the same error. I have no idea why the OAuth token stops working or how to resolve this. Since the user is able to save multiple accounts, I am wondering if I need to save the refresh token for each account in core data and use that if the access token stops working?
(Disclaimer - I don't know iOS or the gtm-oauth2 libraries, but I do know Google's OAuth implementation.)
Conceptually you do need to persist the refresh token for the user. The refresh token is a long-lived credential which is used (along with your client secret) to get a short-lived access token that is used for actual API calls.
If you anticipate making multiple calls in a short period of time then your app will normally actually persist both the refresh token and access token (currently access tokens will last 1 hour).
That all said, it looks like the gtm-oauth2 library should be taking care of persisting these already (looks like authForGoogleFromKeychainForName does this).
What I think you need help with is getting an up-to-date access token that you can use to initiate your IMAP session.
The gtm-oauth2 library does contain an authorizeRequest method. It takes information about an HTTP request you intend to make and adds the appropriate authorization headers. It looks like this code will examine the state of the access token, and refresh it if necessary.
While I know you aren't able to make an HTTP request (you need to speak IMAP), my suggestion is to use this method with a dummy NSMutableURLRequest - and then, once it's finished, don't actually send the HTTP request, instead examine the headers it added and pull the access token from there.
See:
https://code.google.com/p/gtm-oauth2/wiki/Introduction#Using_the_Authentication_Tokens
Hope that helps - I don't have an iOS environment to test it on.

iOS HTTP request - getting the error response message

This is my bit of code doing a GET request to a REST api.
Im not sure how to get back the message if I get an error:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
NSURL *URL = [NSURL URLWithString:urlString];
[request setURL:URL];
[request setHTTPMethod:#"GET"];
NSError *err = nil;
NSHTTPURLResponse *res = nil;
NSData *retData = [NSURLConnection sendSynchronousRequest:request returningResponse:&res error:&err];
if (err) // This part is never called.
{
NSLog(#"Error: %#", err);
}
else
{
if (res.statusCode != 200)
{
// show the user the status message
NSLog(#"Error: %#", res); // This part is called
}
else
{
}
}
I want to get the error message if it was not successful. But the if (err) block is never called. err is still null, although the statuscode is 400.
And if successful I will get back a json response.
In the code above I get back a statusCode of 400
The error block is not called because the error object is created only if a system level error occurs. This does not happen because the request is sent correctly and the server sends a response. If you are in control of the server, you should probably make it return status code 200 and include an app level status code in the response, that would tell your app that the entered credentials are incorrect.
Edit:
To get status message you can use
+ (NSString *)localizedStringForStatusCode:(NSInteger)statusCode
This is a class method of the NSHTTPURLResponse class.
if (res.statusCode != 200)
{
// show the user the status message
NSLog(#"Error: %#", [NSHTTPURLResponse localizedStringForStatusCode: res.statusCode]); // This part is called
}
Take a look at the NSError class reference:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSError_Class/Reference/Reference.html
You can try to log the error message from the localizedDescription.
you are receiving this status code because- The Web server (running the Web site) thinks that the data stream sent by the client (e.g. your Web browser or our CheckUpDown robot) was 'malformed' i.e. did not respect the HTTP protocol completely. So the Web server was unable to understand the request and process it
to log above problem in respect to ios visit this link
If you read the documentation of sendSynchronousRequest...
error
Out parameter used if an error occurs while processing the request. May be NULL.
this mean that erro will be a valid NSError object in case there is a problem to resolve the request, like a malformed URL.
If the request can be resolved error will be NULL and according with HTTP protocol and depending to the server that you are trying to connect, the NSHTTPURLResponse object will contain all the information about the request.
In general is an error think that every status code different than 200 is an error, for example for a REST based API 204 mean empty data, and in this case the request is finished successfully but the requested resource is just empty data, and this is not an error.
So about your question, is absolutely fine that error is NULL most of the time, if is not mean that there is an issue before reach the target server, in general you have to consider both, error and according to the server that you are trying to talk the status code maps, in most of cases the REST pattern

Fetching Current User Profile using Objective C Google Plus Client Library

I am using the Google CLient Libraries for Objective C available here..
I have successfully been able to Authorize the user and get refresh token. (Using the GTMOAuthenticaion api embedded within).
In the Selector called after successful authorization I make the Get User Profile request as follows.. (I need the id of currently loggedin/authenticated user)
-(void)viewController:(GTMOAuth2ViewControllerTouch *)viewController
finishedWithAuth:(GTMOAuth2Authentication *)auth
error:(NSError *)error {
if (error != nil) {
NSLog(#"Stop");
} else {
if ([auth canAuthorize]){
[Mediator plusService].authorizer = auth;
// Problematic Line
GTLQueryPlus *profileQuery = [GTLQueryPlus queryForPeopleGetWithUserId:#"me"]; // Notice the UserId Param
profileQuery.completionBlock = ^(GTLServiceTicket *ticket, id object, NSError *error) {
if (error == nil) {
self.mediator.gProfile = object;
} else {
NSLog(#"GPlus Service Error %#", error);
}
};
[[Mediator plusService] executeQuery:profileQuery completionHandler:
^(GTLServiceTicket *ticket, id result, NSError *error) {
if (error)
NSLog(#"Some Service Error %#", error);
}];
}
}
}
If I put "me" as parameter, I get invalid user ID error string in jSON response.
However, If I provide some userId like my own 113632923069489732066 it works perfectly fine and returns the appropriate jSON response..!!
The Example for Google Plus inside Examples folder also fails to get current user profile ending with following error.
Error Domain=com.google.GTLJSONRPCErrorDomain Code=400 "The operation couldn’t be completed. (Invalid user ID: {0})" UserInfo=0x7a670fa0 {NSLocalizedFailureReason=(Invalid user ID: {0}), GTLStructuredError=GTLErrorObject 0x7a67b130: {message:"Invalid user ID: {0}" code:400 data:[2]}, error=Invalid user ID: {0}}
P.S. My API Console application doesn't work with iOS option under installed app but needs be configured with "Other" option. When configured with iOS option, the oAuth fails with invalid_client error response.
My Mistake .. !! And a very silly one .. !!
I was signing in using a Gmail Account that was yet not associated with GPlus .. !! =/

Resources