11import http from 'node:http'
2+ import { randomBytes } from 'node:crypto'
23import { openBrowser } from '../utils/open-browser.js'
34import { loadConfig } from './config.js'
45
@@ -45,6 +46,10 @@ export async function storeToken(token) {
4546 await keytar . setPassword ( 'devvami' , TOKEN_KEY , token )
4647 } catch {
4748 // Fallback: store in config (less secure)
49+ process . stderr . write (
50+ 'Warning: keytar unavailable. ClickUp token will be stored in plaintext.\n' +
51+ 'Run `dvmi auth logout` after this session on shared machines.\n' ,
52+ )
4853 const config = await loadConfig ( )
4954 await saveConfig ( { ...config , clickup : { ...config . clickup , token } } )
5055 }
@@ -57,11 +62,20 @@ export async function storeToken(token) {
5762 * @returns {Promise<string> } Access token
5863 */
5964export async function oauthFlow ( clientId , clientSecret ) {
65+ const csrfState = randomBytes ( 16 ) . toString ( 'hex' )
6066 return new Promise ( ( resolve , reject ) => {
6167 const server = http . createServer ( async ( req , res ) => {
6268 const url = new URL ( req . url ?? '/' , 'http://localhost' )
6369 const code = url . searchParams . get ( 'code' )
70+ const returnedState = url . searchParams . get ( 'state' )
6471 if ( ! code ) return
72+ if ( ! returnedState || returnedState !== csrfState ) {
73+ res . writeHead ( 400 )
74+ res . end ( 'State mismatch — possible CSRF attack.' )
75+ server . close ( )
76+ reject ( new Error ( 'OAuth state mismatch — possible CSRF attack' ) )
77+ return
78+ }
6579 res . end ( 'Authorization successful! You can close this tab.' )
6680 server . close ( )
6781 try {
@@ -80,7 +94,7 @@ export async function oauthFlow(clientId, clientSecret) {
8094 server . listen ( 0 , async ( ) => {
8195 const addr = /** @type {import('node:net').AddressInfo } */ ( server . address ( ) )
8296 const callbackUrl = `http://localhost:${ addr . port } /callback`
83- const authUrl = `https://app.clickup.com/api?client_id=${ clientId } &redirect_uri=${ encodeURIComponent ( callbackUrl ) } `
97+ const authUrl = `https://app.clickup.com/api?client_id=${ clientId } &redirect_uri=${ encodeURIComponent ( callbackUrl ) } &state= ${ csrfState } `
8498 await openBrowser ( authUrl )
8599 } )
86100 server . on ( 'error' , reject )
0 commit comments