Hijacking Mobile Service Authentication

Recently, while developing Score Predict I ran into a snafu where my iOS version was rejected by the Apple AppStore.  There were two reasons for this rejection; one of the reasons was not surprising, the other one, however, was.  According to Apple, if I chose to rely exclusively on Facebook authentication, I had to use my granted FB access for something else in addition to authentication; a lesson I had to learn.  This effectively met I could either speed up the timeline and implement the FB capable features.  This would mean pulling the Android version (which had already been approved) and likely cost me the entire football season.  So I opted to create a supplemental manual login for Facebook only.

The principle issue was, I was relying on the generated Facebook username, given to me by mobile services, as a means to link data together.  In addition, I had already implemented user level authentication on the sensitive services and I did not want to provide non-social endpoints for the manual users.  So what to do?

Well as I looked at what makes the client authenticated, I realized it was the combination of a generated user Id and what I came to learn was a JWT (JSON Web Token) token, I previously referred to this as a Zumo token.  In addition, it is widely known that you can give the client a pre created token from a social provider to be used.  So I summarized that the same could likely be said for the Auth token.  If I could create it myself, I could likely get Azure Mobile Services to use it and the two logins types could use the same endpoints.

To begin with this, I knew I would need to determine how I might create a JWT token.  Lucky for me, I stumbled across this article: http://www.thejoyofcode.com/Generating_your_own_ZUMO_auth_token_Day_8_.aspx

In reality, all this is a simple hashing algorithm where the salt is unknown to the under user, this is what allows the security to exist, being signed by the Master Key.  For my project I created the following NodeJS module to house this logic:

// require external node modules
var crypto = require("crypto");

// actual node module
module.exports = {
    createZumoToken: function(expiryDate, aud, userId, masterKey) {
        function base64(input) {
            return new Buffer(input, 'utf8').toString('base64');
        }

        function urlFriendly(input) {
            return input.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '');
        }

        function sign(input) {
            var key = crypto.createHash('sha256').update(masterKey + "JWTSig").digest('binary');
            var str = crypto.createHmac('sha256', key).update(input).digest('base64');
            return urlFriendly(str);
        }

        var s1 = '{"alg":"HS256","typ":"JWT","kid":0}';
        var j2 = {
            "exp":expiryDate.valueOf() / 1000,
            "iss":"urn:microsoft:windows-azure:zumo",
            "ver":2,
            "aud":aud,
            "uid":userId
        };

        var s2 = JSON.stringify(j2);
        var b1 = urlFriendly(base64(s1));
        var b2 = urlFriendly(base64(s2));
        var b3 = sign(b1 + "." + b2);

        return [b1,b2,b3].join(".");
    }
};

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

Then it gets down to it, it is very straightforward.  It relies on the crypto library which is included as part of the overall Node setup you are given for every Mobile Service JS backend project.

In terms of usage, following the creation of the user, that is after the record has been inserted, I simply create the token and add it to the response (below):

var expiry = new Date().setUTCDate(new Date().getUTCDate() + 30);
var aud = "";
var master = "";
var token = jwt.createZumoToken(expiry, aud, user.id, master);

complete({ id: user.id, token: token });

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

I have yet to understand how the validation of these tokens works, what it is looking at because, aside from the Master Key, the aud is not stored anywhere.  However, this appears to work, so for right now, that is enough.

To utilize this is no different than if you were working with Facebook.  Below is some Objective-C code which handles the response to the create_user endpoint.

-(void)handleSuccessfulUserCreateWithData:(NSDictionary *)data {
    // get the values from the data
    NSString *userId = [data objectForKey:@"id"];
    NSString *token = [data objectForKey:@"token"];
    
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setValue:userId forKey:@"UserId"];
    [defaults setValue:token forKey:@"Token"];
    [defaults synchronize];
    
    [self goToMainView];
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

You can see that we are simply storing the values as normal in the User Defaults.  They are then accessed through our getClient method from the ClientFactory class (below):

+ (MSClient *)getClient {
    NSURL *url = [NSURL URLWithString:@"https://scorepredict.azure-mobile.net"];
    MSClient *client = [MSClient clientWithApplicationURL:url
                                           applicationKey:@""];

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    if ([defaults objectForKey:@"UserId"] != nil && [defaults objectForKey:@"Token"] != nil) {
        NSString *userId = [defaults stringForKey:@"UserId"];
        NSString *token = [defaults stringForKey:@"Token"];

        MSUser *user = [[MSUser alloc] initWithUserId:userId];
        user.mobileServiceAuthenticationToken = token;
        client.currentUser = user;
    }

    return client;
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

This is all the same for both the manual and Facebook calls, thus the service works regardless of which login method is used.  The difference in the two is the UserId transmitted, which will be the generated Facebook username for the Facebook calls, but a randomly generated Guid for the manual calls.  This being the case, I ensured that a string is used to store the user associated with the data, thus joining is agnostic to this difference.

In reality, we are not really hijacking the login system, we are simply performing some impersonation.  I am still looking at the overall security of the Mobile Service with this approach, however, I feel that as long as the Master Key is kept secret, there should be little chance of a compromise.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s