Search code examples
iosobjective-cappauth

AppAuth-iOS-ObjC: failing at archive/unarchive of OIDAuthState


I'd like to use AppAuth's performActionWithFreshTokens method. This requires using the OIDAuthState returned by the authStateByPresentingAuthorizationRequest when generating access and refresh tokens. I want to save that OIDAuthState in the Keychain for security reasons, then use it at a later time when a token refresh is necessary. What I can't seem to do is properly archive and unarchive the saved value from the Keychain.

I have two methods: (1) used to convert the OIDAuthState to an NSString to store to the Keychain and (2) used to convert the NSString from the Keychain to an OIDAuthState. Here is the first:

NSData *checkEncoding;
- (NSString *)authStateToString:(OIDAuthState *)authState {
    NSError *errRet;
    //NSData *authStateData = [NSKeyedArchiver archivedDataWithRootObject:authState];
    NSData *authStateData = [NSKeyedArchiver archivedDataWithRootObject:authState
        requiringSecureCoding:NO error:&errRet];
    checkEncoding = authStateData;
    NSString *authString = [[NSString alloc] initWithData:authStateData
        encoding:NSUnicodeStringEncoding];
    NSLog(@"ToString length = %ld, authStateData length = %ld",
        (unsigned long)authString.length, (unsigned long)authStateData.length);
    return authString;
}

And here is the second:

- (OIDAuthState *)stringToAuthState:(NSString *)authString {
    NSError *errRet;
    NSData *authStateData = [authString dataUsingEncoding:NSUnicodeStringEncoding allowLossyConversion:NO];
    NSLog(@"ToAuthState length = %ld, authStateData length = %ld, checkEncoding = %d",
        (unsigned long)authString.length, (unsigned long)authStateData.length, [authStateData isEqualToData:checkEncoding]);
    OIDAuthState *authState = [NSKeyedUnarchiver unarchivedObjectOfClass:[OIDAuthState class]
        fromData:authStateData error:&errRet];
    //OIDAuthState *authState = [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:authStateData error:&errRet];
    //OIDAuthState *authState = [NSKeyedUnarchiver unarchiveObjectWithData:authStateData];
    /* NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:authStateData];
    OIDAuthState *authState = [unarchiver decodeObject];
    [unarchiver finishDecoding]; */
    return authState;
}

(The multiple tries at unarchiving don't come into play since they all fail and the uncommented version is probably the right/undeprecated one.)

Note the NSLog statements in each, which seem to point out the problem. For the authStateToString NSLog, the output is "ToString length = 7930, authStateData length = 15860". For the stringToAuthState NSLog, the output is "ToAuthState length = 7930, authStateData length = 15862, checkEncoding = 0". The NSStrings in both methods are equal according to isEqualToString.

So, I'm guessing that the OIDAuthState can't be regenerated because the conversion for the NSString versions is occuring but isn't regenerated the same NSData representation; the encoded version is 15860 bytes while the decoded version is 15862 bytes and the isEqualToData returns false.

I do note that the OIDAuthState class NSKeyedArchiver method encodeWithCoder is called as expected when encoding the state. The corresponding NSKeyedUnarchiver method initWithCoder is not called but that's not surprising since the NSData being passed is probably not valid.

I've tried multiple dataUsingEncoding encoding types with most failing (the resulting NSData is zero-length). NSUnicodeStringEncoding is the only one that seemed to produce appropriate length NSData instances. I've tried setting 'requiringSecureCoding' to both YES and NO. I've tried setting 'allowLossyConversion' to both YES and NO.

I am open to other methods of persisting the OIDAuthState but it seems what I've done should be valid. Can someone suggest why I can't seem to correctly unarchive?


Solution

  • I ended up implementing a different Keychain approach, using the Apple-provided KeychainItemWrapper to simplify Keychain operations.

    - (void)storeAuthState:(OIDAuthState *)authState {
        NSError *errRet;
        NSData *authStateData = [NSKeyedArchiver archivedDataWithRootObject:authState
            requiringSecureCoding:NO error:&errRet];
        KeychainItemWrapper *keychainItem =
            [[KeychainItemWrapper alloc] initWithIdentifier:@"<your ID here>" accessGroup:nil];
        [keychainItem setObject:authStateData forKey:(id)kSecAttrAccount];
    }
    - (OIDAuthState *)retrieveAuthState {
        NSError *errRet;
        KeychainItemWrapper *keychainItem =
            [[KeychainItemWrapper alloc] initWithIdentifier:@"<your ID here>" accessGroup:nil];
        NSData *data = [keychainItem objectForKey:(id)kSecAttrAccount];
        OIDAuthState *authState = [NSKeyedUnarchiver unarchivedObjectOfClass:[OIDAuthState class]
            fromData:data error:&errRet];
        return authState;
    }