Skip to content

Instantly share code, notes, and snippets.

@andyzinsser
Last active October 18, 2024 17:15
Show Gist options
  • Save andyzinsser/8044165 to your computer and use it in GitHub Desktop.
Save andyzinsser/8044165 to your computer and use it in GitHub Desktop.
Full flow of authenticating a Game Center Player on a third party server.
// request Game Center verification data for localPlayer from apple
- (void)authenticateGameCenterPlayer:(GKLocalPlayer *)localPlayer
{
[localPlayer generateIdentityVerificationSignatureWithCompletionHandler:^(NSURL *publicKeyUrl, NSData *signature, NSData *salt, uint64_t timestamp, NSError *error) {
if (error) {
NSLog(@"ERROR: %@", error);
}
else {
// package data to be sent to server for verification
NSDictionary *params = @{@"publicKeyUrl": publicKeyUrl,
@"timestamp": [NSString stringWithFormat:@"%llu", timestamp],
@"signature": [signature base64EncodedStringWithOptions:0],
@"salt": [salt base64EncodedStringWithOptions:0],
@"playerID": localPlayer.playerID,
@"bundleID": [[NSBundle mainBundle] bundleIdentifier]};
// setup AFNetworking request to send the data to the server
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager POST:APILinkWithGameCenterURL # update to your own service URL
parameters:params
constructingBodyWithBlock:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@", responseObject);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"%@", error);
}];
}
}];
}
# NOTE: using django rest framework
class authenticateWithGameCenter(APIView):
def post(self, request):
response = {'errors': []}
# validation
publicKeyUrl = request.DATA.get('publicKeyUrl', None)
if publicKeyUrl is None:
response['errors'].append('publicKeyUrl is required.')
signature = request.DATA.get('signature', None)
if signature is None:
response['errors'].append('signature is required.')
salt = request.DATA.get('salt', None)
if salt is None:
response['errors'].append('salt is required.')
timestamp = request.DATA.get('timestamp', None)
if timestamp is None:
response['errors'].append('timestamp is required.')
playerID = request.DATA.get('playerID', None)
if playerID is None:
response['errors'].append('playerID is required.')
bundleID = request.DATA.get('bundleID', None)
if bundleID is None:
response['errors'].append('bundleID is required.')
if len(response['errors']) > 0:
return APIResponse(response)
decoded_sig = signature.decode('base64')
decoded_salt = salt.decode('base64')
# Download and read the .cer from apple and extract the public key
r = requests.get(publicKeyUrl, stream=True)
local_filename = publicKeyUrl.split('/')[-1]
with open(local_filename, 'wb') as f:
for chunk in r.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
f.flush()
der = open(local_filename, 'r').read()
x509 = crypto.load_certificate(crypto.FILETYPE_ASN1, der)
payload = playerID.encode('UTF-8') + bundleID.encode('UTF-8') + struct.pack('>Q', int(timestamp)) + decoded_salt
try:
verified = crypto.verify(x509, decoded_sig, payload, 'sha1')
logger.info('Successfully verified certificate with signaure')
except Exception as err:
logger.info(err)
response['errors'].append(err)
# TODO:
# Verify that a hash value of the payload matches the signature parameter provided by apple.
# If the generated and retrieved signatures match, the local player has been authenticated.
# Get or create a user for the given playerID
# authenticate a session for this player
# return the user
return APIResponse(response)
@swors
Copy link

swors commented Mar 18, 2014

Is publicKeyUrl stable?
I don't want to download the cer file every time a player log in.

@Maximusya
Copy link

The only hints can be found in HTTP response headers. But it is still unclear without clear statement in documentation, whether the headers can be trusted.
Google at least states in the docs regarding id_token validation:

Since Google changes its public keys only infrequently (on the order of once per day), you can cache them and, in the vast majority of cases, perform local validation much more efficiently.

And Java Google API contains a Manager that downloads and caches certificates based on HTTP headers values.

Also note that even though cache headers or certificate "Valid till" or "Expires" attributes may hint you to cache the certificate, nothing prevents Apple (or Google) from changing the certificate at will - and new user data will get signed by a new certificate - not the one cached.

@johnou
Copy link

johnou commented Nov 18, 2016

@Maximusya isn't that the point of having a variable publicKeyUrl..? Current url is https://static.gc.apple.com/public-key/gc-prod-2.cer and if that needs to be revoked or renewed the new certificate would presumably be https://static.gc.apple.com/public-key/gc-prod-3.cer and so on..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment