Skip to content

fix: ability to publish iOS applications for users with two-factor authentication enabled #4903

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 5 commits into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from 4 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
39 changes: 39 additions & 0 deletions docs/man_pages/publishing/apple-login.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<% if (isJekyll) { %>---
title: tns appstore
position: 5
---<% } %>

# tns apple-login

### Description

Uses the provided Apple credentials to obtain Apple session which can be used when publishing to Apple AppStore.

### Commands

Usage | Synopsis
---|---
General | `$ tns apple-login [<Apple ID>] [<Password>]`

<% if((isConsole && isMacOS) || isHtml) { %>

### Options

### Arguments

* `<Apple ID>` and `<Password>` are your credentials for logging into iTunes Connect.

### Command Limitations

### Related Commands

Command | Description
----------|----------
[appstore](appstore.html) | Lists applications registered in iTunes Connect.
[appstore upload](appstore-upload.html) | Uploads project to iTunes Connect.
[build](../project/testing/build.html) | Builds the project for the selected target platform and produces an application package that you can manually deploy on device or in the native emulator.
[build ios](../project/testing/build-ios.html) | Builds the project for iOS and produces an APP or IPA that you can manually deploy in the iOS Simulator or on device, respectively.
[deploy](../project/testing/deploy.html) | Builds and deploys the project to a connected physical or virtual device.
[run](../project/testing/run.html) | Runs your project on a connected device or in the native emulator for the selected platform.
[run ios](../project/testing/run-ios.html) | Runs your project on a connected iOS device or in the iOS Simulator, if configured.
<% } %>
2 changes: 2 additions & 0 deletions docs/man_pages/publishing/appstore-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Upload package | `$ tns appstore upload [<Apple ID> [<Password>]] --ipa <Ipa Fil
### Options

* `--ipa` - If set, will use provided .ipa file instead of building the project.
* `--appleApplicationSpecificPassword` - Specified the password for your Apple ID that let you sign in to your account and securely access the information you stores from iTunes Transporter application.
* `--appleSessionBase64` - The session that will be reused instead of triggering a new login each time NativeScript CLI communicates with Apple's APIs.

### Arguments

Expand Down
4 changes: 3 additions & 1 deletion docs/man_pages/publishing/publish-ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Upload package | `$ tns publish ios [<Apple ID> [<Password>]] --ipa <Ipa File Pa

* `--ipa` - If set, will use provided .ipa file instead of building the project.
* `--team-id` - Specified the team id for which Xcode will try to find distribution certificate and provisioning profile when exporting for AppStore submission.

* `--appleApplicationSpecificPassword` - Specified the password for your Apple ID that let you sign in to your account and securely access the information you stores from iTunes Transporter application.
* `--appleSessionBase64` - The session that will be reused instead of triggering a new login each time NativeScript CLI communicates with Apple's APIs.

### Arguments

* `<Apple ID>` and `<Password>` are your credentials for logging into iTunes Connect.
Expand Down
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ $injector.requireCommand("dev-generate-help", "./commands/generate-help");
$injector.requireCommand("appstore|*list", "./commands/appstore-list");
$injector.requireCommand("appstore|upload", "./commands/appstore-upload");
$injector.requireCommand("publish|ios", "./commands/appstore-upload");
$injector.requireCommand("apple-login", "./commands/apple-login");
$injector.require("itmsTransporterService", "./services/itmstransporter-service");

$injector.requireCommand("setup|*", "./commands/setup");
Expand Down
34 changes: 34 additions & 0 deletions lib/commands/apple-login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { StringCommandParameter } from "../common/command-params";

export class AppleLogin implements ICommand {
public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];

constructor(
private $applePortalSessionService: IApplePortalSessionService,
private $errors: IErrors,
private $injector: IInjector,
private $logger: ILogger,
private $prompter: IPrompter
) { }

public async execute(args: string[]): Promise<void> {
let username = args[0];
if (!username) {
username = await this.$prompter.getString("Apple ID", { allowEmpty: false });
}

let password = args[1];
if (!password) {
password = await this.$prompter.getPassword("Apple ID password");
}

const user = await this.$applePortalSessionService.createUserSession({ username, password });
if (!user.areCredentialsValid) {
this.$errors.failWithoutHelp(`Invalid username and password combination. Used '${username}' as the username.`);
}

const output = Buffer.from(user.userSessionCookie).toString("base64");
this.$logger.info(output);
}
}
$injector.registerCommand("apple-login", AppleLogin);
8 changes: 7 additions & 1 deletion lib/commands/appstore-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export class ListiOSApps implements ICommand {

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

const applications = await this.$applePortalApplicationService.getApplications({ username, password });
const user = await this.$applePortalSessionService.createUserSession({ username, password });
if (!user.areCredentialsValid) {
this.$errors.failWithoutHelp(`Invalid username and password combination. Used '${username}' as the username.`);
}

const applications = await this.$applePortalApplicationService.getApplications(user);

if (!applications || !applications.length) {
this.$logger.info("Seems you don't have any applications yet.");
Expand Down
15 changes: 13 additions & 2 deletions lib/commands/appstore-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class PublishIOS implements ICommand {
new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector)];

constructor(
private $applePortalSessionService: IApplePortalSessionService,
private $injector: IInjector,
private $itmsTransporterService: IITMSTransporterService,
private $logger: ILogger,
Expand Down Expand Up @@ -38,6 +39,15 @@ export class PublishIOS implements ICommand {
password = await this.$prompter.getPassword("Apple ID password");
}

const user = await this.$applePortalSessionService.createUserSession({ username, password }, {
applicationSpecificPassword: this.$options.appleApplicationSpecificPassword,
sessionBase64: this.$options.appleSessionBase64,
ensureConsoleIsInteractive: true
});
if (!user.areCredentialsValid) {
this.$errors.failWithoutHelp(`Invalid username and password combination. Used '${username}' as the username.`);
}

if (!mobileProvisionIdentifier && !ipaFilePath) {
this.$logger.warn("No mobile provision identifier set. A default mobile provision will be used. You can set one in app/App_Resources/iOS/build.xcconfig");
}
Expand Down Expand Up @@ -69,8 +79,9 @@ export class PublishIOS implements ICommand {
}

await this.$itmsTransporterService.upload({
username,
password,
credentials: { username, password },
user,
applicationSpecificPassword: this.$options.appleApplicationSpecificPassword,
ipaFilePath,
shouldExtractIpa: !!this.$options.ipa,
verboseLogging: this.$logger.getLevel() === "TRACE"
Expand Down
1 change: 1 addition & 0 deletions lib/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export class HttpStatusCodes {
static NOT_MODIFIED = 304;
static PAYMENT_REQUIRED = 402;
static PROXY_AUTHENTICATION_REQUIRED = 407;
static CONFLICTING_RESOURCE = 409;
}

export const HttpProtocolToPort: IDictionary<number> = {
Expand Down
4 changes: 3 additions & 1 deletion lib/common/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ private defaultUserAgent: string;
this.$logger.error(`You can run ${EOL}\t${clientNameLowerCase} proxy set <url> <username> <password>.${EOL}In order to supply ${clientNameLowerCase} with the credentials needed.`);
return "Your proxy requires authentication.";
} else if (statusCode === HttpStatusCodes.PAYMENT_REQUIRED) {
return util.format("Your subscription has expired.");
return "Your subscription has expired.";
} else if (statusCode === HttpStatusCodes.CONFLICTING_RESOURCE) {
return "The request conflicts with the current state of the server.";
} else {
this.$logger.trace("Request was unsuccessful. Server returned: ", body);
try {
Expand Down
10 changes: 9 additions & 1 deletion lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,8 @@ interface IOptions extends IRelease, IDeviceIdentifier, IJustLaunch, IAvd, IAvai
analyticsLogFile: string;
performance: Object;
cleanupLogFile: string;
appleApplicationSpecificPassword: string;
appleSessionBase64: string;
}

interface IEnvOptions {
Expand Down Expand Up @@ -609,7 +611,13 @@ interface IAndroidResourcesMigrationService {
/**
* Describes properties needed for uploading a package to iTunes Connect
*/
interface IITMSData extends ICredentials {
interface IITMSData {
credentials: ICredentials;

user: IApplePortalUserDetail;

applicationSpecificPassword: string;

/**
* Path to a .ipa file which will be uploaded.
* @type {string}
Expand Down
4 changes: 3 additions & 1 deletion lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export class Options {
hooks: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
link: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
aab: { type: OptionType.Boolean, hasSensitiveValue: false },
performance: { type: OptionType.Object, hasSensitiveValue: true }
performance: { type: OptionType.Object, hasSensitiveValue: true },
appleApplicationSpecificPassword: { type: OptionType.String, hasSensitiveValue: true },
appleSessionBase64: { type: OptionType.String, hasSensitiveValue: true },
};
}

Expand Down
9 changes: 4 additions & 5 deletions lib/services/apple-portal/apple-portal-application-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ export class ApplePortalApplicationService implements IApplePortalApplicationSer
private $httpClient: Server.IHttpClient
) { }

public async getApplications(credentials: ICredentials): Promise<IApplePortalApplicationSummary[]> {
public async getApplications(user: IApplePortalUserDetail): 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;
Expand Down Expand Up @@ -36,10 +35,10 @@ export class ApplePortalApplicationService implements IApplePortalApplicationSer
return JSON.parse(response.body).data;
}

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

const application = _.find(applications, app => app.bundleId === bundleId);
Expand Down
4 changes: 2 additions & 2 deletions lib/services/apple-portal/apple-portal-cookie-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class ApplePortalCookieService implements IApplePortalCookieService {
private userSessionCookies: IStringDictionary = {};
private validUserSessionCookieNames = ["myacinfo", "dqsid", "itctx", "itcdq", "acn01"];
private validUserSessionCookieNames = ["myacinfo", "dqsid", "itctx", "itcdq", "acn01", "DES"];
private validWebSessionCookieNames = ["wosid", "woinst", "itctx"];

public getWebSessionCookie(cookiesData: string[]): string {
Expand Down Expand Up @@ -29,7 +29,7 @@ export class ApplePortalCookieService implements IApplePortalCookieService {
for (const cookie of parts) {
const trimmedCookie = cookie.trim();
const [cookieKey, cookieValue] = trimmedCookie.split("=");
if (_.includes(validCookieNames, cookieKey)) {
if (_.includes(validCookieNames, cookieKey) || _.some(validCookieNames, validCookieName => cookieKey.startsWith(validCookieName))) {
result[cookieKey] = { key: cookieKey, value: cookieValue, cookie: trimmedCookie };
}
}
Expand Down
Loading