feat: unify OAuth scope discovery between automatic and manual flows
- Add shared discoverScopes function in auth.ts with resource metadata preference - Update automatic flow (handleAuthError) to use dynamic scope discovery - Update manual flow (OAuth state machine) to use shared scope discovery logic - Add comprehensive test suite for discoverScopes function (11 test cases) - Fix empty array handling to return undefined instead of empty string - Maintain backward compatibility with existing oauthScope parameter - All quality gates pass: TypeScript, linting, formatting, and tests Resolves OAuth scope discovery inconsistency where automatic flow used static scopes while manual flow used metadata scopes. Both flows now use identical shared logic with proper resource metadata preference.
This commit is contained in:
parent
c5047ca30a
commit
74900fca5d
6 changed files with 451 additions and 20 deletions
|
|
@ -25,6 +25,7 @@ const mockOAuthMetadata = {
|
|||
token_endpoint: "https://oauth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
};
|
||||
|
||||
const mockOAuthClientInfo = {
|
||||
|
|
@ -56,6 +57,40 @@ import {
|
|||
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import { EMPTY_DEBUGGER_STATE } from "@/lib/auth-types";
|
||||
|
||||
// Mock local auth module
|
||||
jest.mock("@/lib/auth", () => ({
|
||||
DebugInspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||
tokens: jest.fn().mockImplementation(() => Promise.resolve(undefined)),
|
||||
clear: jest.fn().mockImplementation(() => {
|
||||
// Mock the real clear() behavior which removes items from sessionStorage
|
||||
sessionStorage.removeItem("[https://example.com/mcp] mcp_tokens");
|
||||
sessionStorage.removeItem("[https://example.com/mcp] mcp_client_info");
|
||||
sessionStorage.removeItem(
|
||||
"[https://example.com/mcp] mcp_server_metadata",
|
||||
);
|
||||
}),
|
||||
redirectUrl: "http://localhost:3000/oauth/callback/debug",
|
||||
clientMetadata: {
|
||||
redirect_uris: ["http://localhost:3000/oauth/callback/debug"],
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
client_name: "MCP Inspector",
|
||||
},
|
||||
clientInformation: jest.fn(),
|
||||
saveClientInformation: jest.fn(),
|
||||
saveTokens: jest.fn(),
|
||||
redirectToAuthorization: jest.fn(),
|
||||
saveCodeVerifier: jest.fn(),
|
||||
codeVerifier: jest.fn(),
|
||||
saveServerMetadata: jest.fn(),
|
||||
getServerMetadata: jest.fn(),
|
||||
})),
|
||||
discoverScopes: jest.fn().mockResolvedValue("read write" as never),
|
||||
}));
|
||||
|
||||
import { discoverScopes } from "@/lib/auth";
|
||||
|
||||
// Type the mocked functions properly
|
||||
const mockDiscoverAuthorizationServerMetadata =
|
||||
discoverAuthorizationServerMetadata as jest.MockedFunction<
|
||||
|
|
@ -75,6 +110,9 @@ const mockDiscoverOAuthProtectedResourceMetadata =
|
|||
discoverOAuthProtectedResourceMetadata as jest.MockedFunction<
|
||||
typeof discoverOAuthProtectedResourceMetadata
|
||||
>;
|
||||
const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
|
||||
typeof discoverScopes
|
||||
>;
|
||||
|
||||
const sessionStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
|
|
@ -103,9 +141,15 @@ describe("AuthDebugger", () => {
|
|||
// Suppress console errors in tests to avoid JSDOM navigation noise
|
||||
jest.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
mockDiscoverAuthorizationServerMetadata.mockResolvedValue(
|
||||
mockOAuthMetadata,
|
||||
);
|
||||
// Set default mock behaviors with complete OAuth metadata
|
||||
mockDiscoverAuthorizationServerMetadata.mockResolvedValue({
|
||||
issuer: "https://oauth.example.com",
|
||||
authorization_endpoint: "https://oauth.example.com/authorize",
|
||||
token_endpoint: "https://oauth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
});
|
||||
mockRegisterClient.mockResolvedValue(mockOAuthClientInfo);
|
||||
mockDiscoverOAuthProtectedResourceMetadata.mockRejectedValue(
|
||||
new Error("No protected resource metadata found"),
|
||||
|
|
@ -427,7 +471,24 @@ describe("AuthDebugger", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should not include scope in authorization URL when scopes_supported is not present", async () => {
|
||||
it("should include scope in authorization URL when scopes_supported is not present", async () => {
|
||||
const updateAuthState =
|
||||
await setupAuthorizationUrlTest(mockOAuthMetadata);
|
||||
|
||||
// Wait for the updateAuthState to be called
|
||||
await waitFor(() => {
|
||||
expect(updateAuthState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
authorizationUrl: expect.stringContaining("scope="),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should omit scope from authorization URL when discoverScopes returns undefined", async () => {
|
||||
// Mock discoverScopes to return undefined (no scopes available)
|
||||
mockDiscoverScopes.mockResolvedValueOnce(undefined);
|
||||
|
||||
const updateAuthState =
|
||||
await setupAuthorizationUrlTest(mockOAuthMetadata);
|
||||
|
||||
|
|
|
|||
154
client/src/lib/__tests__/auth.test.ts
Normal file
154
client/src/lib/__tests__/auth.test.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { discoverScopes } from "../auth";
|
||||
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
discoverAuthorizationServerMetadata: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockDiscoverAuth =
|
||||
discoverAuthorizationServerMetadata as jest.MockedFunction<
|
||||
typeof discoverAuthorizationServerMetadata
|
||||
>;
|
||||
|
||||
const baseMetadata = {
|
||||
issuer: "https://test.com",
|
||||
authorization_endpoint: "https://test.com/authorize",
|
||||
token_endpoint: "https://test.com/token",
|
||||
response_types_supported: ["code"],
|
||||
grant_types_supported: ["authorization_code"],
|
||||
scopes_supported: ["read", "write"],
|
||||
};
|
||||
|
||||
describe("discoverScopes", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
name: "returns joined scopes from OAuth metadata",
|
||||
mockResolves: baseMetadata,
|
||||
serverUrl: "https://example.com",
|
||||
expected: "read write",
|
||||
expectedCallUrl: "https://example.com/",
|
||||
},
|
||||
{
|
||||
name: "prefers resource metadata over OAuth metadata",
|
||||
mockResolves: baseMetadata,
|
||||
serverUrl: "https://example.com",
|
||||
resourceMetadata: {
|
||||
resource: "https://example.com",
|
||||
scopes_supported: ["admin", "full"],
|
||||
},
|
||||
expected: "admin full",
|
||||
},
|
||||
{
|
||||
name: "falls back to OAuth when resource has empty scopes",
|
||||
mockResolves: baseMetadata,
|
||||
serverUrl: "https://example.com",
|
||||
resourceMetadata: {
|
||||
resource: "https://example.com",
|
||||
scopes_supported: [],
|
||||
},
|
||||
expected: "read write",
|
||||
},
|
||||
{
|
||||
name: "normalizes URL with port and path",
|
||||
mockResolves: baseMetadata,
|
||||
serverUrl: "https://example.com:8080/some/path",
|
||||
expected: "read write",
|
||||
expectedCallUrl: "https://example.com:8080/",
|
||||
},
|
||||
{
|
||||
name: "normalizes URL with trailing slash",
|
||||
mockResolves: baseMetadata,
|
||||
serverUrl: "https://example.com/",
|
||||
expected: "read write",
|
||||
expectedCallUrl: "https://example.com/",
|
||||
},
|
||||
{
|
||||
name: "handles single scope",
|
||||
mockResolves: { ...baseMetadata, scopes_supported: ["admin"] },
|
||||
serverUrl: "https://example.com",
|
||||
expected: "admin",
|
||||
},
|
||||
{
|
||||
name: "prefers resource metadata even with fewer scopes",
|
||||
mockResolves: {
|
||||
...baseMetadata,
|
||||
scopes_supported: ["read", "write", "admin", "full"],
|
||||
},
|
||||
serverUrl: "https://example.com",
|
||||
resourceMetadata: {
|
||||
resource: "https://example.com",
|
||||
scopes_supported: ["read"],
|
||||
},
|
||||
expected: "read",
|
||||
},
|
||||
];
|
||||
|
||||
const undefinedCases = [
|
||||
{
|
||||
name: "returns undefined when OAuth discovery fails",
|
||||
mockRejects: new Error("Discovery failed"),
|
||||
serverUrl: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "returns undefined when OAuth has no scopes",
|
||||
mockResolves: { ...baseMetadata, scopes_supported: [] },
|
||||
serverUrl: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "returns undefined when scopes_supported missing",
|
||||
mockResolves: (() => {
|
||||
const { scopes_supported, ...rest } = baseMetadata;
|
||||
return rest;
|
||||
})(),
|
||||
serverUrl: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "returns undefined with resource metadata but OAuth fails",
|
||||
mockRejects: new Error("No OAuth metadata"),
|
||||
serverUrl: "https://example.com",
|
||||
resourceMetadata: {
|
||||
resource: "https://example.com",
|
||||
scopes_supported: ["read", "write"],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
"$name",
|
||||
async ({
|
||||
mockResolves,
|
||||
serverUrl,
|
||||
resourceMetadata,
|
||||
expected,
|
||||
expectedCallUrl,
|
||||
}) => {
|
||||
mockDiscoverAuth.mockResolvedValue(mockResolves);
|
||||
|
||||
const result = await discoverScopes(serverUrl, resourceMetadata);
|
||||
|
||||
expect(result).toBe(expected);
|
||||
if (expectedCallUrl) {
|
||||
expect(mockDiscoverAuth).toHaveBeenCalledWith(new URL(expectedCallUrl));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test.each(undefinedCases)(
|
||||
"$name",
|
||||
async ({ mockResolves, mockRejects, serverUrl, resourceMetadata }) => {
|
||||
if (mockRejects) {
|
||||
mockDiscoverAuth.mockRejectedValue(mockRejects);
|
||||
} else {
|
||||
mockDiscoverAuth.mockResolvedValue(mockResolves);
|
||||
}
|
||||
|
||||
const result = await discoverScopes(serverUrl, resourceMetadata);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -6,10 +6,45 @@ import {
|
|||
OAuthTokensSchema,
|
||||
OAuthClientMetadata,
|
||||
OAuthMetadata,
|
||||
OAuthProtectedResourceMetadata,
|
||||
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
|
||||
import { generateOAuthState } from "@/utils/oauthUtils";
|
||||
|
||||
/**
|
||||
* Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
|
||||
* @param serverUrl - The MCP server URL
|
||||
* @param resourceMetadata - Optional resource metadata containing preferred scopes
|
||||
* @returns Promise resolving to space-separated scope string or undefined
|
||||
*/
|
||||
export const discoverScopes = async (
|
||||
serverUrl: string,
|
||||
resourceMetadata?: OAuthProtectedResourceMetadata,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const metadata = await discoverAuthorizationServerMetadata(
|
||||
new URL("/", serverUrl),
|
||||
);
|
||||
|
||||
// Prefer resource metadata scopes, but fall back to OAuth metadata if empty
|
||||
const resourceScopes = resourceMetadata?.scopes_supported;
|
||||
const oauthScopes = metadata?.scopes_supported;
|
||||
|
||||
const scopesSupported =
|
||||
resourceScopes && resourceScopes.length > 0
|
||||
? resourceScopes
|
||||
: oauthScopes;
|
||||
|
||||
return scopesSupported && scopesSupported.length > 0
|
||||
? scopesSupported.join(" ")
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
console.debug("OAuth scope discovery failed:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getClientInformationFromSessionStorage = async ({
|
||||
serverUrl,
|
||||
isPreregistered,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@ import { useConnection } from "../useConnection";
|
|||
import { z } from "zod";
|
||||
import { ClientRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { DEFAULT_INSPECTOR_CONFIG } from "../../constants";
|
||||
import { SSEClientTransportOptions } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import {
|
||||
SSEClientTransportOptions,
|
||||
SseError,
|
||||
} from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import {
|
||||
ElicitResult,
|
||||
ElicitRequest,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
import { discoverScopes } from "../../auth";
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn().mockResolvedValue({
|
||||
|
|
@ -53,14 +58,27 @@ jest.mock("@modelcontextprotocol/sdk/client/index.js", () => ({
|
|||
Client: jest.fn().mockImplementation(() => mockClient),
|
||||
}));
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => ({
|
||||
SSEClientTransport: jest.fn((url, options) => {
|
||||
mockSSETransport.url = url;
|
||||
mockSSETransport.options = options;
|
||||
return mockSSETransport;
|
||||
}),
|
||||
SseError: jest.fn(),
|
||||
}));
|
||||
jest.mock("@modelcontextprotocol/sdk/client/sse.js", () => {
|
||||
// Minimal mock class that supports instanceof checks
|
||||
class SseError extends Error {
|
||||
code: number;
|
||||
event: ErrorEvent;
|
||||
constructor(code: number, message: string, event: ErrorEvent) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.event = event;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
SSEClientTransport: jest.fn((url, options) => {
|
||||
mockSSETransport.url = url;
|
||||
mockSSETransport.options = options;
|
||||
return mockSSETransport;
|
||||
}),
|
||||
SseError,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
||||
StreamableHTTPClientTransport: jest.fn((url, options) => {
|
||||
|
|
@ -85,11 +103,18 @@ jest.mock("@/lib/hooks/useToast", () => ({
|
|||
jest.mock("../../auth", () => ({
|
||||
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||
tokens: jest.fn().mockResolvedValue({ access_token: "mock-token" }),
|
||||
redirectUrl: "http://localhost:3000/oauth/callback",
|
||||
})),
|
||||
clearClientInformationFromSessionStorage: jest.fn(),
|
||||
saveClientInformationToSessionStorage: jest.fn(),
|
||||
discoverScopes: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockAuth = auth as jest.MockedFunction<typeof auth>;
|
||||
const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
|
||||
typeof discoverScopes
|
||||
>;
|
||||
|
||||
describe("useConnection", () => {
|
||||
const defaultProps = {
|
||||
transportType: "sse" as const,
|
||||
|
|
@ -714,4 +739,142 @@ describe("useConnection", () => {
|
|||
).toHaveProperty("X-MCP-Proxy-Auth", "Bearer test-proxy-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth Error Handling with Scope Discovery", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockAuth.mockResolvedValue("AUTHORIZED");
|
||||
mockDiscoverScopes.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
const setup401Error = () => {
|
||||
const mockErrorEvent = new ErrorEvent("error", {
|
||||
message: "Mock error event",
|
||||
});
|
||||
mockClient.connect.mockRejectedValueOnce(
|
||||
new SseError(401, "Unauthorized", mockErrorEvent),
|
||||
);
|
||||
};
|
||||
|
||||
const attemptConnection = async (props = defaultProps) => {
|
||||
const { result } = renderHook(() => useConnection(props));
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.connect();
|
||||
} catch {
|
||||
// Expected error from auth handling
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testCases = [
|
||||
[
|
||||
"discovers and includes scopes in auth call",
|
||||
{
|
||||
discoveredScope: "read write admin",
|
||||
oauthScope: undefined,
|
||||
expectScopeCall: true,
|
||||
expectedAuthScope: "read write admin",
|
||||
authResult: "AUTHORIZED",
|
||||
},
|
||||
],
|
||||
[
|
||||
"handles scope discovery failure gracefully",
|
||||
{
|
||||
discoveredScope: undefined,
|
||||
oauthScope: undefined,
|
||||
expectScopeCall: true,
|
||||
expectedAuthScope: undefined,
|
||||
authResult: "AUTHORIZED",
|
||||
},
|
||||
],
|
||||
[
|
||||
"uses manual oauthScope override instead of discovered scopes",
|
||||
{
|
||||
discoveredScope: "discovered:scope",
|
||||
oauthScope: "manual:scope",
|
||||
expectScopeCall: false,
|
||||
expectedAuthScope: "manual:scope",
|
||||
authResult: "AUTHORIZED",
|
||||
},
|
||||
],
|
||||
[
|
||||
"triggers scope discovery when oauthScope is whitespace",
|
||||
{
|
||||
discoveredScope: "discovered:scope",
|
||||
oauthScope: " ",
|
||||
expectScopeCall: true,
|
||||
expectedAuthScope: "discovered:scope",
|
||||
authResult: "AUTHORIZED",
|
||||
},
|
||||
],
|
||||
[
|
||||
"handles auth failure after scope discovery",
|
||||
{
|
||||
discoveredScope: "read write",
|
||||
oauthScope: undefined,
|
||||
expectScopeCall: true,
|
||||
expectedAuthScope: "read write",
|
||||
authResult: "UNAUTHORIZED",
|
||||
},
|
||||
],
|
||||
] as const;
|
||||
|
||||
test.each(testCases)(
|
||||
"should %s",
|
||||
async (
|
||||
_,
|
||||
{
|
||||
discoveredScope,
|
||||
oauthScope,
|
||||
expectScopeCall,
|
||||
expectedAuthScope,
|
||||
authResult = "AUTHORIZED",
|
||||
},
|
||||
) => {
|
||||
mockDiscoverScopes.mockResolvedValue(discoveredScope);
|
||||
mockAuth.mockResolvedValue(authResult as never);
|
||||
setup401Error();
|
||||
|
||||
const props =
|
||||
oauthScope !== undefined
|
||||
? { ...defaultProps, oauthScope }
|
||||
: defaultProps;
|
||||
await attemptConnection(props);
|
||||
|
||||
if (expectScopeCall) {
|
||||
expect(mockDiscoverScopes).toHaveBeenCalledWith(
|
||||
defaultProps.sseUrl,
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
expect(mockDiscoverScopes).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), {
|
||||
serverUrl: defaultProps.sseUrl,
|
||||
scope: expectedAuthScope,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("should handle slow scope discovery gracefully", async () => {
|
||||
mockDiscoverScopes.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => setTimeout(() => resolve(undefined), 100)),
|
||||
);
|
||||
|
||||
setup401Error();
|
||||
await attemptConnection();
|
||||
|
||||
expect(mockDiscoverScopes).toHaveBeenCalledWith(
|
||||
defaultProps.sseUrl,
|
||||
undefined,
|
||||
);
|
||||
expect(mockAuth).toHaveBeenCalledWith(expect.any(Object), {
|
||||
serverUrl: defaultProps.sseUrl,
|
||||
scope: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,11 +37,15 @@ import { useToast } from "@/lib/hooks/useToast";
|
|||
import { z } from "zod";
|
||||
import { ConnectionStatus } from "../constants";
|
||||
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
||||
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
import {
|
||||
auth,
|
||||
discoverOAuthProtectedResourceMetadata,
|
||||
} from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
import {
|
||||
clearClientInformationFromSessionStorage,
|
||||
InspectorOAuthClientProvider,
|
||||
saveClientInformationToSessionStorage,
|
||||
discoverScopes,
|
||||
} from "../auth";
|
||||
import packageJson from "../../../package.json";
|
||||
import {
|
||||
|
|
@ -317,9 +321,23 @@ export function useConnection({
|
|||
if (is401Error(error)) {
|
||||
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
|
||||
|
||||
let scope = oauthScope?.trim();
|
||||
if (!scope) {
|
||||
// Only discover resource metadata when we need to discover scopes
|
||||
let resourceMetadata;
|
||||
try {
|
||||
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
|
||||
new URL("/", sseUrl),
|
||||
);
|
||||
} catch {
|
||||
// Resource metadata is optional, continue without it
|
||||
}
|
||||
scope = await discoverScopes(sseUrl, resourceMetadata);
|
||||
}
|
||||
|
||||
const result = await auth(serverAuthProvider, {
|
||||
serverUrl: sseUrl,
|
||||
scope: oauthScope,
|
||||
scope,
|
||||
});
|
||||
return result === "AUTHORIZED";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { OAuthStep, AuthDebuggerState } from "./auth-types";
|
||||
import { DebugInspectorOAuthClientProvider } from "./auth";
|
||||
import { DebugInspectorOAuthClientProvider, discoverScopes } from "./auth";
|
||||
import {
|
||||
discoverAuthorizationServerMetadata,
|
||||
registerClient,
|
||||
|
|
@ -113,10 +113,10 @@ export const oauthTransitions: Record<OAuthStep, StateTransition> = {
|
|||
const metadata = context.state.oauthMetadata!;
|
||||
const clientInformation = context.state.oauthClientInfo!;
|
||||
|
||||
let scope: string | undefined = undefined;
|
||||
if (metadata.scopes_supported) {
|
||||
scope = metadata.scopes_supported.join(" ");
|
||||
}
|
||||
const scope = await discoverScopes(
|
||||
context.serverUrl,
|
||||
context.state.resourceMetadata ?? undefined,
|
||||
);
|
||||
|
||||
const { authorizationUrl, codeVerifier } = await startAuthorization(
|
||||
context.serverUrl,
|
||||
|
|
|
|||
Loading…
Reference in a new issue