FatFractal customer forums



Author Topic: resetPassword() and expired session error  (Read 2926 times)

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
resetPassword() and expired session error
« on: March 04, 2014, 06:28:22 PM »
I've written a server extension to allow a logged in users to change his password which calls resetPassword() on the server to actually change the password. Everything is working great as long as the user doesn't try to change his password more than once. If a user attempts to change his password multiple times in succession, the first attempt succeeds and all subsequent attempts fail with a 401 response code. The message from the backend is "Your session has expired; please re-login".

Is this error expected? Do I need to log the user out and then log back in every time a password is changed? If so, is this documented somewhere that I missed? (I didn't see any mention in the JavaScript Server API docs I found here: http://www.fatfractal.com/prod/linked_files/FF-Javascript-Server-Side-Docs/global.html#toc42)

Is there a better way to do what I want to do, or some other explanation for the error that I'm missing? This is my first server extension, so it's quite likely that I'm just making a obvious mistake.

Thanks for your help.

kevin@fatfractal.com

  • Administrator
  • *****
  • Posts: 56
    • View Profile
Re: resetPassword() and expired session error
« Reply #1 on: March 04, 2014, 07:41:53 PM »
If you want to reset passwords, then you should probably create a level of indirection.

Here is a really good sample on how to do that...

https://github.com/gkc/fatfractal-code-samples/tree/master/PasswordReset


Otherwise, if you are looking a means to just change the password for a user that is logged in, then you will likely need to log the user out...

Please let me know if if I understand your use case correctly - is this right?
  • User logs in
  • User selects change password
  • User provides new password
  • You call the server extension to change the password using resetPassword

Also if you don't mind sharing your extension code, that will make it easier for me to help...

Thanks,

Kevin

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
Re: resetPassword() and expired session error
« Reply #2 on: March 04, 2014, 10:56:10 PM »
Quote
If you want to reset passwords, then you should probably create a level of indirection.

Here is a really good sample on how to do that...
I saw the code that you linked to (and in fact I used it as a starting point for this extension), but the script I wrote is trying to solve a slightly different use case. The code you linked to allows a user who forgot his password to reset it via email. So, when the user's password is reset he's presumably already logged out, which neatly avoids the problem I'm experiencing. What I'm trying to do is allow a logged in user to change his password in-app.

Quote
Please let me know if if I understand your use case correctly - is this right?

1. User logs in
2. User selects change password
3. User provides new password
4. You call the server extension to change the password using resetPassword

Yes, that's what I'm trying to do.

Quote
Also if you don't mind sharing your extension code, that will make it easier for me to help...

Sure, no problem. I'll warn you that this is the first JavaScript I've written, so if something looks horribly wrong, please let me know.

Code: [Select]
var require = require;
var exports = exports;
var print = print;

var ff = require('ffef/FatFractal');

exports.changePassword= function() {

    // Restrict execution of this code to only logged in users
    if (ff.getActiveUser().guid == "anonymous") {
        throw {statusCode:403, statusMessage:"Must be logged in."};
    }

    var data = ff.getExtensionRequestData();
    var r = ff.response();

    // check that the password has been supplied
    var secret = data.httpContent['password'];
    if (! secret || secret === null || secret == '') {
        r.responseCode = "400";
        r.statusMessage = "Empty password";
        return;
    }
   
    // Change the password
    try {
        ff.resetPassword (ff.getActiveUser().guid, secret);
    } catch (ex1) {
        try {
            ff.logger.error("Change password failed: " + JSON.stringify(ex1));
        } catch (ex2) {
        }
       
        r.responseCode = "500";
        r.statusMessage = "Internal server error";
        return;
    }
   
    // return statusCode 200 and an appropriate message
    r.responseCode = "200";
    r.statusMessage = "Password has been changed";
};

kevin@fatfractal.com

  • Administrator
  • *****
  • Posts: 56
    • View Profile
Re: resetPassword() and expired session error
« Reply #3 on: March 06, 2014, 01:48:48 AM »
Sorry for the delayed response, but I wanted to add this to the test harness on GitHub.

From the tests below it seems like your issue is likely client-side. I would suggest making sure you login again after changing the password and be careful of the asynchronous call to the extension. At any rate, hopefully the test code below (which seems to work fine) will help isolate the issue for you...

Note: I did not quite finish the Android tests, but you can see the HTML5/JS and iOS tests for changing the users password. I hope to complete that tomorrow morning.

The extension is basically identical to what you had written:

Code: [Select]
exports.changePassword= function() {

    // Restrict execution of this code to only logged in users
    if (ff.getActiveUser().guid == "anonymous") {
        throw {statusCode:403, statusMessage:"Must be logged in."};
    }

    var data = ff.getExtensionRequestData();
    var r = ff.response();

    // check that the password has been supplied
    var secret = data.httpContent['password'];
    if (! secret || secret === null || secret == '') {
        r.responseCode = "400";
        r.statusMessage = "Empty password";
        return;
    }
   
    // Change the password
    try {
        ff.resetPassword (ff.getActiveUser().guid, secret);
    } catch (ex1) {
        ff.logger.error("Change password failed: " + JSON.stringify(ex1));
        r.responseCode = "500";
        r.statusMessage = "Internal server error";
        return;
    }
   
    // return statusCode 200 and an appropriate message
    r.responseCode = "200";
    r.statusMessage = "Password has been changed";
};

Note - you can see my version test source here: https://github.com/FatFractal/fyi.managingusers/blob/master/ff-scripts/ManagingUsersExtensions.js#L7-38

The test source for HTML5/JS is here: https://github.com/FatFractal/fyi.managingusers/blob/master/webapp/js/ManagingUsersTests.js#L119-163

The test source for iOS is here: https://github.com/FatFractal/fyi.managingusers/blob/master/ManagingUsersIOSApp/ManagingUsersIOSAppTests/ManagingUsersIOSAppTests.m#L123-163

You can run the HTML5/JS tests in your browser here: https://fyi.fatfractal.com/managingusers/
« Last Edit: March 06, 2014, 01:53:24 AM by kevin@fatfractal.com »

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
Re: resetPassword() and expired session error
« Reply #4 on: March 06, 2014, 03:35:15 PM »
Thanks Kevin. I took my project, stripped it down to bare bones and I'm still getting the error. I can't see how this could be related to something wonky that I'm doing in my project at this point, but I'm including code below. Maybe you can see a problem that I missed. (I can also give you a zip file containing the entire test project if that would be helpful.)

In case it's relevant, here's my application.ffdl file:
Code: [Select]
#
# Application configuration
#
SET ActivateUsersOnReg          true
SET AllowAutoRegistration       false
SET AllowNewCollections         false
SET AllowNewObjectTypes         false
SET AllowNewMembers             false
SET AllowSystemUserToLogin      true
SET SystemUserPassword          <password redacted>
SET AndroidPushAuthToken        YOUR_ANDROID_AUTH_TOKEN_GOES_HERE
SET ApplePushKeystorePassword   YOUR_KEYSTORE_PASSWORD_GOES_HERE
SET ApplePushUsingProduction    false
SET LogLevel                    INFO
SET PushIsInactive              true
SET ETaggingEnabled             true
SET CORS                        https://system.fatfractal.com
SET LogToDatabase               true
SET PasswordValidation          true
SET PasswordPattern             (.+)
SET PasswordMessage             Password cannot be empty


#
# FFUser
#
CREATE OBJECTTYPE FFUser (userName STRING, firstName STRING, lastName STRING, email STRING, active BOOLEAN, authDomain STRING, scriptAuthService STRING, groups GRABBAG /FFUserGroup, notif_ids GRABBAG /FFNotificationID)
CREATE ALIAS ON OBJECTTYPE FFUser GRABBAG BackReferences.FFUserGroup.users AS memberOfGroups
CREATE COLLECTION /FFUser OBJECTTYPE FFUser


#
# FFUserGroup
#
CREATE OBJECTTYPE FFUserGroup (groupName STRING, users GRABBAG /FFUser)
CREATE COLLECTION /FFUserGroup OBJECTTYPE FFUserGroup


#
# FFNotificationID
#
CREATE OBJECTTYPE FFNotificationID (idType STRING, idValue STRING)
CREATE COLLECTION /FFNotificationID OBJECTTYPE FFNotificationID

#
# PasswordResetToken
# An object-type and a collection to hold password reset tokens
# Only allow the system user, or members of the system.admins group, to create, retrieve, update or delete password reset tokens
#
CREATE OBJECTTYPE PasswordResetToken (userGuid STRING)
CREATE COLLECTION /PasswordResetToken OBJECTTYPE PasswordResetToken
PERMIT create:system.admins read:system.admins write:system.admins ON /PasswordResetToken

#
# Event Handlers
#

#
# Extension Resources
#
CREATE EXTENSION /changePassword                   AS javascript:require ('scripts/ChangePassword.js').changePassword();
CREATE EXTENSION /sendPasswordReset    UNSECURED   AS javascript:require ('scripts/PasswordReset.js').sendPasswordResetToken();
CREATE EXTENSION /resetPassword        UNSECURED   AS javascript:require ('scripts/PasswordReset.js').resetPassword();

Here is the ChangePassword.js script I'm using which is essentially the same as the one you posted:
Code: [Select]
var require = require;
var exports = exports;
var print = print;

var ff = require('ffef/FatFractal');

exports.changePassword= function() {

    // Restrict execution of this code to only logged in users
    if (ff.getActiveUser().guid == "anonymous") {
        throw {statusCode:403, statusMessage:"Must be logged in."};
}

var data = ff.getExtensionRequestData();
    var r = ff.response();

// check that the password has been supplied
    var secret = data.httpContent['password'];
    if (! secret || secret === null || secret == '') {
        r.responseCode = "400";
        r.statusMessage = "Empty password";
        return;
    }
   
    // Change the password
    try {
        ff.resetPassword (ff.getActiveUser().guid, secret);
    } catch (ex1) {
        ff.logger.error("Change password failed for user " + ff.getActiveUser.userName + ": " + JSON.stringify(ex1));
        r.responseCode = "500";
        r.statusMessage = "Internal server error";
        return;
    }
   
    // return statusCode 200 and an appropriate message
    r.responseCode = "200";
    r.statusMessage = "Password has been changed";
};

In my sample iOS project, the user enters a username and the current password, then taps a button. When the button is tapped, the changePasswordButtonTapped: method is called, which logs into FatFractal using the entered credentials. That method then calls the changePasswordTrial: method which attempts to repeatedly change the logged in user's password. These are the only two methods in the sample project that invoke anything FatFractal related.
Code: [Select]
#pragma mark - FatFractal Interactions
- (void)changePasswordTrial:(NSNumber *)trialNumber {
   
    NSInteger trialInteger = [trialNumber integerValue];
   
    if(trialInteger <= 3) {
       
        NSString *password = [NSString stringWithFormat:@"password%d", trialInteger];
       
        NSLog(@"Beginning Trial %d with password: %@", trialInteger, password);
       
        NSDictionary *passwordDictionary = [NSDictionary dictionaryWithObject:password forKey:@"password"];
       
        [self.fatFractal postObj:passwordDictionary toExtension:@"changePassword" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
           
            if(theErr == nil) {
               
                // Success
               
                NSLog(@"Trial %d Succeeded", trialInteger);
               
                [self performSelector:@selector(changePasswordTrial:) withObject:[NSNumber numberWithInteger:(trialInteger + 1)] afterDelay:1.0f];
            }
            else {
               
                // Failure
               
                NSLog(@"Trial %d Failed with error: %@", trialInteger,[theErr.userInfo objectForKey:NSLocalizedDescriptionKey]);
            }
        }];       
    }
    else {
       
        NSLog(@"All trials completed");
    }
}

#pragma mark - Interface Actions
- (void)changePasswordButtonTapped:(id)sender {
   
    self.fatFractal = [[FatFractal alloc] initWithBaseUrl:@"https://metakite.fatfractal.com/benjamin"];
   
   
    [self.fatFractal loginWithUserName:self.usernameTextField.text andPassword:self.passwordTextField.text onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
       
        if(theErr == nil) {
           
            // Success
           
            [self performSelector:@selector(changePasswordTrial:) withObject:[NSNumber numberWithInteger:1] afterDelay:1.0f];
        }
        else {
           
            // Failure
           
            NSLog(@"Cannot log in with error: %@", [theErr.userInfo objectForKey:NSLocalizedDescriptionKey]);
        }
    }];
}


When I run this code, I get these messages printed to the console:
Code: [Select]
2014-03-06 15:11:06.414 Benjamin[33842:70b] invokeHttpMethod: Will send HTTP POST to url: https://metakite.fatfractal.com/benjamin/ff/login
2014-03-06 15:11:06.545 Benjamin[33842:70b] canAuthenticateAgainstProtectionSpace <NSURLProtectionSpace: 0xc58cf50>: Host:metakite.fatfractal.com, Server:https, Auth-Scheme:NSURLAuthenticationMethodServerTrust, Realm:(null), Port:443, Proxy:NO, Proxy-Type:(null) called
2014-03-06 15:11:07.712 Benjamin[33842:70b] Beginning Trial 1 with password: password1
2014-03-06 15:11:07.712 Benjamin[33842:70b] invokeHttpMethod: Will send HTTP POST to url: https://metakite.fatfractal.com/benjamin/ff/ext/changePassword
2014-03-06 15:11:07.870 Benjamin[33842:70b] Trial 1 Succeeded
2014-03-06 15:11:08.871 Benjamin[33842:70b] Beginning Trial 2 with password: password2
2014-03-06 15:11:08.871 Benjamin[33842:70b] invokeHttpMethod: Will send HTTP POST to url: https://metakite.fatfractal.com/benjamin/ff/ext/changePassword
2014-03-06 15:11:09.021 Benjamin[33842:70b] HTTP POST failed with response code 401
Status message from backend is Your session has expired; please re-login
2014-03-06 15:11:09.021 Benjamin[33842:70b] Trial 2 Failed with error: HTTP POST failed with response code 401
Status message from backend is Your session has expired; please re-login

I also get these messages logged to my FatFractal server logs:
Code: [Select]
INFO - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:57.292Z - /ff/login - NoUser - Thread[pool-3-thread-23,5,main] - REQUEST: METHOD: POST, HEADERS: {X-Forwarded-Protocol=https, Accept-Language=en-us, Cookie=sessionId=(null); userGuid=(null), Host=metakite.fatfractal.com, Content-Length=108, Accept-Encoding=gzip, deflate, X-Real-IP=98.220.168.122, X-Forwarded-For=98.220.168.122, User-Agent=Benjamin/2.0.4 CFNetwork/672.0.8 Darwin/13.1.0, Connection=close, Content-Type=application/json, Accept=*/*}, PARAMS: {}, BODY: {"credential":{"authDomain":"LOCAL","userName":"test","password":"########"}}
INFO - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:57.310Z - /ff/login - NoUser - Thread[pool-3-thread-23,5,main] - RESPONSE :   METHOD: POST, HEADERS: [Content-Type=application/json, Set-Cookie=userGuid=c4KvxgHG6obdfypIuI6Xf4; max-age=129600; expires=8 Mar 2014 08:10:57 GMT; domain=metakite.fatfractal.com; path=/benjamin/, Set-Cookie=sessionId=7C0TOZR-qk4ItelkvV4Cl7; max-age=129600; expires=8 Mar 2014 08:10:57 GMT; domain=metakite.fatfractal.com; path=/benjamin/], CODE: 200
INFO - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:58.386Z - /ff/ext/changePassword - NoUser - Thread[pool-3-thread-13,5,main] - REQUEST: METHOD: POST, HEADERS: {X-Forwarded-Protocol=https, Accept-Language=en-us, Cookie=sessionId=7C0TOZR-qk4ItelkvV4Cl7; userGuid=c4KvxgHG6obdfypIuI6Xf4, Host=metakite.fatfractal.com, Content-Length=30, Accept-Encoding=gzip, deflate, X-Real-IP=98.220.168.122, X-Forwarded-For=98.220.168.122, User-Agent=Benjamin/2.0.4 CFNetwork/672.0.8 Darwin/13.1.0, Connection=close, Content-Type=application/json, Accept=*/*}, PARAMS: {}, BODY: {"password":"########"}
INFO - com.fatfractal.noserver.actions.Extension - 2014-03-06T20:10:58.392Z - /ff/ext/changePassword - test - Thread[pool-3-thread-13,5,main] - Executing script require ('scripts/ChangePassword.js').changePassword();
INFO - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:58.415Z - /ff/ext/changePassword - test - Thread[pool-3-thread-13,5,main] - RESPONSE :   METHOD: POST, HEADERS: [Content-Type=application/json], CODE: 200
INFO - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:59.609Z - /ff/ext/changePassword - NoUser - Thread[pool-3-thread-10,5,main] - REQUEST: METHOD: POST, HEADERS: {X-Forwarded-Protocol=https, Accept-Language=en-us, Cookie=sessionId=7C0TOZR-qk4ItelkvV4Cl7; userGuid=c4KvxgHG6obdfypIuI6Xf4, Host=metakite.fatfractal.com, Content-Length=30, Accept-Encoding=gzip, deflate, X-Real-IP=98.220.168.122, X-Forwarded-For=98.220.168.122, User-Agent=Benjamin/2.0.4 CFNetwork/672.0.8 Darwin/13.1.0, Connection=close, Content-Type=application/json, Accept=*/*}, PARAMS: {}, BODY: {"password":"########"}
INFO - com.fatfractal.noserver.security.AppManagerUsersAndGroups - 2014-03-06T20:10:59.611Z - /ff/ext/changePassword - NoUser - Thread[pool-3-thread-10,5,main] - sessionId 7C0TOZR-qk4ItelkvV4Cl7 not current for test - invalidating cache and trying again
INFO - com.fatfractal.noserver.security.AppManagerUsersAndGroups - 2014-03-06T20:10:59.619Z - /ff/ext/changePassword - NoUser - Thread[pool-3-thread-10,5,main] - after cache invalidation, sessionId 7C0TOZR-qk4ItelkvV4Cl7 is STILL NOT current
WARN - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:59.620Z - /ff/ext/changePassword - NoUser - Thread[pool-3-thread-10,5,main] - com.fatfractal.noserver.exceptions.FFHttpExceptionUnauthorized: Your session has expired; please re-login
INFO - com.fatfractal.noserver.actions.AbstractAction - 2014-03-06T20:10:59.621Z - /ff/ext/changePassword - NoUser - Thread[pool-3-thread-10,5,main] - RESPONSE :   METHOD: POST, HEADERS: [Content-Type=application/json, Set-Cookie=userGuid=deleted; max-age=0; expires=6 Mar 2014 20:10:59 GMT; domain=metakite.fatfractal.com; path=/benjamin/, Set-Cookie=sessionId=deleted; max-age=0; expires=6 Mar 2014 20:10:59 GMT; domain=metakite.fatfractal.com; path=/benjamin/], CODE: 401

Like I said, it seems like a pretty vanilla use of the FatFractal SDK, so I'm a bit confused why this should fail. Thanks for taking a look.

kevin@fatfractal.com

  • Administrator
  • *****
  • Posts: 56
    • View Profile
Re: resetPassword() and expired session error
« Reply #5 on: March 06, 2014, 06:42:59 PM »
I am pretty sure the issue is that you need to login with the new password after it is changed.

I have to run off to a meeting, but will add your code to my test harness to verify later tonight...

Kevin

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: resetPassword() and expired session error
« Reply #6 on: March 07, 2014, 04:23:19 AM »
@kevin that's right, need to login with the new password after it is changed, as when the actual credentials are changed on the backend, existing 'session tokens' are invalidated.

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
Re: resetPassword() and expired session error
« Reply #7 on: March 07, 2014, 08:43:15 AM »
Quote
@kevin that's right, need to login with the new password after it is changed, as when the actual credentials are changed on the backend, existing 'session tokens' are invalidated.
That's unfortunate, but thank you for the clarification.

I say it's unfortunate because my app syncs data asynchronously between the client and server. If a sync is running when the user decides to change his password, the sync is probably going to fail with errors when a call to FatFractal occurs during the window between resetting the password and logging back in with the new password.

I don't know all the ins and outs of how you authenticate users, but if there's a way to securely keep a user logged in after he changes his password, it would make my life easier. As it stands, I suppose I'm going to have to wrap every call to FatFractal in an if statement to check if a password is being changed, and then abort the sync if it is. That's a nuisance not just for me, but also for the user.

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: resetPassword() and expired session error
« Reply #8 on: March 07, 2014, 11:09:36 AM »
I'm a bit confused. Are you requesting that a session which was established with one password should continue to work if the password is subsequently changed somewhere else?

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
Re: resetPassword() and expired session error
« Reply #9 on: March 07, 2014, 11:33:35 AM »
Yes, that's what I'm requesting. Basically, if a user starts an authenticated session, I want the authenticated session to remain valid until the user logs out of that session.

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: resetPassword() and expired session error
« Reply #10 on: March 07, 2014, 12:17:20 PM »
Ok two things:
1) A 401 response code is always sent for authentication failures, including if a request is made with an expired session. I'd say it's probably good idea to be checking for that code anyway
2) you could implement the same reset password workflow but within the iOS app so you know when it's happening and can login with new password straight away
3) I will look into option of keeping old sessions as valid for some period of time

Bottom line though is that, at some time, the session needs to expire...

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
Re: resetPassword() and expired session error
« Reply #11 on: March 07, 2014, 12:36:29 PM »
Quote
1) A 401 response code is always sent for authentication failures, including if a request is made with an expired session. I'd say it's probably good idea to be checking for that code anyway

Hmm... I hadn't considered that. This might render my request moot. How long does it take for a session to expire presently?

kevin@fatfractal.com

  • Administrator
  • *****
  • Posts: 56
    • View Profile
Re: resetPassword() and expired session error
« Reply #12 on: March 07, 2014, 12:50:16 PM »
Session expiration is under your control via FFDL...

You can see more in the FFDL documentation here: http://fatfractal.com/prod/docs/reference/#ffdl (expand the Security parameters button).

Code: [Select]
SET SessionTimeoutInSeconds <timeout>
# Sets the amount of time after login that a session times out. Time is given in seconds. Default is 129600, i.e. 36 hours.
# Sessions are reset every time the user authenticates with credentials.

Kevin

metakite

  • Jr. Member
  • **
  • Posts: 54
    • View Profile
Re: resetPassword() and expired session error
« Reply #13 on: March 07, 2014, 07:21:27 PM »
@gkc wrote:
Quote
1) A 401 response code is always sent for authentication failures, including if a request is made with an expired session. I'd say it's probably good idea to be checking for that code anyway

He was right. The better solution than what I requested above is to check for authentication errors. If any authentication errors are received, log back into FatFractal and try again. This applies not only to the changePassword script discussed above, but to any calls to FatFractal.

In iOS, that's easier said than done, though. You pretty quickly end up with lots of nested blocks and repeated code. I came up with a solution involving recursive blocks, but since dealing with asynchronous recursive blocks can be pretty confusing to deal with, I thought I'd share some code in case it can help someone else.

In your class...

1. Define a type for the recursive block:
Code: [Select]
typedef void (^RetryBlock)(NSInteger numberOfRetriesPermitted);
2. Define some properties:
Code: [Select]
@property (nonatomic, strong, readonly) FatFractal *fatFractal;
@property (nonatomic, strong) NSMutableArray *retryBlocks;          // Used to store a reference to asynchronous blocks so they aren't released when they go out of scope

3. Init your properties somewhere. Perhaps an init method, or perhaps viewDidLoad:
Code: [Select]
self.fatFractal = [[FatFractal alloc] initWithBaseUrl:@"https://your_domain.fatfractal.com/your_application"];
self.retryBlocks = [[NSMutableArray alloc] init];

4. Write a method to call the changePassword server extension above. This is where two main complications come into play.

First, we want to call FatFractal in a block so that we don't have to repeat a lot of code, but we need to be careful to avoid retain cycles since we're calling the block from within itself.

Second, because the FatFractal calls are asynchronous, we need to be careful that the block isn't released before our completion handlers run. (If the blocks were released we'd get a segmentation fault since we'd be trying to execute a NULL block.) To avoid this problem we store a reference to the block in the retryBlocks property where it won't go out of scope. There might be a better way of keeping a reference to the block, but this gets the job done. Just be sure to remove it from the array once you're done with the block.
Code: [Select]
- (void)setLoggedInUserPassword:(NSString*)newPassword {
   
    NSDictionary *passwordDictionary = [NSDictionary dictionaryWithObject:newPassword forKey:@"password"];
   
   
    // We're using a recursive block to call FatFractal so that we can easily retry in case of an authentication error.
    // All the hoops below are necessary in order to avoid retain cycles in the recursive blocks and
    // to ensure that the block is not prematurely released (since it is asynchronous).
    __weak __block RetryBlock recursiveRetryBlock;
    RetryBlock strongRetryBlock;
    recursiveRetryBlock = strongRetryBlock = ^void(NSInteger numberOfRetriesPermitted){
       
        [self.fatFractal postObj:passwordDictionary toExtension:kMKSFatFractalConnectorServerExtensionChangePassword onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
           
            if(theErr == nil) {
               
                // Success
               
                //
                // This is where you can store the password in the keychain, and do whatever else you need to do after you change a password.
                //
               
                [self.retryBlocks removeObjectIdenticalTo:recursiveRetryBlock]; // Remove the retry block since we don't need a reference to it anymore.
            }
            else if(numberOfRetriesPermitted >= 1 && [theErr code] == 401) {
               
                //
                // There was an authentication error, which indicates that the user's session has expired.
                // Re-authenticate and try the request again.
                //
                [self.fatFractal logout];
               
                NSString *aUsername = ... // Get the user's username from wherever you stored it
                NSString *aPassword = ... // Get the user's password from wherever you stored it
               
                [self.fatFractal loginWithUserName:aUsername andPassword:aPassword onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
                   
                    if(theErr == nil) {
                       
                        // Success
                       
                        // Now that we've logged in and renewed our session, we can try to reset the password again.
                        NSInteger numberOfRetriesLeft = numberOfRetriesPermitted - 1;
                        recursiveRetryBlock(numberOfRetriesLeft);
                    }
                    else {
                       
                        // Failure
                       
                        //
                        // This is where you can show the user an error, and do whatever else you need to do when a call to FatFractal fails.
                        //
                       
                        [self.retryBlocks removeObjectIdenticalTo:recursiveRetryBlock]; // Remove the retry block since we don't need a reference to it anymore.
                    }
                }];
            }
            else {
               
                // Failure
               
               
                //
                // This is where you can show the user an error, and do whatever else you need to do when a call to FatFractal fails.
                //
               
                [self.retryBlocks removeObjectIdenticalTo:recursiveRetryBlock]; // Remove the retry block since we don't need a reference to it anymore.
            }
        }];
    };
    [self.retryBlocks addObject:[recursiveRetryBlock copy]];    // Copy the block to the heap and save a reference so that it's not deleted when it goes out of scope.
    recursiveRetryBlock(1);  // Allow only one retry.
}

I hope this saves someone some time. If you see any improvements that can be made, please share.

gkc

  • Administrator
  • *****
  • Posts: 375
    • View Profile
Re: resetPassword() and expired session error
« Reply #14 on: March 08, 2014, 04:37:17 AM »
Nice ... in thinking about this, it seems to me like it would help a lot if you were able to supply a delegate which will have some method (for example didReceiveAuthenticationFailure:) which would be invoked in the event of any auth failure ... what do you think ?

 

Copyright © FatFractal customer forums