fix(auth): clear OAuth callback timer and reorder EMA scope persist
Clear the loopback callback timeout in runner-interactive-oauth finally so already_authorized and error paths do not hang the CLI/TUI. Check EMA step-up satisfaction against minted token scope before persisting the requested union, matching the standard OAuth path. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0635d09e83
commit
52023ff1c3
2 changed files with 92 additions and 10 deletions
|
|
@ -84,6 +84,34 @@ describe("runRunnerInteractiveOAuth", () => {
|
|||
expect(client.completeOAuthFlow).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("clears the callback timeout on already_authorized", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const client = mockClient({
|
||||
authenticate: vi.fn(async () => undefined),
|
||||
});
|
||||
const redirectUrlProvider = { redirectUrl: "" };
|
||||
|
||||
const result = await runRunnerInteractiveOAuth({
|
||||
client,
|
||||
redirectUrlProvider,
|
||||
callbackListen: {
|
||||
hostname: "127.0.0.1",
|
||||
port: 6276,
|
||||
pathname: "/oauth/callback",
|
||||
},
|
||||
callbackTimeoutMs: 60_000,
|
||||
createCallbackServer: () => createMockCallbackServer(handlers),
|
||||
});
|
||||
|
||||
expect(result).toEqual({ kind: "already_authorized" });
|
||||
expect(vi.getTimerCount()).toBe(0);
|
||||
await vi.advanceTimersByTimeAsync(60_000);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("completes connect-time OAuth via authenticate and callback", async () => {
|
||||
const redirectUrlProvider = { redirectUrl: "" };
|
||||
const client = mockClient({
|
||||
|
|
|
|||
|
|
@ -1215,9 +1215,6 @@ describe("OAuthManager", () => {
|
|||
});
|
||||
|
||||
it("returns satisfied for EMA insufficient_scope after user confirms", async () => {
|
||||
const silentSpy = vi
|
||||
.spyOn(emaFlow, "trySilentEmaAuth")
|
||||
.mockResolvedValue({ status: "success" });
|
||||
const params = createMockParams({
|
||||
enterpriseManagedAuth: {
|
||||
idp: {
|
||||
|
|
@ -1237,6 +1234,14 @@ describe("OAuthManager", () => {
|
|||
token_type: "Bearer",
|
||||
scope: "mcp",
|
||||
});
|
||||
const silentSpy = vi.spyOn(emaFlow, "trySilentEmaAuth").mockImplementation(async () => {
|
||||
storageOf(params).getTokens.mockResolvedValue({
|
||||
access_token: "tok",
|
||||
token_type: "Bearer",
|
||||
scope: "mcp weather:read",
|
||||
});
|
||||
return { status: "success" };
|
||||
});
|
||||
const manager = new OAuthManager(params);
|
||||
manager.setOAuthConfig({ enterpriseManaged: true });
|
||||
|
||||
|
|
@ -1257,6 +1262,51 @@ describe("OAuthManager", () => {
|
|||
silentSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("does not persist union scope or return satisfied when silent EMA mint is down-scoped", async () => {
|
||||
const silentSpy = vi
|
||||
.spyOn(emaFlow, "trySilentEmaAuth")
|
||||
.mockResolvedValue({ status: "success" });
|
||||
const authUrl = new URL("https://idp.example.com/authorize?state=ema");
|
||||
const startSpy = vi
|
||||
.spyOn(emaFlow, "startEmaIdpAuthorization")
|
||||
.mockResolvedValue(authUrl);
|
||||
const params = createMockParams({
|
||||
enterpriseManagedAuth: {
|
||||
idp: {
|
||||
issuer: "https://idp.example.com",
|
||||
clientId: "app-client",
|
||||
clientSecret: "secret",
|
||||
},
|
||||
},
|
||||
});
|
||||
storageOf(params).getScope.mockReturnValue("mcp");
|
||||
storageOf(params).getTokens.mockResolvedValue({
|
||||
access_token: "tok",
|
||||
token_type: "Bearer",
|
||||
scope: "mcp",
|
||||
});
|
||||
const manager = new OAuthManager(params);
|
||||
manager.setOAuthConfig({ enterpriseManaged: true });
|
||||
|
||||
const outcome = await manager.handleAuthChallenge(
|
||||
{
|
||||
reason: "insufficient_scope",
|
||||
requiredScopes: ["weather:read"],
|
||||
},
|
||||
{ confirmedStepUp: true },
|
||||
);
|
||||
|
||||
expect(outcome).toEqual(
|
||||
expect.objectContaining({
|
||||
kind: "interactive",
|
||||
authorizationUrl: authUrl,
|
||||
}),
|
||||
);
|
||||
expect(storageOf(params).saveScope).not.toHaveBeenCalled();
|
||||
silentSpy.mockRestore();
|
||||
startSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("completeOAuthFlow mints EMA tokens with pending step-up union scope", async () => {
|
||||
const silentSpy = vi
|
||||
.spyOn(emaFlow, "trySilentEmaAuth")
|
||||
|
|
|
|||
Loading…
Reference in a new issue