fix(auth): keep CLI approval links on the public instance origin

Use the existing canonical public origin resolver instead of proxy request headers when building the CLI browser approval URL. Cover upstream forwarded-host replacement with a route regression.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-09 11:08:35 -06:00
1 parent 1b7fe33432
commit 9dd05f2aa3
3 files changed
+30 -3

No files matched your search

+3 -1
View File
@@ -9,7 +9,9 @@ Paperclip CLI now supports both:
## Browser-approved Board authentication
`npx paperclipai auth login --api-base <instance-url>` creates a ten-minute
challenge and prints a browser approval link. Sign in to the instance, review
challenge and prints a browser approval link. The link uses the instance's
canonical public origin when configured, so reverse proxies do not send the
browser to an internal upstream hostname. Sign in to the instance, review
the requesting command and full Board access, then approve. The pending token
cannot authenticate before approval. The CLI verifies its resulting identity
before storing its credential in its private auth store. The default Board key
+26 -1
View File
@@ -53,7 +53,7 @@ function registerModuleMocks() {
let appImportCounter = 0;
async function createApp(actor: any, db: any = {} as any, options: { cliAuthCreateRateLimiter?: InviteRateLimiter; cliAuthPollRateLimiter?: InviteRateLimiter } = {}) {
async function createApp(actor: any, db: any = {} as any, options: { authPublicBaseUrl?: string; cliAuthCreateRateLimiter?: InviteRateLimiter; cliAuthPollRateLimiter?: InviteRateLimiter } = {}) {
appImportCounter += 1;
const routeModulePath = `../routes/access.js?cli-auth-routes-${appImportCounter}`;
const middlewareModulePath = `../middleware/index.js?cli-auth-routes-${appImportCounter}`;
@@ -137,6 +137,31 @@ describe("cli auth routes", () => {
expect(res.body.approvalUrl).toContain("/cli-auth/12345678-1234-4123-8123-123456789abc?token=pcp_cli_auth_secret");
});
it("uses the configured public origin for CLI approval behind an upstream proxy", async () => {
mockBoardAuthService.createCliAuthChallenge.mockResolvedValue({
challenge: {
id: "12345678-1234-4123-8123-123456789abc",
expiresAt: new Date(Date.now() + 60_000),
},
challengeSecret: "pcp_cli_auth_secret",
pendingBoardToken: "pcp_board_token",
});
const app = await createApp({ type: "none", source: "none" }, {}, {
authPublicBaseUrl: "https://workspace.example.com/",
});
const res = await request(app)
.post("/api/cli-auth/challenges")
.set("Host", "upstream.example.net")
.set("X-Forwarded-Host", "upstream.example.net")
.set("X-Forwarded-Proto", "https")
.send({ command: "paperclipai auth login", requestedAccess: "board" });
expect(res.status).toBe(201);
expect(res.body.approvalUrl).toBe(
`https://workspace.example.com${res.body.approvalPath}`,
);
});
it("bounds anonymous create and poll independently without trusting spoofed forwarding headers", async () => {
let now = 1_000;
mockBoardAuthService.createCliAuthChallenge.mockResolvedValue({
+1 -1
View File
@@ -2795,7 +2795,7 @@ export function accessRoutes(
created.challenge.id,
created.challengeSecret,
);
const baseUrl = requestBaseUrl(req);
const baseUrl = resolveBaseUrl(req, opts.authPublicBaseUrl);
res.status(201).json({
id: created.challenge.id,
token: created.challengeSecret,