#import "AFNetworking.h" #import "BaseAuthenticator.h" #import "../ios_uikit_bridge.h" #import "../utils.h" typedef void(^XSTSCallback)(NSString *xsts, NSString *uhs); @implementation MicrosoftAuthenticator - (void)acquireAccessToken:(NSString *)authcode refresh:(BOOL)refresh callback:(Callback)callback { callback(localize(@"login.msa.progress.acquireAccessToken", nil), YES); NSDictionary *data = @{ @"client_id": @"00000000402b5328", (refresh ? @"refresh_token" : @"code"): authcode, @"grant_type": refresh ? @"refresh_token" : @"authorization_code", @"redirect_url": @"https://login.live.com/oauth20_desktop.srf", @"scope": @"service::user.auth.xboxlive.com::MBI_SSL" }; AFHTTPSessionManager *manager = AFHTTPSessionManager.manager; [manager GET:@"https://login.live.com/oauth20_token.srf" parameters:data headers:nil progress:nil success:^(NSURLSessionDataTask *task, NSDictionary *response) { self.authData[@"msaRefreshToken"] = response[@"refresh_token"]; [self acquireXBLToken:response[@"access_token"] callback:callback]; } failure:^(NSURLSessionDataTask *task, NSError *error) { if (error.code == NSURLErrorDataNotAllowed) { // The account token is expired and offline self.authData[@"accessToken"] = @"offline"; callback(nil, YES); } else { callback(error.localizedDescription, NO); } }]; } - (void)acquireXBLToken:(NSString *)accessToken callback:(Callback)callback { callback(localize(@"login.msa.progress.acquireXBLToken", nil), YES); NSDictionary *data = @{ @"Properties": @{ @"AuthMethod": @"RPS", @"SiteName": @"user.auth.xboxlive.com", @"RpsTicket": accessToken }, @"RelyingParty": @"http://auth.xboxlive.com", @"TokenType": @"JWT" }; AFHTTPSessionManager *manager = AFHTTPSessionManager.manager; manager.requestSerializer = AFJSONRequestSerializer.serializer; [manager POST:@"https://user.auth.xboxlive.com/user/authenticate" parameters:data headers:nil progress:nil success:^(NSURLSessionDataTask *task, NSDictionary *response) { Callback innerCallback = ^(NSString* status, BOOL success) { if (success == NO) { callback(status, NO); return; } // Obtain XSTS for authenticating to Minecraft [self acquireXSTSFor:@"rp://api.minecraftservices.com/" token:response[@"Token"] xstsCallback:^(NSString *xsts, NSString *uhs){ if (xsts == nil) { callback(nil, NO); return; } self.authData[@"xuid"] = uhs; [self acquireMinecraftToken:uhs xstsToken:xsts callback:callback]; } callback:callback]; }; // Obtain XSTS for getting the Xbox gamertag [self acquireXSTSFor:@"http://xboxlive.com" token:response[@"Token"] xstsCallback:^(NSString *xsts, NSString *uhs){ if (xsts == nil) { callback(nil, NO); return; } [self acquireXboxProfile:uhs xstsToken:xsts callback:innerCallback]; } callback:callback]; } failure:^(NSURLSessionDataTask *task, NSError *error) { callback(error.localizedDescription, NO); }]; } - (void)acquireXSTSFor:(NSString *)replyingParty token:(NSString *)xblToken xstsCallback:(XSTSCallback)xstsCallback callback:(Callback)callback { callback(localize(@"login.msa.progress.acquireXSTS", nil), YES); NSDictionary *data = @{ @"Properties": @{ @"SandboxId": @"RETAIL", @"UserTokens": @[ xblToken ] }, @"RelyingParty": replyingParty, @"TokenType": @"JWT", }; AFHTTPSessionManager *manager = AFHTTPSessionManager.manager; manager.requestSerializer = AFJSONRequestSerializer.serializer; [manager POST:@"https://xsts.auth.xboxlive.com/xsts/authorize" parameters:data headers:nil progress:nil success:^(NSURLSessionDataTask *task, NSDictionary *response) { NSString *uhs = response[@"DisplayClaims"][@"xui"][0][@"uhs"]; xstsCallback(response[@"Token"], uhs); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSString *errorString; NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; if (errorData == nil) { callback(error.localizedDescription, NO); return; } NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData:errorData options:kNilOptions error:nil]; switch ((int)([errorDict[@"XErr"] longValue]-2148916230l)) { case 3: errorString = @"login.msa.error.xsts.noxboxacc"; break; case 5: errorString = @"login.msa.error.xsts.noxbox"; break; case 6: case 7: errorString = @"login.msa.error.xsts.krverify"; break; case 8: errorString = @"login.msa.error.xsts.underage"; break; default: errorString = [NSString stringWithFormat:@"%@\n\nUnknown XErr code, response:\n%@", error.localizedDescription, errorDict]; break; } callback(localize(errorString, nil), NO); }]; } - (void)acquireXboxProfile:(NSString *)xblUhs xstsToken:(NSString *)xblXsts callback:(Callback)callback { callback(localize(@"login.msa.progress.acquireXboxProfile", nil), YES); NSDictionary *headers = @{ @"x-xbl-contract-version": @"2", @"Authorization": [NSString stringWithFormat:@"XBL3.0 x=%@;%@", xblUhs, xblXsts] }; AFHTTPSessionManager *manager = AFHTTPSessionManager.manager; [manager GET:@"https://profile.xboxlive.com/users/me/profile/settings?settings=PublicGamerpic,Gamertag" parameters:nil headers:headers progress:nil success:^(NSURLSessionDataTask *task, NSDictionary *response) { self.authData[@"profilePicURL"] = [NSString stringWithFormat:@"%@&h=120&w=120", response[@"profileUsers"][0][@"settings"][0][@"value"]]; self.authData[@"xboxGamertag"] = response[@"profileUsers"][0][@"settings"][1][@"value"]; callback(nil, YES); } failure:^(NSURLSessionDataTask *task, NSError *error) { callback(error.localizedDescription, NO); }]; } - (void)acquireMinecraftToken:(NSString *)xblUhs xstsToken:(NSString *)xblXsts callback:(Callback)callback { callback(localize(@"login.msa.progress.acquireMCToken", nil), YES); NSDictionary *data = @{ @"identityToken": [NSString stringWithFormat:@"XBL3.0 x=%@;%@", xblUhs, xblXsts] }; AFHTTPSessionManager *manager = AFHTTPSessionManager.manager; manager.requestSerializer = AFJSONRequestSerializer.serializer; [manager POST:@"https://api.minecraftservices.com/authentication/login_with_xbox" parameters:data headers:nil progress:nil success:^(NSURLSessionDataTask *task, NSDictionary *response) { self.authData[@"accessToken"] = response[@"access_token"]; [self checkMCProfile:response[@"access_token"] callback:callback]; } failure:^(NSURLSessionDataTask *task, NSError *error) { callback(error.localizedDescription, NO); }]; } - (void)checkMCProfile:(NSString *)mcAccessToken callback:(Callback)callback { self.authData[@"expiresAt"] = @((long)[NSDate.date timeIntervalSince1970] + 86400); callback(localize(@"login.msa.progress.checkMCProfile", nil), YES); NSDictionary *headers = @{ @"Authorization": [NSString stringWithFormat:@"Bearer %@", mcAccessToken] }; AFHTTPSessionManager *manager = AFHTTPSessionManager.manager; manager.requestSerializer = AFJSONRequestSerializer.serializer; [manager GET:@"https://api.minecraftservices.com/minecraft/profile" parameters:nil headers:headers progress:nil success:^(NSURLSessionDataTask *task, NSDictionary *response) { NSString *uuid = response[@"id"]; self.authData[@"profileId"] = [NSString stringWithFormat:@"%@-%@-%@-%@-%@", [uuid substringWithRange:NSMakeRange(0, 8)], [uuid substringWithRange:NSMakeRange(8, 4)], [uuid substringWithRange:NSMakeRange(12, 4)], [uuid substringWithRange:NSMakeRange(16, 4)], [uuid substringWithRange:NSMakeRange(20, 12)] ]; self.authData[@"profilePicURL"] = [NSString stringWithFormat:@"https://mc-heads.net/head/%@/120", self.authData[@"profileId"]]; self.authData[@"oldusername"] = self.authData[@"username"]; self.authData[@"username"] = response[@"name"]; callback(nil, [super saveChanges]); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSData *errorData = error.userInfo[AFNetworkingOperationFailingURLResponseDataErrorKey]; NSDictionary *errorDict = [NSJSONSerialization JSONObjectWithData: errorData options:kNilOptions error:nil]; if ([errorDict[@"error"] isEqualToString:@"NOT_FOUND"]) { // If there is no profile, use the Xbox gamertag as username with Demo mode self.authData[@"profileId"] = @"00000000-0000-0000-0000-000000000000"; self.authData[@"username"] = [NSString stringWithFormat:@"Demo.%@", self.authData[@"xboxGamertag"]]; callback(@"DEMO", [super saveChanges]); callback(nil, YES); return; } callback([[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding], NO); }]; } - (void)loginWithCallback:(Callback)callback { [self acquireAccessToken:self.authData[@"input"] refresh:NO callback:callback]; } - (void)refreshTokenWithCallback:(Callback)callback { if ([NSDate.date timeIntervalSince1970] > [self.authData[@"expiresAt"] longValue]) { [self acquireAccessToken:self.authData[@"msaRefreshToken"] refresh:YES callback:callback]; } else { callback(nil, YES); } } @end