(expectedState: string)
| 839 | |
| 840 | // Start local HTTP server to receive OAuth callback |
| 841 | private startCallbackServer(expectedState: string): Promise<string> { |
| 842 | return new Promise((resolve, reject) => { |
| 843 | const timeout = setTimeout(() => { |
| 844 | this.stopServer(); |
| 845 | reject(new Error('OAuth timeout - no callback received')); |
| 846 | }, 300000); // 5 minute timeout |
| 847 | |
| 848 | this.server = createServer((req, res) => { |
| 849 | const url = new URL(req.url || '/', `http://localhost:${CALLBACK_PORT}`); |
| 850 | |
| 851 | if (url.pathname === CALLBACK_PATH) { |
| 852 | const code = url.searchParams.get('code'); |
| 853 | const state = url.searchParams.get('state'); |
| 854 | const error = url.searchParams.get('error'); |
| 855 | |
| 856 | if (error) { |
| 857 | res.writeHead(400, { 'Content-Type': 'text/html' }); |
| 858 | res.end(generateOAuthPage({ |
| 859 | title: 'Authorization Failed', |
| 860 | message: 'You can close this window.', |
| 861 | isSuccess: false, |
| 862 | errorDetail: error, |
| 863 | })); |
| 864 | clearTimeout(timeout); |
| 865 | this.stopServer(); |
| 866 | reject(new Error(`OAuth error: ${error}`)); |
| 867 | return; |
| 868 | } |
| 869 | |
| 870 | if (state !== expectedState) { |
| 871 | res.writeHead(400, { 'Content-Type': 'text/html' }); |
| 872 | res.end(generateOAuthPage({ |
| 873 | title: 'Security Error', |
| 874 | message: 'State mismatch - possible CSRF attack.', |
| 875 | isSuccess: false, |
| 876 | })); |
| 877 | clearTimeout(timeout); |
| 878 | this.stopServer(); |
| 879 | reject(new Error('OAuth state mismatch')); |
| 880 | return; |
| 881 | } |
| 882 | |
| 883 | if (!code) { |
| 884 | res.writeHead(400, { 'Content-Type': 'text/html' }); |
| 885 | res.end(generateOAuthPage({ |
| 886 | title: 'Authorization Failed', |
| 887 | message: 'No authorization code received.', |
| 888 | isSuccess: false, |
| 889 | })); |
| 890 | clearTimeout(timeout); |
| 891 | this.stopServer(); |
| 892 | reject(new Error('No authorization code')); |
| 893 | return; |
| 894 | } |
| 895 | |
| 896 | // Success! |
| 897 | res.writeHead(200, { 'Content-Type': 'text/html' }); |
| 898 | res.end(generateOAuthPage({ |
no test coverage detected