Skip to content

fix: fix upload to appstore for accounts without 2 factor authentication #4547

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,7 @@ $injector.require("qrCodeTerminalService", "./services/qr-code-terminal-service"
$injector.require("testInitializationService", "./services/test-initialization-service");

$injector.require("networkConnectivityValidator", "./helpers/network-connectivity-validator");

$injector.require("applePortalSessionService", "./services/apple-portal/apple-portal-session-service");
$injector.require("applePortalCookieService", "./services/apple-portal/apple-portal-cookie-service");
$injector.require("applePortalApplicationService", "./services/apple-portal/apple-portal-application-service");
11 changes: 6 additions & 5 deletions lib/commands/appstore-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export class ListiOSApps implements ICommand {
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];

constructor(private $injector: IInjector,
private $itmsTransporterService: IITMSTransporterService,
private $applePortalApplicationService: IApplePortalApplicationService,
private $logger: ILogger,
private $projectData: IProjectData,
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
Expand All @@ -31,13 +31,14 @@ export class ListiOSApps implements ICommand {
password = await this.$prompter.getPassword("Apple ID password");
}

const iOSApplications = await this.$itmsTransporterService.getiOSApplications({ username, password });
const applications = await this.$applePortalApplicationService.getApplications({ username, password });

if (!iOSApplications || !iOSApplications.length) {
if (!applications || !applications.length) {
this.$logger.out("Seems you don't have any applications yet.");
} else {
const table: any = createTable(["Application Name", "Bundle Identifier", "Version"], iOSApplications.map(element => {
return [element.name, element.bundleId, element.version];
const table: any = createTable(["Application Name", "Bundle Identifier", "In Flight Version"], applications.map(application => {
const version = (application && application.versionSets && application.versionSets.length && application.versionSets[0].inFlightVersion && application.versionSets[0].inFlightVersion.version) || "";
return [application.name, application.bundleId, version];
}));

this.$logger.out(table.toString());
Expand Down
11 changes: 9 additions & 2 deletions lib/commands/appstore-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ export class PublishIOS implements ICommand {
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector),
new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];

constructor(private $errors: IErrors,
constructor(
private $injector: IInjector,
private $itmsTransporterService: IITMSTransporterService,
private $logger: ILogger,
private $projectData: IProjectData,
private $options: IOptions,
private $prompter: IPrompter,
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) {
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
private $hostInfo: IHostInfo,
private $errors: IErrors) {
this.$projectData.initializeProjectData();
}

Expand Down Expand Up @@ -107,11 +109,16 @@ export class PublishIOS implements ICommand {
username,
password,
ipaFilePath,
shouldExtractIpa: !!this.$options.ipa,
verboseLogging: this.$logger.getLevel() === "TRACE"
});
}

public async canExecute(args: string[]): Promise<boolean> {
if (!this.$hostInfo.isDarwin) {
this.$errors.failWithoutHelp("iOS publishing is only available on Mac OS X.");
}

if (!this.$platformService.isPlatformSupportedForOS(this.$devicePlatformsConstants.iOS, this.$projectData)) {
this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`);
}
Expand Down
11 changes: 5 additions & 6 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,11 @@ interface IITMSData extends ICredentials {
* @type {string}
*/
ipaFilePath: string;

/**
* Specify if the service should extract the `.ipa` file into `temp` directory in order to get bundleIdentifier from info.plist
*/
shouldExtractIpa: boolean;
/**
* Specifies whether the logging level of the itmstransporter command-line tool should be set to verbose.
* @type {string}
Expand All @@ -639,12 +644,6 @@ interface IITMSTransporterService {
* @return {Promise<void>}
*/
upload(data: IITMSData): Promise<void>;
/**
* Queries Apple's content delivery API to get the user's registered iOS applications.
* @param {ICredentials} credentials Credentials for authentication with iTunes Connect.
* @return {Promise<IItunesConnectApplication[]>} The user's iOS applications.
*/
getiOSApplications(credentials: ICredentials): Promise<IiTunesConnectApplication[]>;
}

/**
Expand Down
54 changes: 54 additions & 0 deletions lib/services/apple-portal/apple-portal-application-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export class ApplePortalApplicationService implements IApplePortalApplicationService {
constructor(
private $applePortalSessionService: IApplePortalSessionService,
private $errors: IErrors,
private $httpClient: Server.IHttpClient
) { }

public async getApplications(credentials: ICredentials): Promise<IApplePortalApplicationSummary[]> {
let result: IApplePortalApplicationSummary[] = [];

const user = await this.$applePortalSessionService.createUserSession(credentials);
for (const account of user.associatedAccounts) {
const contentProviderId = account.contentProvider.contentProviderId;
const dsId = user.sessionToken.dsId;
const applications = await this.getApplicationsByProvider(contentProviderId, dsId);
result = result.concat(applications.summaries);
}

return result;
}

public async getApplicationsByProvider(contentProviderId: number, dsId: string): Promise<IApplePortalApplication> {
const webSessionCookie = await this.$applePortalSessionService.createWebSession(contentProviderId, dsId);
const response = await this.$httpClient.httpRequest({
url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/apps/manageyourapps/summary/v2",
method: "GET",
body: JSON.stringify({
contentProviderId
}),
headers: {
'Content-Type': 'application/json',
'Cookie': webSessionCookie
}
});

return JSON.parse(response.body).data;
}

public async getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise<IApplePortalApplicationSummary> {
const applications = await this.getApplications(credentials);
if (!applications || !applications.length) {
this.$errors.failWithoutHelp(`Cannot find any registered applications for Apple ID ${credentials.username} in iTunes Connect.`);
}

const application = _.find(applications, app => app.bundleId === bundleId);

if (!application) {
this.$errors.failWithoutHelp(`Cannot find registered applications that match the specified identifier ${bundleId} in iTunes Connect.`);
}

return application;
}
}
$injector.register("applePortalApplicationService", ApplePortalApplicationService);
41 changes: 41 additions & 0 deletions lib/services/apple-portal/apple-portal-cookie-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export class ApplePortalCookieService implements IApplePortalCookieService {
private userSessionCookies: IStringDictionary = {};
private validUserSessionCookieNames = ["myacinfo", "dqsid", "itctx", "itcdq", "acn01"];
private validWebSessionCookieNames = ["wosid", "woinst", "itctx"];

public getWebSessionCookie(cookiesData: string[]): string {
const webSessionCookies = _.cloneDeep(this.userSessionCookies);

const parsedCookies = this.parseCookiesData(cookiesData, this.validWebSessionCookieNames);
_.each(parsedCookies, parsedCookie => webSessionCookies[parsedCookie.key] = parsedCookie.cookie);

return _.values(webSessionCookies).join("; ");
}

public getUserSessionCookie(): string {
return _.values(this.userSessionCookies).join("; ");
}

public updateUserSessionCookie(cookiesData: string[]): void {
const parsedCookies = this.parseCookiesData(cookiesData, this.validUserSessionCookieNames);
_.each(parsedCookies, parsedCookie => this.userSessionCookies[parsedCookie.key] = parsedCookie.cookie);
}

private parseCookiesData(cookiesData: string[], validCookieNames: string[]): IDictionary<{key: string, value: string, cookie: string}> {
const result: IDictionary<{key: string, value: string, cookie: string}> = {};

for (const c of cookiesData) {
const parts = c.split(";");
for (const cookie of parts) {
const trimmedCookie = cookie.trim();
const [cookieKey, cookieValue] = trimmedCookie.split("=");
if (_.includes(validCookieNames, cookieKey)) {
result[cookieKey] = { key: cookieKey, value: cookieValue, cookie: trimmedCookie };
}
}
}

return result;
}
}
$injector.register("applePortalCookieService", ApplePortalCookieService);
95 changes: 95 additions & 0 deletions lib/services/apple-portal/apple-portal-session-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export class ApplePortalSessionService implements IApplePortalSessionService {
private loginConfigEndpoint = "https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com";
private defaultLoginConfig = {
authServiceUrl : "https://idmsa.apple.com/appleautcodh",
authServiceKey : "e0b80c3bf78523bfe80974d320935bfa30add02e1bff88ec2166c6bd5a706c42"
};

constructor(
private $applePortalCookieService: IApplePortalCookieService,
private $httpClient: Server.IHttpClient,
private $logger: ILogger
) { }

public async createUserSession(credentials: ICredentials): Promise<IApplePortalUserDetail> {
const loginConfig = await this.getLoginConfig();
const loginUrl = `${loginConfig.authServiceUrl}/auth/signin`;
const loginResponse = await this.$httpClient.httpRequest({
url: loginUrl,
method: "POST",
body: JSON.stringify({
accountName: credentials.username,
password: credentials.password,
rememberMe: true
}),
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-Apple-Widget-Key': loginConfig.authServiceKey,
'Accept': 'application/json, text/javascript'
}
});

this.$applePortalCookieService.updateUserSessionCookie(loginResponse.headers["set-cookie"]);

const sessionResponse = await this.$httpClient.httpRequest({
url: "https://appstoreconnect.apple.com/olympus/v1/session",
method: "GET",
headers: {
'Cookie': this.$applePortalCookieService.getUserSessionCookie()
}
});

this.$applePortalCookieService.updateUserSessionCookie(sessionResponse.headers["set-cookie"]);

const userDetailResponse = await this.$httpClient.httpRequest({
url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/user/detail",
method: "GET",
headers: {
'Content-Type': 'application/json',
'Cookie': this.$applePortalCookieService.getUserSessionCookie(),
}
});

this.$applePortalCookieService.updateUserSessionCookie(userDetailResponse.headers["set-cookie"]);

return JSON.parse(userDetailResponse.body).data;
}

public async createWebSession(contentProviderId: number, dsId: string): Promise<string> {
const webSessionResponse = await this.$httpClient.httpRequest({
url: "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/ra/v1/session/webSession",
method: "POST",
body: JSON.stringify({
contentProviderId,
dsId,
ipAddress: null
}),
headers: {
'Accept': 'application/json, text/plain, */*',
'Accept-Encoding': 'gzip, deflate, br',
'X-Csrf-Itc': 'itc',
'Content-Type': 'application/json;charset=UTF-8',
'Cookie': this.$applePortalCookieService.getUserSessionCookie()
}
});

const webSessionCookie = this.$applePortalCookieService.getWebSessionCookie(webSessionResponse.headers["set-cookie"]);

return webSessionCookie;
}

private async getLoginConfig(): Promise<{authServiceUrl: string, authServiceKey: string}> {
let config = null;

try {
const response = await this.$httpClient.httpRequest({ url: this.loginConfigEndpoint, method: "GET" });
config = JSON.parse(response.body);
} catch (err) {
this.$logger.trace(`Error while executing request to ${this.loginConfigEndpoint}. More info: ${err}`);
}

return config || this.defaultLoginConfig;
}
}
$injector.register("applePortalSessionService", ApplePortalSessionService);
72 changes: 72 additions & 0 deletions lib/services/apple-portal/definitions.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
interface IApplePortalSessionService {
createUserSession(credentials: ICredentials): Promise<IApplePortalUserDetail>;
createWebSession(contentProviderId: number, dsId: string): Promise<string>;
}

interface IApplePortalCookieService {
getWebSessionCookie(cookiesData: string[]): string;
getUserSessionCookie(): string;
updateUserSessionCookie(cookie: string[]): void;
}

interface IApplePortalApplicationService {
getApplications(credentials: ICredentials): Promise<IApplePortalApplicationSummary[]>
getApplicationsByProvider(contentProviderId: number, dsId: string): Promise<IApplePortalApplication>;
getApplicationByBundleId(credentials: ICredentials, bundleId: string): Promise<IApplePortalApplicationSummary>;
}

interface IApplePortalUserDetail {
associatedAccounts: IApplePortalAssociatedAccountData[];
sessionToken: {
dsId: string;
contentProviderId: number;
ipAddress: string;
}
contentProviderFeatures: string[];
contentProviderId: number;
firstname: string;
displayName: string;
userName: string;
userId: string;
contentProvider: string;
visibility: boolean;
DYCVisibility: boolean;
}

interface IApplePortalAssociatedAccountData {
contentProvider: {
name: string;
contentProviderId: number;
contentProviderPublicId: string;
contentProviderTypes: string[];
};
roles: string[];
lastLogin: number;
}

interface IApplePortalApplication {
summaries: IApplePortalApplicationSummary[];
showSharedSecret: boolean;
macBundlesEnabled: boolean;
canCreateMacApps: boolean;
cloudStorageEnabled: boolean;
sharedSecretLink: string;
gameCenterGroupLink: string;
enabledPlatforms: string[];
cloudStorageLink: string;
catalogReportsLink: string;
canCreateIOSApps: boolean;
}

interface IApplePortalApplicationSummary {
name: string;
adamId: string;
vendorId: string;
bundleId: string;
appType: any;
versionSets: any[];
lastModifiedDate: number;
iconUrl: string;
issuesCount: number;
priceTier: string;
}
Loading