Conversation
…ticket auth Implement POST /v1/provision to headless upsert local user and organization projections from DOS ID and issue one-time login tickets, and add /auth/ticket consumer to eliminate interactive onboarding for agent connections. Co-authored-by: Cursor <cursoragent@cursor.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_760ab92d-bd04-45bf-8305-ac42166679a6) |
There was a problem hiding this comment.
Code Review
This pull request introduces a first-party headless provisioning and one-time ticket authentication flow, adding the ProvisionController in the backend, a client-side ticket consumption page in the frontend, and corresponding API documentation and DTOs. Key feedback includes a critical security vulnerability where authentication fails open if no secret is configured, a potential bug where newly provisioned users are created inactive and cannot consume tickets, a vulnerability to replay attacks on the stateless one-time tickets, a recommendation to use sameSite: 'lax' instead of 'none' for cookies to prevent CSRF, and a Next.js best practice to wrap useSearchParams in a <Suspense> boundary.
| if (!secret) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Security Vulnerability: Fail-Open Authentication
If none of the environment variables (PROVISIONING_SECRET_KEY, DOS_PROVISIONING_SECRET, etc.) are configured, verifyAuth returns true. This creates a critical security vulnerability where the /provision endpoint becomes completely public and unauthenticated in environments where these variables are missing or misconfigured.
Recommendation:
Fail secure by returning false if no secret is configured.
| if (!secret) { | |
| return true; | |
| } | |
| if (!secret) { | |
| return false; | |
| } |
| user = created.users[0].user; | ||
| targetOrg = created; |
There was a problem hiding this comment.
Potential Bug: Provisioned User is Inactive
When a user is created via createOrgAndUser, they are typically created with activated: false (requiring email verification). However, in consumeTicket (line 188), there is a strict check that throws an error if the user is not activated. Since this is a headless provisioning flow for trusted first-party integrations, the user should be activated automatically upon creation so they can consume the ticket immediately.
Recommendation:
Explicitly activate the user immediately after creation.
| user = created.users[0].user; | |
| targetOrg = created; | |
| user = created.users[0].user; | |
| await this._userService.activateUser(user.id); | |
| targetOrg = created; |
| let payload: any; | ||
| try { | ||
| payload = AuthChecker.verifyJWT(body.ticket); | ||
| } catch (e) { | ||
| throw new HttpException('Invalid or expired ticket', HttpStatus.BAD_REQUEST); | ||
| } |
There was a problem hiding this comment.
Security Issue: Replay Attack on "One-Time" Ticket
The ticket issued is a stateless JWT. Although it has a short expiration (5 minutes), there is no mechanism to track whether a ticket has already been consumed. An attacker who intercepts the ticket can reuse the same ticket multiple times within its 5-minute validity window.
Recommendation:
To make the ticket truly "one-time", implement a blocklist or consumption tracker (e.g., using Redis with a 5-minute TTL or a database table) to record consumed ticket IDs (using a jti claim in the JWT) and reject any ticket that has already been consumed.
| ...(!process.env.NOT_SECURED | ||
| ? { | ||
| secure: true, | ||
| httpOnly: true, | ||
| sameSite: 'none', | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
Security Recommendation: Use sameSite: 'lax' instead of 'none'
Setting sameSite: 'none' allows the authentication cookie to be sent on cross-site requests, which exposes the application to CSRF (Cross-Site Request Forgery) attacks. Since the cookie is scoped to the domain/subdomain of FRONTEND_URL (using getCookieUrlFromDomain), sameSite: 'lax' is a much safer default and is sufficient for subdomain-based cookie sharing.
| ...(!process.env.NOT_SECURED | |
| ? { | |
| secure: true, | |
| httpOnly: true, | |
| sameSite: 'none', | |
| } | |
| : {}), | |
| ...(!process.env.NOT_SECURED | |
| ? { | |
| secure: true, | |
| httpOnly: true, | |
| sameSite: 'lax', | |
| } | |
| : {}), |
| 'use client'; | ||
|
|
||
| import { useEffect, useState } from 'react'; | ||
| import { useSearchParams } from 'next/navigation'; | ||
| import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; | ||
| import { LoadingComponent } from '@gitroom/frontend/components/layout/loading'; | ||
|
|
||
| export default function TicketAuthPage() { | ||
| const searchParams = useSearchParams(); | ||
| const fetch = useFetch(); | ||
| const [error, setError] = useState(''); | ||
|
|
||
| const ticket = searchParams.get('ticket'); | ||
| const redirectTo = searchParams.get('redirect_to') || '/'; | ||
|
|
||
| useEffect(() => { | ||
| if (!ticket) { | ||
| setError('Missing ticket parameter'); | ||
| return; | ||
| } | ||
|
|
||
| fetch('/v1/ticket/consume', { | ||
| method: 'POST', | ||
| body: JSON.stringify({ | ||
| ticket, | ||
| redirect_to: redirectTo, | ||
| }), | ||
| }) | ||
| .then((r) => r.json()) | ||
| .then((data) => { | ||
| if (data.success) { | ||
| window.location.href = data.redirect_to || redirectTo; | ||
| } else { | ||
| setError(data.message || 'Invalid or expired ticket'); | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| setError('Failed to consume authentication ticket'); | ||
| }); | ||
| }, [ticket, redirectTo]); | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div className="flex flex-1 items-center justify-center text-white"> | ||
| <div className="bg-third p-8 rounded-xl border border-tableBorder text-center max-w-md"> | ||
| <h2 className="text-xl font-bold text-red-500 mb-2">Authentication Error</h2> | ||
| <p className="text-gray-300 text-sm mb-4">{error}</p> | ||
| <a | ||
| href="/auth/login" | ||
| className="inline-block bg-btnPrimary px-4 py-2 rounded-lg text-sm font-medium" | ||
| > | ||
| Go to Login | ||
| </a> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return <LoadingComponent />; | ||
| } |
There was a problem hiding this comment.
Next.js Best Practice: Missing Suspense Boundary for useSearchParams
In Next.js (App Router), calling useSearchParams() in a client component during static rendering will cause the entire page to deoptimize to client-side rendering or throw an error during the production build unless it is wrapped in a <Suspense> boundary.
Recommendation:
Wrap the component or the hook usage in a <Suspense> boundary to ensure successful static builds and proper loading states.
'use client';
import { useEffect, useState, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
import { LoadingComponent } from '@gitroom/frontend/components/layout/loading';
function TicketAuthContent() {
const searchParams = useSearchParams();
const fetch = useFetch();
const [error, setError] = useState('');
const ticket = searchParams.get('ticket');
const redirectTo = searchParams.get('redirect_to') || '/';
useEffect(() => {
if (!ticket) {
setError('Missing ticket parameter');
return;
}
fetch('/v1/ticket/consume', {
method: 'POST',
body: JSON.stringify({
ticket,
redirect_to: redirectTo,
}),
})
.then((r) => r.json())
.then((data) => {
if (data.success) {
window.location.href = data.redirect_to || redirectTo;
} else {
setError(data.message || 'Invalid or expired ticket');
}
})
.catch((err) => {
setError('Failed to consume authentication ticket');
});
}, [ticket, redirectTo]);
if (error) {
return (
<div className="flex flex-1 items-center justify-center text-white">
<div className="bg-third p-8 rounded-xl border border-tableBorder text-center max-w-md">
<h2 className="text-xl font-bold text-red-500 mb-2">Authentication Error</h2>
<p className="text-gray-300 text-sm mb-4">{error}</p>
<a
href="/auth/login"
className="inline-block bg-btnPrimary px-4 py-2 rounded-lg text-sm font-medium"
>
Go to Login
</a>
</div>
</div>
);
}
return <LoadingComponent />;
}
export default function TicketAuthPage() {
return (
<Suspense fallback={<LoadingComponent />}>
<TicketAuthContent />
</Suspense>
);
}
|
DOS.AI connector UAT is blocked on a Crove Post hotfix. PR #13 was merged with unresolved review findings that affect the first-party bootstrap path:
Please ship these fixes to Beta first and provide the deployed commit/revision. DOS.AI will keep Crove Post unbound from Em Hương until the Beta contract is verified. NueLink remains independent and operational. |
|
DOSClaw handoff update on 2026-08-27: The Crove Post team reports that the follow-up hotfix is implementation-ready with:
Current GitHub evidence still shows no hotfix branch or PR, and |
Official Handover & Security Verification Evidence for DOSClaw / DOS.AITo: DOSClaw Team & DOS.AI Maintainers 1. Handover References & Artifacts
2. Live Security & Concurrency Test ResultsTest 1: Health Check Endpoint
{
"status": "ok",
"timestamp": "2026-08-28T08:44:09.705Z",
"service": "crove-post"
}Test 2: Fail-Closed Authentication (Missing / Invalid Secret)
Test 3: One-Time Ticket Replay Protection
Test 4: Concurrent Execution (Race Condition Protection)
3. Conclusion & Next Steps for DOS.AIAll 7 handover requirements have been fully verified against the live environment. The blocker is resolved, and DOS.AI is cleared to proceed with merging DOS.AI PR #1912 and executing OAuth UAT for Agent Em Hương. |
What kind of change does this PR introduce?
Feature & Zero-Friction Autonomous Agent Integration
Why was this change needed?
Implements First-Party Headless Provisioning & One-Time Ticket Auth:
POST /v1/provision: Allows internal services (e.g.api.dos.me/ DOSClaw) to idempotently upsert local User and Workspace projections without requiring user interaction, returning a 5-minute one-time authentication ticket./auth/ticketFrontend Consumer: Intercepts one-time tickets, invokes/v1/ticket/consumeto set secure session cookies, and immediately forwards the user directly to the OAuth Authorize Consent screen (bypassing Login, Register, and Company onboarding).docs/first-party-provisioning.mdwith full request/response schemas.Checklist:
Note
High Risk
Introduces privileged account/org provisioning and a ticket-based session bootstrap; misconfigured provisioning secrets (
verifyAuthsucceeds when no secret is set) or leaked tickets could allow unauthorized access.Overview
Adds headless first-party provisioning so trusted internal callers can upsert users/orgs and get a short-lived login handoff without UI onboarding.
Backend: New
ProvisionControllerexposesPOST /v1/provision(Bearer secret; idempotent user/org upsert viaGENERICprovider + org membership) and returns a 5-minuteone_time_ticketJWT plusloginUrl.POST /v1/ticket/consumevalidates that ticket, issues a normal session JWT, setsauth/showorgcookies, and returnsredirect_to.AuthService.jwtis now public so ticket consumption can mint sessions. Controller is wired inapi.moduleoutside the authenticated middleware group.Frontend: New
/auth/ticketpage POSTs the ticket to consume and redirects on success. Proxy no longer auto-redirects logged-in users away from/auth/ticket.Shared/docs: Validation DTOs for provision and consume payloads;
docs/first-party-provisioning.mddocuments both endpoints and ticket flow.Reviewed by Cursor Bugbot for commit 03bfebf. Bugbot is set up for automated code reviews on this repo. Configure here.