Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/product/error-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ Expected failures should:

### Operational Error

An OAuth `access_denied` callback during `auth login` is an expected authorization
refusal, reported as `AUTH.LOGIN_DENIED`, not `CLI.INTERNAL_ERROR`. It does not
create or clear stored sessions. The CLI suggests signing in again only if the
user intends to authorize access. Callback descriptions are not reflected into
diagnostics because they are untrusted input.

An expected external fault, not a product bug.

Examples:
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ A credential's `workspace_id` claim disagrees with the workspace it is being sto

`prisma auth login` completed the browser sign-in but the minted credential carries no `workspace_id` claim, so no workspace session can be keyed by it. The fix is to sign in again and pick a workspace in the browser. Meta: none.

### AUTH.LOGIN_DENIED

The OAuth callback reported `access_denied`. Authorization was not granted; this is an expected refusal, not a CLI crash. No session is created or cleared. Run `prisma auth login` again only to grant access intentionally. The callback description is not echoed. Meta: none.

### AUTH.NO_SESSION_FOR_WORKSPACE

A workspace reference matched none of the stored workspace sessions — raised by the command-side ref resolver behind `prisma auth workspace use` and `prisma auth workspace logout` (exact id match first, then case-insensitive name match), and by the credential managers when a session operation names a workspace with no stored record. Sessions are created only by `prisma auth login`, so the suggested fix is to sign in and pick that workspace in the browser; the workspace reference appears in the message, not in meta. Meta: none.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import http from "node:http";
import type { AddressInfo } from "node:net";
import readline from "node:readline/promises";
import type { Readable, Writable } from "node:stream";
import { CliStructuredError } from "@prisma/cli-engine/protocol";

import {
createManagementApiSdk,
Expand All @@ -12,6 +13,7 @@ import {
type TokenStorage,
} from "@prisma/management-api-sdk";
import open from "open";
import { CLI_NAME } from "../cli-name";
import { CLIENT_ID, getApiBaseUrl } from "./client";
import { FileTokenStorage } from "./token-storage";

Expand Down Expand Up @@ -333,6 +335,21 @@ class LoginState {

const params = url.searchParams;
const error = params.get("error");
if (error === "access_denied") {
throw new CliStructuredError(
"AUTH.LOGIN_DENIED",
"Sign-in was not authorized.",
{
nextActions: [
{
kind: "run-command",
label: "Sign in again if you want to grant access",
command: `${CLI_NAME} auth login`,
},
],
},
);
}
if (error) {
const desc = params.get("error_description");
throw new AuthError(desc ? `${error}: ${desc}` : error);
Expand Down
37 changes: 37 additions & 0 deletions packages/cli/tests/auth-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@ afterEach(() => {
});

describe("auth login callback", () => {
it("reports OAuth denial as an expected refusal without persisting credentials or reflecting callback text", async () => {
const tokenStorage: TokenStorage = {
getTokens: vi.fn().mockResolvedValue(null),
setTokens: vi.fn(),
clearTokens: vi.fn(),
};
const { login } = await import("../src/auth/login");
await expect(
login({
hostname: "127.0.0.1",
tokenStorage,
openUrl: async (authorizationUrl) => {
const redirect = new URL(authorizationUrl).searchParams.get(
"redirect_uri",
);
if (redirect === null) throw new Error("Missing OAuth redirect_uri");
const callback = new URL(redirect);
callback.searchParams.set("error", "access_denied");
callback.searchParams.set(
"error_description",
"private-callback-detail",
);
const response = await fetch(callback);
expect(response.status).toBe(400);
expect(await response.text()).not.toContain(
"private-callback-detail",
);
},
}),
).rejects.toMatchObject({
code: "AUTH.LOGIN_DENIED",
message: "Sign-in was not authorized.",
});
expect(tokenStorage.setTokens).not.toHaveBeenCalled();
expect(tokenStorage.clearTokens).not.toHaveBeenCalled();
});

it("serves the success page as UTF-8 HTML", async () => {
const result = await requestSuccessPage({ workspaceName: "Acme Corp" });

Expand Down
Loading