validate(['api' => ['nullable', 'string', 'max:32']]); $code = DeviceCode::generateCode(); $pollToken = DeviceCode::generatePollToken(); DeviceCode::create([ 'code' => $code, 'poll_token_hash' => hash('sha256', $pollToken), 'expires_at' => now()->addMinutes(10), ]); return response()->json([ 'code' => $code, 'poll_token' => $pollToken, 'verify_url' => url('/cli/authorize/'.$code), 'expires_in' => 600, ], 201); } /** * Step 3: the CLI polls until the user confirms in the browser. * Returns pending / expired / granted{token}. */ public function poll(Request $request): JsonResponse { $validated = $request->validate([ 'code' => ['required', 'string'], 'poll_token' => ['required', 'string'], ]); $device = DeviceCode::where('code', $validated['code'])->first(); if ($device === null || ! hash_equals($device->poll_token_hash, hash('sha256', $validated['poll_token']))) { // Same answer for unknown code and wrong token: no oracle. return response()->json(['status' => 'unknown'], 404); } if ($device->expires_at->isPast()) { return response()->json(['status' => 'expired']); } if ($device->consumed_at !== null) { // The token can be picked up exactly once by poll; a later poll // after consumption is a replay and gets a bare granted status. return response()->json(['status' => 'granted']); } if ($device->user_id === null) { return response()->json(['status' => 'pending']); } $token = $device->personalAccessToken; if ($token === null || $token->expires_at !== null && $token->expires_at->isPast()) { return response()->json(['status' => 'expired']); } $device->forceFill(['consumed_at' => now()])->save(); return response()->json([ 'status' => 'granted', 'token' => $token->token, 'abilities' => $token->abilities, ]); } }