1import assert from 'assert'; 2import openBrowserAsync from 'better-opn'; 3import http from 'http'; 4import { Socket } from 'node:net'; 5import querystring from 'querystring'; 6 7import * as Log from '../../log'; 8 9const successBody = ` 10<!DOCTYPE html> 11<html lang="en"> 12<head> 13 <title>Expo SSO Login</title> 14 <meta charset="utf-8"> 15 <style type="text/css"> 16 html { 17 margin: 0; 18 padding: 0 19 } 20 21 body { 22 background-color: #fff; 23 font-family: Tahoma,Verdana; 24 font-size: 16px; 25 color: #000; 26 max-width: 100%; 27 box-sizing: border-box; 28 padding: .5rem; 29 margin: 1em; 30 overflow-wrap: break-word 31 } 32 </style> 33</head> 34<body> 35 SSO login complete. You may now close this tab and return to the command prompt. 36</body> 37</html>`; 38 39export async function getSessionUsingBrowserAuthFlowAsync({ 40 expoWebsiteUrl, 41}: { 42 expoWebsiteUrl: string; 43}): Promise<string> { 44 const scheme = 'http'; 45 const hostname = 'localhost'; 46 const path = '/auth/callback'; 47 48 const buildExpoSsoLoginUrl = (port: number): string => { 49 const data = { 50 app_redirect_uri: `${scheme}://${hostname}:${port}${path}`, 51 }; 52 const params = querystring.stringify(data); 53 return `${expoWebsiteUrl}/sso-login?${params}`; 54 }; 55 56 // Start server and begin auth flow 57 const executeAuthFlow = (): Promise<string> => { 58 return new Promise<string>(async (resolve, reject) => { 59 const connections = new Set<Socket>(); 60 61 const server = http.createServer( 62 (request: http.IncomingMessage, response: http.ServerResponse) => { 63 try { 64 if (!(request.method === 'GET' && request.url?.includes(path))) { 65 throw new Error('Unexpected SSO login response.'); 66 } 67 const url = new URL(request.url, `http:${request.headers.host}`); 68 const sessionSecret = url.searchParams.get('session_secret'); 69 70 if (!sessionSecret) { 71 throw new Error('Request missing session_secret search parameter.'); 72 } 73 resolve(sessionSecret); 74 response.writeHead(200, { 'Content-Type': 'text/html' }); 75 response.write(successBody); 76 response.end(); 77 } catch (error) { 78 reject(error); 79 } finally { 80 server.close(); 81 // Ensure that the server shuts down 82 for (const connection of connections) { 83 connection.destroy(); 84 } 85 } 86 } 87 ); 88 89 server.listen(0, hostname, () => { 90 Log.log('Waiting for browser login...'); 91 92 const address = server.address(); 93 assert( 94 address !== null && typeof address === 'object', 95 'Server address and port should be set after listening has begun' 96 ); 97 const port = address.port; 98 const authorizeUrl = buildExpoSsoLoginUrl(port); 99 openBrowserAsync(authorizeUrl); 100 }); 101 102 server.on('connection', (connection) => { 103 connections.add(connection); 104 105 connection.on('close', () => { 106 connections.delete(connection); 107 }); 108 }); 109 }); 110 }; 111 112 return await executeAuthFlow(); 113} 114