24

I'm implementing JSON Web Token authentication on the iOS (7) cient-side. It's working nicely. My app rceives tokens, and can make authenticated calls to my server with them.

Now, I want my client side code to check for an expiration date on the token so it can know when to re-authenticate. Checking for the expiration date on a JWT auth token is straightforward. The authorization token is 3 base64 encoded JSON blobs, separated by a '.' - The expiration timestamp is in the middle blob, in a field called ext. It's seconds since unix epoch.

So my code's looking like so:

- (NSDate*) expirationDate
{
    if ( !_tokenAppearsValid ) return nil;

    if ( !_parsedExpirationDate )
    {
        //
        //  Token is three base64 encoded payloads separated by '.'
        //  The payload we want is the middle one, which is a JSON dict, with
        //  'exp' being the unix seconds timestamp of the expiration date
        //  Returning nil is appropriate if no 'exp' is findable
        //

        NSArray *components = [self.token componentsSeparatedByString:@"."];

        NSString *payload = components[1];

        NSData* payloadJsonData = [[NSData alloc]
            initWithBase64EncodedString:payload
            options:NSDataBase64DecodingIgnoreUnknownCharacters];

        NSError* jsonError = nil;
        NSDictionary* payloadJson = [NSJSONSerialization JSONObjectWithData:payloadJsonData options:0 error:&jsonError];
        if ( payloadJson )
        {
            if ( payloadJson[@"exp"] )
            {
                NSTimeInterval timestampSeconds = [payloadJson[@"exp"] doubleValue];
                _expirationDate = [NSDate dateWithTimeIntervalSince1970:timestampSeconds];
            }
        }

        _parsedExpirationDate = YES;
    }

    return _expirationDate;
}

The problem is simple. The middle base64 blob, when parsed by NSData -initWithBase64EncodedString is nil - and that's bad.

I've checked the base64 blob and it seems to be valid. My server's returning dummy data for the moment so here's an example blob: eyJlbWFpbCI6ImZvb0BiYXIuYmF6IiwiYWNjb3VudElkIjoiMTIzNDUtNjc4OTAtYmFyLWJheiIsImV4cCI6MTM5MDkxNTAzNywiaWF0IjoxMzkwOTE0MTM3fQ

It decodes to:

{"email":"foo@bar.baz","accountId":"12345-67890-bar-baz","exp":1390915037,"iat":1390914137}

I tested it here: http://www.base64decode.org

I've used NSData's base64 methods elswhere in my app with success - I don't think I'm doing anything particularly broken here. But I'm all ears! Any ideas?

TomorrowPlusX
  • 1,205
  • 2
  • 14
  • 27

5 Answers5

41

Your Base64 string is not valid. It must be padded with = characters to have a length that is a multiple of 4. In your case: "eyJlbWFp....MTM3fQ==".

With this padding, initWithBase64EncodedString decodes the Base64 string correctly.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • 3
    Padding of Base64 data is not mandatory , see section 3.2 of the RFC : http://www.faqs.org/rfcs/rfc4648.html – droussel May 22 '15 at 17:37
  • 3
    @droussel: Thank you for the feedback. The methods from the NSData class do not accept a Base64 string which is not padded to a length which is a multiple of 4. I cannot judge if that is a bug in Apple's frameworks or intended. – Martin R May 22 '15 at 17:44
  • 6
    Yes, you are right that initWithBase64EncodedString does not accept unpadded base64. This is not a bug but they could provide the capability as an option though. In the mean time, you can always pad the string yourself like this : int padLenght = (4 - (base64.length % 4)) % 4; NSString *paddedBase64 = [NSString stringWithFormat:@"%s%.*s", [base64 UTF8String], padLenght, "=="]; – droussel May 22 '15 at 18:20
  • @droussel You are incorrect. The standard says "Implementations MUST include appropriate pad characters at the end of encoded data unless the specification referring to this document explicitly states otherwise. The base64 and base32 alphabets use padding, as described below in sections 4 and 6". Also see section 4. The only time Base64 encoding can skip padding is if the length of the data is already explicitly known (or passed separately). That is not the case here so stripping the padding is invalid. – russbishop Jun 07 '16 at 00:27
  • @russbishop JWT requires unpadded base64. It's specified in https://www.rfc-editor.org/rfc/rfc7515.txt and allowed per section 3.2 of rfc 4648 : In some circumstances, the use of padding ("=") in base-encoded data is not required or used. In the general case, when assumptions about the size of transported data cannot be made, padding is required to yield correct decoded data. – droussel Nov 04 '16 at 20:07
21

Although Martin's answer is correct, here is a quick and correct(!) way to fix the problem:

NSString *base64String = @"<the token>";
NSUInteger paddedLength = base64String.length + (4 - (base64String.length % 4));
NSString* correctBase64String = [base64String stringByPaddingToLength:paddedLength withString:@"=" startingAtIndex:0];
christopher
  • 520
  • 3
  • 10
  • 3
    This answer creates in some cases a padding of "====". There is a "% 4" missing at the end of line two. – Klaas May 04 '18 at 18:41
  • Klass is correct. The paddedLength is not computed correctly. Here is the correct code for line #2 (as suggested by Klass) NSUInteger paddedLength = base64NotPaddedString.length + ((4 - (base64NotPaddedString.length % 4)) % 4); – Joshua Jul 20 '21 at 12:21
4

Here is a solution that pads the Base-64 string appropriately and works in iOS 4+:

NSData+Base64.h

@interface NSData (Base64)

/**
 Returns a data object initialized with the given Base-64 encoded string.
 @param base64String A Base-64 encoded NSString
 @returns A data object built by Base-64 decoding the provided string. Returns nil if the data object could not be decoded.
 */
- (instancetype) initWithBase64EncodedString:(NSString *)base64String;

/**
 Create a Base-64 encoded NSString from the receiver's contents
 @returns A Base-64 encoded NSString
 */
- (NSString *) base64EncodedString;

@end

NSData+Base64.m

@interface NSString (Base64)

- (NSString *) stringPaddedForBase64;

@end

@implementation NSString (Base64)

- (NSString *) stringPaddedForBase64 {
    NSUInteger paddedLength = self.length + (self.length % 3);
    return [self stringByPaddingToLength:paddedLength withString:@"=" startingAtIndex:0];
}

@end

@implementation NSData (Base64)

- (instancetype) initWithBase64EncodedString:(NSString *)base64String {
    return [self initWithBase64Encoding:[base64String stringPaddedForBase64]];
}

- (NSString *) base64EncodedString {
    return [self base64Encoding];
}

@end
phatmann
  • 18,161
  • 7
  • 61
  • 51
3

A Swift version of Paul's answer

func paddedBase64EncodedString(encodedString: String) -> String
{
    let encodedStringLength = encodedString.characters.count
    let paddedLength = encodedStringLength + (4 - (encodedStringLength % 4))
    let paddedBase64String = encodedString.stringByPaddingToLength(paddedLength,
                                                                    withString: "=",
                                                                    startingAtIndex: 0)

    return paddedBase64String
}
Community
  • 1
  • 1
Simo
  • 2,172
  • 3
  • 19
  • 23
  • 1
    This answer creates in some cases a padding of "====". There is a "% 4" missing at the end of line two. – Klaas May 04 '18 at 18:46
0

I faced the same issue, but resolved it by adding == at end of string

base64UserStr = NSString(format: "%@%@", base64UserStr,"==") as String
let decodedData = NSData(base64EncodedString: base64UserStr, options: NSDataBase64DecodingOptions.init(rawValue: 0))


if (decodedData != nil)
{
    let decodedString = NSString(data: decodedData!, encoding: NSUTF8StringEncoding)

    print("Base 64 decode string is \(decodedString)")
}

This will definitely work.

Ash Furrow
  • 12,391
  • 3
  • 57
  • 92
Anita Nagori
  • 707
  • 1
  • 7
  • 21