diff --git a/client/src/App.tsx b/client/src/App.tsx
index 74cdca19..a332f9aa 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -329,8 +329,6 @@ const App = () => {
}
} catch (error) {
console.error("Error loading OAuth tokens:", error);
- } finally {
- updateAuthState({ loading: false });
}
};
diff --git a/client/src/components/AuthDebugger.tsx b/client/src/components/AuthDebugger.tsx
index b88758d4..9757af95 100644
--- a/client/src/components/AuthDebugger.tsx
+++ b/client/src/components/AuthDebugger.tsx
@@ -60,23 +60,23 @@ const AuthDebugger = ({
authState,
updateAuthState,
}: AuthDebuggerProps) => {
- // Initialize loading state
+ // Check for existing tokens on mount
useEffect(() => {
- if (authState.loading && serverUrl) {
- // Check if we have existing tokens
+ if (serverUrl && !authState.oauthTokens) {
const checkTokens = async () => {
const provider = new DebugInspectorOAuthClientProvider(serverUrl);
const existingTokens = await provider.tokens();
-
- updateAuthState({
- loading: false,
- oauthTokens: existingTokens || null,
- });
+ if (existingTokens) {
+ updateAuthState({
+ oauthTokens: existingTokens,
+ oauthStep: "complete",
+ });
+ }
};
-
checkTokens();
}
- }, [serverUrl, authState.loading, updateAuthState]);
+ }, [serverUrl]); // Only run when serverUrl changes
+
const startOAuthFlow = useCallback(() => {
if (!serverUrl) {
updateAuthState({
@@ -241,10 +241,7 @@ const AuthDebugger = ({
)}
- {authState.loading ? (
-
Loading authentication status...
- ) : (
-
+
{authState.oauthTokens && (
Access Token:
@@ -286,7 +283,6 @@ const AuthDebugger = ({
the standard automatic flow.
- )}
{
mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
- mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(null);
+ mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
+ new Error("No protected resource metadata found")
+ );
mockStartAuthorization.mockImplementation(async (_sseUrl, options) => {
const authUrl = new URL("https://oauth.example.com/authorize");
@@ -448,7 +450,7 @@ describe("AuthDebugger", () => {
await act(async () => {
renderAuthDebugger({
updateAuthState,
- authState: { ...defaultAuthState, loading: false },
+ authState: { ...defaultAuthState },
});
});
@@ -471,7 +473,7 @@ describe("AuthDebugger", () => {
).mock.calls.find((call) => call[0] === SESSION_KEYS.AUTH_DEBUGGER_STATE);
expect(storedStateCall).toBeDefined();
- const storedState = JSON.parse(storedStateCall![1]);
+ const storedState = JSON.parse(storedStateCall![1] as string);
expect(storedState).toMatchObject({
oauthStep: "authorization_code",
@@ -487,4 +489,119 @@ describe("AuthDebugger", () => {
});
});
});
+
+ describe("OAuth Protected Resource Metadata", () => {
+ it("should successfully fetch and display protected resource metadata", async () => {
+ const updateAuthState = jest.fn();
+ const mockResourceMetadata = {
+ resource: "https://example.com/api",
+ authorization_servers: ["https://custom-auth.example.com"],
+ bearer_methods_supported: ["header", "body"],
+ resource_documentation: "https://example.com/api/docs",
+ resource_policy_uri: "https://example.com/api/policy",
+ };
+
+ // Mock successful metadata discovery
+ mockDiscoverOAuthProtectedResourceMetadata.mockResolvedValue(
+ mockResourceMetadata
+ );
+ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: { ...defaultAuthState },
+ });
+ });
+
+ // Click Guided OAuth Flow to start the process
+ await act(async () => {
+ fireEvent.click(screen.getByText("Guided OAuth Flow"));
+ });
+
+ // Verify that the flow started with metadata discovery
+ expect(updateAuthState).toHaveBeenCalledWith({
+ oauthStep: "metadata_discovery",
+ authorizationUrl: null,
+ statusMessage: null,
+ latestError: null,
+ });
+
+ // Click Continue to trigger metadata discovery
+ const continueButton = await screen.findByText("Continue");
+ await act(async () => {
+ fireEvent.click(continueButton);
+ });
+
+ // Wait for the metadata to be fetched
+ await waitFor(() => {
+ expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
+ "https://example.com"
+ );
+ });
+
+ // Verify the state was updated with the resource metadata
+ await waitFor(() => {
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ resourceMetadata: mockResourceMetadata,
+ authServerUrl: new URL("https://custom-auth.example.com"),
+ oauthStep: "client_registration",
+ })
+ );
+ });
+ });
+
+ it("should handle protected resource metadata fetch failure gracefully", async () => {
+ const updateAuthState = jest.fn();
+ const mockError = new Error("Failed to fetch resource metadata");
+
+ // Mock failed metadata discovery
+ mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(mockError);
+ // But OAuth metadata should still work with the original URL
+ mockDiscoverOAuthMetadata.mockResolvedValue(mockOAuthMetadata);
+
+ await act(async () => {
+ renderAuthDebugger({
+ updateAuthState,
+ authState: { ...defaultAuthState },
+ });
+ });
+
+ // Click Guided OAuth Flow
+ await act(async () => {
+ fireEvent.click(screen.getByText("Guided OAuth Flow"));
+ });
+
+ // Click Continue to trigger metadata discovery
+ const continueButton = await screen.findByText("Continue");
+ await act(async () => {
+ fireEvent.click(continueButton);
+ });
+
+ // Wait for the metadata fetch to fail
+ await waitFor(() => {
+ expect(mockDiscoverOAuthProtectedResourceMetadata).toHaveBeenCalledWith(
+ "https://example.com"
+ );
+ });
+
+ // Verify the flow continues despite the error
+ await waitFor(() => {
+ expect(updateAuthState).toHaveBeenCalledWith(
+ expect.objectContaining({
+ resourceMetadataError: mockError,
+ // Should use the original server URL as fallback
+ authServerUrl: new URL("https://example.com"),
+ oauthStep: "client_registration",
+ })
+ );
+ });
+
+ // Verify that regular OAuth metadata discovery was still called
+ expect(mockDiscoverOAuthMetadata).toHaveBeenCalledWith(
+ new URL("https://example.com")
+ );
+ });
+ });
});
diff --git a/client/src/lib/auth-types.ts b/client/src/lib/auth-types.ts
index e69dcd12..daf1bc15 100644
--- a/client/src/lib/auth-types.ts
+++ b/client/src/lib/auth-types.ts
@@ -27,7 +27,6 @@ export interface StatusMessage {
export interface AuthDebuggerState {
isInitiatingAuth: boolean;
oauthTokens: OAuthTokens | null;
- loading: boolean;
oauthStep: OAuthStep;
resourceMetadata: OAuthProtectedResourceMetadata | null;
resourceMetadataError:
@@ -47,7 +46,6 @@ export interface AuthDebuggerState {
export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
isInitiatingAuth: false,
oauthTokens: null,
- loading: true,
oauthStep: "metadata_discovery",
oauthMetadata: null,
resourceMetadata: null,