Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- Add `--browser` flag to `eas login` for browser-based authentication. ([#3312](https://github.com/expo/eas-cli/pull/3312) by [@byronkarlen](https://github.com/byronkarlen))
- Add App Clip bundle identifier registration support for multi-target iOS builds. ([#3300](https://github.com/expo/eas-cli/pull/3300) by [@evanbacon](https://github.com/evanbacon))
- Add `--runtime-version` and `--platform` filters to `eas update:list`. ([#3261](https://github.com/expo/eas-cli/pull/3261) by [@HarelSultan](https://github.com/HarelSultan))

Expand Down
9 changes: 7 additions & 2 deletions packages/eas-cli/src/commands/account/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export default class AccountLogin extends EasCommand {
char: 's',
default: false,
}),
browser: Flags.boolean({
description: 'Login with your browser',
char: 'b',
default: false,
}),
};

static override contextDefinition = {
Expand All @@ -26,7 +31,7 @@ export default class AccountLogin extends EasCommand {

async runAsync(): Promise<void> {
const {
flags: { sso },
flags: { sso, browser },
} = await this.parse(AccountLogin);

const {
Expand All @@ -51,7 +56,7 @@ export default class AccountLogin extends EasCommand {
}
}

await sessionManager.showLoginPromptAsync({ sso });
await sessionManager.showLoginPromptAsync({ sso, browser });
Log.log('Logged in');
}
}
13 changes: 8 additions & 5 deletions packages/eas-cli/src/user/SessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';

import { fetchSessionSecretAndSsoUserAsync } from './fetchSessionSecretAndSsoUser';
import { fetchSessionSecretAndUserAsync } from './fetchSessionSecretAndUser';
import { fetchSessionSecretAndUserFromBrowserAuthFlowAsync } from './fetchSessionSecretAndUserFromBrowserAuthFlow';
import { ApiV2Error } from '../ApiV2Error';
import { AnalyticsWithOrchestration } from '../analytics/AnalyticsManager';
import { ApiV2Client } from '../api';
Expand Down Expand Up @@ -149,6 +149,7 @@ export default class SessionManager {
nonInteractive = false,
printNewLine = false,
sso = false,
browser = false,
} = {}): Promise<void> {
if (nonInteractive) {
Errors.error(
Expand All @@ -164,8 +165,8 @@ export default class SessionManager {
Log.newLine();
}

if (sso) {
await this.ssoLoginAsync();
if (sso || browser) {
await this.browserLoginAsync({ sso });
return;
}

Expand Down Expand Up @@ -205,8 +206,10 @@ export default class SessionManager {
}
}

private async ssoLoginAsync(): Promise<void> {
const { sessionSecret, id, username } = await fetchSessionSecretAndSsoUserAsync();
private async browserLoginAsync({ sso = false }): Promise<void> {
const { sessionSecret, id, username } = await fetchSessionSecretAndUserFromBrowserAuthFlowAsync(
{ sso }
);
await this.setSessionAsync({
sessionSecret,
userId: id,
Expand Down
12 changes: 6 additions & 6 deletions packages/eas-cli/src/user/__tests__/SessionManager-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import Log from '../../log';
import { promptAsync, selectAsync } from '../../prompts';
import { getStateJsonPath } from '../../utils/paths';
import SessionManager, { UserSecondFactorDeviceMethod } from '../SessionManager';
import { fetchSessionSecretAndSsoUserAsync } from '../fetchSessionSecretAndSsoUser';
import { fetchSessionSecretAndUserAsync } from '../fetchSessionSecretAndUser';
import { fetchSessionSecretAndUserFromBrowserAuthFlowAsync } from '../fetchSessionSecretAndUserFromBrowserAuthFlow';

jest.mock('../../prompts');
jest.mock('../../log');
Expand All @@ -25,7 +25,7 @@ jest.mock('../../graphql/queries/UserQuery', () => ({
},
}));
jest.mock('../fetchSessionSecretAndUser');
jest.mock('../fetchSessionSecretAndSsoUser');
jest.mock('../fetchSessionSecretAndUserFromBrowserAuthFlow');
jest.mock('../../api');

const authStub: any = {
Expand Down Expand Up @@ -156,15 +156,15 @@ describe(SessionManager, () => {
});
});

describe('ssoLoginAsync', () => {
describe('browserLoginAsync', () => {
it('saves user data to ~/.expo/state.json', async () => {
jest.mocked(fetchSessionSecretAndSsoUserAsync).mockResolvedValue({
jest.mocked(fetchSessionSecretAndUserFromBrowserAuthFlowAsync).mockResolvedValue({
sessionSecret: 'SESSION_SECRET',
id: 'USER_ID',
username: 'USERNAME',
});
const sessionManager = new SessionManager(analytics);
await sessionManager['ssoLoginAsync']();
await sessionManager['browserLoginAsync']({ sso: true });
expect(await fs.readFile(getStateJsonPath(), 'utf8')).toMatchInlineSnapshot(`
"{
"auth": {
Expand Down Expand Up @@ -292,7 +292,7 @@ describe(SessionManager, () => {
// SSO login
await sessionManager.showLoginPromptAsync({ sso: true });
expect(promptAsync).not.toHaveBeenCalled();
expect(fetchSessionSecretAndSsoUserAsync).toHaveBeenCalled();
expect(fetchSessionSecretAndUserFromBrowserAuthFlowAsync).toHaveBeenCalled();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import http from 'http';
import { Socket } from 'node:net';
import querystring from 'querystring';

import { getExpoWebsiteBaseUrl } from '../api';
import Log from '../log';

const successBody = `
<!DOCTYPE html>
<html lang="en">
<head>
<title>Expo SSO Login</title>
<title>Expo Login</title>
<meta charset="utf-8">
<style type="text/css">
html {
Expand All @@ -32,25 +33,23 @@ const successBody = `
</style>
</head>
<body>
SSO login complete. You may now close this tab and return to the command prompt.
Login complete. You may now close this tab and return to the command prompt.
</body>
</html>`;

export async function getSessionUsingBrowserAuthFlowAsync({
expoWebsiteUrl,
}: {
expoWebsiteUrl: string;
}): Promise<string> {
export async function getSessionUsingBrowserAuthFlowAsync({ sso = false }): Promise<string> {
const scheme = 'http';
const hostname = 'localhost';
const path = '/auth/callback';

const buildExpoSsoLoginUrl = (port: number): string => {
const expoWebsiteUrl = getExpoWebsiteBaseUrl();

const buildExpoLoginUrl = (port: number, sso: boolean): string => {
const data = {
app_redirect_uri: `${scheme}://${hostname}:${port}${path}`,
};
const params = querystring.stringify(data);
return `${expoWebsiteUrl}/sso-login?${params}`;
return `${expoWebsiteUrl}${sso ? '/sso-login' : '/login'}?${params}`;
};

// Start server and begin auth flow
Expand All @@ -62,7 +61,7 @@ export async function getSessionUsingBrowserAuthFlowAsync({
(request: http.IncomingMessage, response: http.ServerResponse) => {
try {
if (!(request.method === 'GET' && request.url?.includes('/auth/callback'))) {
throw new Error('Unexpected SSO login response.');
throw new Error('Unexpected login response.');
}
const url = new URL(request.url, `http:${request.headers.host}`);
const sessionSecret = url.searchParams.get('session_secret');
Expand Down Expand Up @@ -95,7 +94,7 @@ export async function getSessionUsingBrowserAuthFlowAsync({
'Server address and port should be set after listening has begun'
);
const port = address.port;
const authorizeUrl = buildExpoSsoLoginUrl(port);
const authorizeUrl = buildExpoLoginUrl(port, sso);
Log.log(
`If your browser doesn't automatically open, visit this link to log in: ${authorizeUrl}`
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { getSessionUsingBrowserAuthFlowAsync } from './expoSsoLauncher';
import { getSessionUsingBrowserAuthFlowAsync } from './expoBrowserAuthFlowLauncher';
import { fetchUserAsync } from './fetchUser';
import { getExpoWebsiteBaseUrl } from '../api';

export async function fetchSessionSecretAndSsoUserAsync(): Promise<{
export async function fetchSessionSecretAndUserFromBrowserAuthFlowAsync({ sso = false }): Promise<{
sessionSecret: string;
id: string;
username: string;
}> {
const sessionSecret = await getSessionUsingBrowserAuthFlowAsync({
expoWebsiteUrl: getExpoWebsiteBaseUrl(),
});
const sessionSecret = await getSessionUsingBrowserAuthFlowAsync({ sso });

const userData = await fetchUserAsync({ sessionSecret });

Expand Down