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