@@ -17,6 +17,8 @@ export interface AuthResult {
1717
1818let axiosInstance : AxiosInstance | null = null ;
1919let isAuthenticated = false ;
20+ let authenticationTime : number | null = null ;
21+ const SESSION_TIMEOUT_MS = 3600000 ; // 1 hour
2022
2123debugLog ( `[AUTH] Module loaded - PID: ${ process . pid } ` ) . catch ( ( ) => { } ) ;
2224
@@ -28,7 +30,11 @@ export function getAxiosInstance(): AxiosInstance {
2830 if ( ! axiosInstance ) {
2931 debugLog ( "[AUTH] Creating new axios instance with cookie jar" ) . catch ( ( ) => { } ) ;
3032 const jar = new CookieJar ( ) ;
31- axiosInstance = wrapper ( axios . create ( { jar, withCredentials : true } ) ) ;
33+ axiosInstance = wrapper ( axios . create ( {
34+ jar,
35+ withCredentials : true ,
36+ timeout : 30000 // 30 second timeout
37+ } ) ) ;
3238 } else {
3339 debugLog ( "[AUTH] Reusing existing axios instance" ) . catch ( ( ) => { } ) ;
3440 }
@@ -64,9 +70,16 @@ export async function authenticateWithPassword(
6470 await debugLog ( `[AUTH] Host URL: ${ host } ` ) ;
6571
6672 // Step 1: Get CSRF token (stored in cookie jar automatically)
67- const csrfResponse = await instance . get ( `${ host } auth/get-csrf-token/` ) ;
73+ // Use explicit path to ensure we capture path-scoped cookies
74+ const csrfUrl = `${ host } auth/get-csrf-token/` ;
75+ const csrfResponse = await instance . get ( csrfUrl ) ;
6876 await debugLog ( `[AUTH] CSRF response status: ${ csrfResponse . status } ` ) ;
69- await debugLog ( `[AUTH] CSRF response headers: ${ JSON . stringify ( csrfResponse . headers ) } ` ) ;
77+
78+ // Only log headers if verbose debug is enabled to avoid leaking config details
79+ if ( process . env . PLANE_MCP_DEBUG === 'verbose' ) {
80+ await debugLog ( `[AUTH] CSRF response headers: ${ JSON . stringify ( csrfResponse . headers ) } ` ) ;
81+ }
82+
7083 await debugLog ( "[AUTH] CSRF token requested" ) ;
7184
7285 // Step 2: Extract CSRF token from cookie jar for the request header
@@ -76,7 +89,8 @@ export async function authenticateWithPassword(
7689 return { success : false , error : "cookies" , message : "Cookie jar not available for session authentication" } ;
7790 }
7891 const jar = maybeJar ;
79- const cookies = await jar . getCookies ( host ) ;
92+ // Check cookies on the specific CSRF URL to ensure we get path-scoped cookies
93+ const cookies = await jar . getCookies ( csrfUrl ) ;
8094 await debugLog ( `[AUTH] Cookies after CSRF request: ${ cookies . map ( c => c . key ) . join ( ", " ) } ` ) ;
8195
8296 const csrfCookie = cookies . find ( ( c ) => [ "csrftoken" , "csrf" , "XSRF-TOKEN" ] . includes ( c . key ) ) ;
@@ -106,7 +120,7 @@ export async function authenticateWithPassword(
106120 "Content-Type" : "application/x-www-form-urlencoded" ,
107121 } ,
108122 maxRedirects : 0 , // Don't follow redirects, we just need the cookies
109- validateStatus : ( status ) => status >= 200 && status < 400 , // Accept redirects as success
123+ validateStatus : ( status ) => ( status >= 200 && status < 300 ) || status === 302 , // Accept 2xx and 302 (redirect) as success
110124 }
111125 ) ;
112126
@@ -115,16 +129,17 @@ export async function authenticateWithPassword(
115129 const headerNames = Object . keys ( loginResponse . headers ?? { } ) ;
116130 await debugLog ( `[AUTH] Login response headers present: ${ headerNames . join ( ", " ) } ` ) ;
117131
118- // Log ALL headers for debugging
119- await debugLog ( `[AUTH] Login response headers FULL: ${ JSON . stringify ( loginResponse . headers ) } ` ) ;
132+ // Log ALL headers for debugging ONLY if verbose
133+ if ( process . env . PLANE_MCP_DEBUG === 'verbose' ) {
134+ await debugLog ( `[AUTH] Login response headers FULL: ${ JSON . stringify ( loginResponse . headers ) } ` ) ;
135+ }
120136
121137 // Check if Set-Cookie headers are present
122138 const setCookieHeader = loginResponse . headers [ 'set-cookie' ] ;
123139 if ( setCookieHeader ) {
124140 await debugLog ( `[AUTH] Set-Cookie headers received: ${ Array . isArray ( setCookieHeader ) ? setCookieHeader . length : 1 } cookie(s)` ) ;
125141 } else {
126142 await debugLog ( `[AUTH] WARNING: No Set-Cookie headers in login response! Checking cookie jar anyway...` ) ;
127- // We don't return error here, we assume cookies might be in the jar (e.g. from redirects or axios processing)
128143 }
129144
130145 // Verify cookies were stored in the jar
@@ -137,17 +152,16 @@ export async function authenticateWithPassword(
137152 const sessionCookie = loginCookies . find ( ( c ) => sessionCookieNames . includes ( c . key ) ) ;
138153 if ( ! sessionCookie ) {
139154 await debugLog ( `[AUTH] WARNING: No standard session cookie found (looked for: ${ sessionCookieNames . join ( ", " ) } )` ) ;
140- // We don't return error here anymore, we let the verification step decide
141155 }
142156
143- // Log full cookie details for debugging
144- loginCookies . forEach ( c => {
145- // Redacted logging of cookie values
146- debugLog ( `[AUTH] Cookie detail - ${ c . key } : domain=${ c . domain } , path=${ c . path } , httpOnly=${ c . httpOnly } , secure=${ c . secure } ` ) . catch ( ( ) => { } ) ;
147- } ) ;
157+ // Log full cookie details for debugging - gated
158+ if ( process . env . PLANE_MCP_DEBUG === 'verbose' ) {
159+ loginCookies . forEach ( c => {
160+ debugLog ( `[AUTH] Cookie detail - ${ c . key } : domain=${ c . domain } , path=${ c . path } , httpOnly=${ c . httpOnly } , secure=${ c . secure } ` ) . catch ( ( ) => { } ) ;
161+ } ) ;
162+ }
148163
149164 // Verify the session works with a test API call
150- // Note: Use /api/ endpoint (not /api/v1/) since session cookies work with /api/ endpoints
151165 try {
152166 const verifyUrl = `${ host } api/users/me/` ;
153167 await debugLog ( `[AUTH] Attempting to verify session with: ${ verifyUrl } ` ) ;
@@ -174,9 +188,14 @@ export async function authenticateWithPassword(
174188 }
175189
176190 isAuthenticated = true ;
191+ authenticationTime = Date . now ( ) ;
177192 await debugLog ( "[AUTH] Authentication successful" ) ;
178193 return { success : true } ;
179194 } catch ( error ) {
195+ // Reset auth state on failure to avoid stale state
196+ isAuthenticated = false ;
197+ authenticationTime = null ;
198+
180199 await debugLog ( `[AUTH] Authentication failed: ${ error } ` ) ;
181200
182201 if ( axios . isAxiosError ( error ) ) {
@@ -198,6 +217,19 @@ export async function authenticateWithPassword(
198217 * @returns true if authenticated, false otherwise
199218 */
200219export function isSessionAuthenticated ( ) : boolean {
220+ if ( ! isAuthenticated || ! authenticationTime ) {
221+ debugLog ( `[AUTH] isSessionAuthenticated() - not authenticated` ) . catch ( ( ) => { } ) ;
222+ return false ;
223+ }
224+
225+ const isStale = Date . now ( ) - authenticationTime > SESSION_TIMEOUT_MS ;
226+ if ( isStale ) {
227+ debugLog ( `[AUTH] Session expired, resetting authentication` ) . catch ( ( ) => { } ) ;
228+ isAuthenticated = false ;
229+ authenticationTime = null ;
230+ return false ;
231+ }
232+
201233 debugLog ( `[AUTH] isSessionAuthenticated() called - returning: ${ isAuthenticated } ` ) . catch ( ( ) => { } ) ;
202234 return isAuthenticated ;
203235}
@@ -230,6 +262,7 @@ export async function resetAuthentication(): Promise<void> {
230262 } finally {
231263 axiosInstance = null ;
232264 isAuthenticated = false ;
265+ authenticationTime = null ;
233266 await debugLog ( "[AUTH] Authentication reset" ) ;
234267 }
235- }
268+ }
0 commit comments