File size: 4,948 Bytes
f326613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { exchangeCodeForToken } from "../services/oauth";
import { secureStorage } from "../utils/storage";
import type { MCPServerConfig } from "../types/mcp";
import { STORAGE_KEYS, DEFAULTS } from "../config/constants";

interface OAuthTokens {
  access_token: string;
  refresh_token?: string;
  expires_in?: number;
  token_type?: string;
  [key: string]: string | number | undefined;
}

interface OAuthCallbackProps {
  serverUrl: string;
  onSuccess?: (tokens: OAuthTokens) => void;
  onError?: (error: Error) => void;
}

const OAuthCallback: React.FC<OAuthCallbackProps> = ({
  serverUrl,
  onSuccess,
  onError,
}) => {
  const [status, setStatus] = useState<string>("Authorizing...");
  const navigate = useNavigate(); // Add this hook

    useEffect(() => {
    // Parse parameters from URL search params (OAuth providers send code in query string)
    const parseHashParams = () => {
      return new URLSearchParams(window.location.search);
    };

    const params = parseHashParams();
    const code = params.get("code");
    const state = params.get("state");
    const error = params.get("error");

    // Verify state parameter for CSRF protection
    const savedState = localStorage.getItem('oauth_state');
    if (state !== savedState) {
      setStatus("Invalid state parameter. Possible CSRF attack.");
      if (onError) onError(new Error("Invalid state parameter"));
      return;
    }

    // Check for OAuth errors
    if (error) {
      const errorDescription = params.get("error_description") || error;
      setStatus(`OAuth error: ${errorDescription}`);
      if (onError) onError(new Error(errorDescription));
      return;
    }

    // Always persist MCP server URL for robustness
    localStorage.setItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL, serverUrl);
    
    if (code) {
      exchangeCodeForToken({
        serverUrl,
        code,
        redirectUri: window.location.origin + "/#" + DEFAULTS.OAUTH_REDIRECT_PATH, // Add hash
      })
        .then(async (tokens) => {
          await secureStorage.setItem(STORAGE_KEYS.OAUTH_ACCESS_TOKEN, tokens.access_token);
          
          // Add MCP server to MCPClientService for UI
          const mcpServerUrl = localStorage.getItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL);
          if (mcpServerUrl) {
            const serverName =
              localStorage.getItem(STORAGE_KEYS.MCP_SERVER_NAME) || mcpServerUrl;
            const serverTransport = 
              (localStorage.getItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT) as MCPServerConfig['transport']) || DEFAULTS.MCP_TRANSPORT;
            
            const serverConfig = {
              id: `server_${Date.now()}`,
              name: serverName,
              url: mcpServerUrl,
              enabled: true,
              transport: serverTransport,
              auth: {
                type: "bearer" as const,
                token: tokens.access_token,
              },
            };
            
            let servers: MCPServerConfig[] = [];
            try {
              const stored = localStorage.getItem(STORAGE_KEYS.MCP_SERVERS);
              if (stored) servers = JSON.parse(stored);
            } catch {}
            
            const exists = servers.some((s: MCPServerConfig) => s.url === mcpServerUrl);
            if (!exists) {
              servers.push(serverConfig);
              localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(servers));
            }
            
            // Clear temp values
            localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_NAME);
            localStorage.removeItem(STORAGE_KEYS.MCP_SERVER_TRANSPORT);
            localStorage.removeItem(STORAGE_KEYS.OAUTH_MCP_SERVER_URL);
          }
          
          // Clear OAuth state
          localStorage.removeItem('oauth_state');
          
          setStatus("Authorization successful! Redirecting...");
          if (onSuccess) onSuccess(tokens);
          
          // Use React Router navigation instead of window.location.replace
          setTimeout(() => {
            navigate("/", { replace: true });
          }, 1000);
        })
        .catch((err) => {
          setStatus("OAuth token exchange failed: " + err.message);
          if (onError) onError(err);
          // Clear OAuth state on error
          localStorage.removeItem('oauth_state');
        });
    } else {
      setStatus("Missing authorization code in callback URL.");
      if (onError) onError(new Error("Missing authorization code"));
    }
  }, [serverUrl, onSuccess, onError, navigate]);

  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="text-center">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
        <p>{status}</p>
      </div>
    </div>
  );
};

export default OAuthCallback;