From f7e613d59b9bcca11c22a68312f77aab16def884 Mon Sep 17 00:00:00 2001 From: Byron Karlen Date: Tue, 13 Jan 2026 15:45:09 -0700 Subject: [PATCH] wip --- CHANGELOG.md | 1 + .../eas-cli/src/commands/account/login.ts | 9 ++++++-- packages/eas-cli/src/user/SessionManager.ts | 13 +++++++----- .../src/user/__tests__/SessionManager-test.ts | 12 +++++------ ...cher.ts => expoBrowserAuthFlowLauncher.ts} | 21 +++++++++---------- ...essionSecretAndUserFromBrowserAuthFlow.ts} | 9 +++----- 6 files changed, 35 insertions(+), 30 deletions(-) rename packages/eas-cli/src/user/{expoSsoLauncher.ts => expoBrowserAuthFlowLauncher.ts} (83%) rename packages/eas-cli/src/user/{fetchSessionSecretAndSsoUser.ts => fetchSessionSecretAndUserFromBrowserAuthFlow.ts} (57%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0753a687fd..bf27ba35e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/packages/eas-cli/src/commands/account/login.ts b/packages/eas-cli/src/commands/account/login.ts index d3c946c1f6..1662680947 100644 --- a/packages/eas-cli/src/commands/account/login.ts +++ b/packages/eas-cli/src/commands/account/login.ts @@ -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 = { @@ -26,7 +31,7 @@ export default class AccountLogin extends EasCommand { async runAsync(): Promise { const { - flags: { sso }, + flags: { sso, browser }, } = await this.parse(AccountLogin); const { @@ -51,7 +56,7 @@ export default class AccountLogin extends EasCommand { } } - await sessionManager.showLoginPromptAsync({ sso }); + await sessionManager.showLoginPromptAsync({ sso, browser }); Log.log('Logged in'); } } diff --git a/packages/eas-cli/src/user/SessionManager.ts b/packages/eas-cli/src/user/SessionManager.ts index 9a59126b8a..9a9d0484e0 100644 --- a/packages/eas-cli/src/user/SessionManager.ts +++ b/packages/eas-cli/src/user/SessionManager.ts @@ -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'; @@ -149,6 +149,7 @@ export default class SessionManager { nonInteractive = false, printNewLine = false, sso = false, + browser = false, } = {}): Promise { if (nonInteractive) { Errors.error( @@ -164,8 +165,8 @@ export default class SessionManager { Log.newLine(); } - if (sso) { - await this.ssoLoginAsync(); + if (sso || browser) { + await this.browserLoginAsync({ sso }); return; } @@ -205,8 +206,10 @@ export default class SessionManager { } } - private async ssoLoginAsync(): Promise { - const { sessionSecret, id, username } = await fetchSessionSecretAndSsoUserAsync(); + private async browserLoginAsync({ sso = false }): Promise { + const { sessionSecret, id, username } = await fetchSessionSecretAndUserFromBrowserAuthFlowAsync( + { sso } + ); await this.setSessionAsync({ sessionSecret, userId: id, diff --git a/packages/eas-cli/src/user/__tests__/SessionManager-test.ts b/packages/eas-cli/src/user/__tests__/SessionManager-test.ts index 40168a0e38..6a92b50c0c 100644 --- a/packages/eas-cli/src/user/__tests__/SessionManager-test.ts +++ b/packages/eas-cli/src/user/__tests__/SessionManager-test.ts @@ -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'); @@ -25,7 +25,7 @@ jest.mock('../../graphql/queries/UserQuery', () => ({ }, })); jest.mock('../fetchSessionSecretAndUser'); -jest.mock('../fetchSessionSecretAndSsoUser'); +jest.mock('../fetchSessionSecretAndUserFromBrowserAuthFlow'); jest.mock('../../api'); const authStub: any = { @@ -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": { @@ -292,7 +292,7 @@ describe(SessionManager, () => { // SSO login await sessionManager.showLoginPromptAsync({ sso: true }); expect(promptAsync).not.toHaveBeenCalled(); - expect(fetchSessionSecretAndSsoUserAsync).toHaveBeenCalled(); + expect(fetchSessionSecretAndUserFromBrowserAuthFlowAsync).toHaveBeenCalled(); }); }); diff --git a/packages/eas-cli/src/user/expoSsoLauncher.ts b/packages/eas-cli/src/user/expoBrowserAuthFlowLauncher.ts similarity index 83% rename from packages/eas-cli/src/user/expoSsoLauncher.ts rename to packages/eas-cli/src/user/expoBrowserAuthFlowLauncher.ts index 2dcbf6dcf6..54425553fd 100644 --- a/packages/eas-cli/src/user/expoSsoLauncher.ts +++ b/packages/eas-cli/src/user/expoBrowserAuthFlowLauncher.ts @@ -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 = ` - Expo SSO Login + Expo Login - 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. `; -export async function getSessionUsingBrowserAuthFlowAsync({ - expoWebsiteUrl, -}: { - expoWebsiteUrl: string; -}): Promise { +export async function getSessionUsingBrowserAuthFlowAsync({ sso = false }): Promise { 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 @@ -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'); @@ -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}` ); diff --git a/packages/eas-cli/src/user/fetchSessionSecretAndSsoUser.ts b/packages/eas-cli/src/user/fetchSessionSecretAndUserFromBrowserAuthFlow.ts similarity index 57% rename from packages/eas-cli/src/user/fetchSessionSecretAndSsoUser.ts rename to packages/eas-cli/src/user/fetchSessionSecretAndUserFromBrowserAuthFlow.ts index eefb5d8ea6..54edecc72a 100644 --- a/packages/eas-cli/src/user/fetchSessionSecretAndSsoUser.ts +++ b/packages/eas-cli/src/user/fetchSessionSecretAndUserFromBrowserAuthFlow.ts @@ -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 });