
Как запечатлеть обратные вызовы OAuth в CLI и настольные приложения с местными серверами
20 августа 2025 г.При создании инструментов CLI или настольных приложений, которые интегрируются с поставщиками OAuth, вы сталкиваетесь с уникальной задачей: как вы понимаете код авторизации, когда нет общего сервера для получения обратного вызова? Ответ заключается в умной технике, которая была прямо под нашим носом - раскручивая временный сервер локального хозяйства, чтобы поймать перенаправление OAuth.
Этот учебник проходит через создание готового производства сервера обратного вызова OAuth, который работает через Node.js, Deno и Bun. Мы рассмотрим все, от базовой настройки HTTP -сервера до обработки краев, которые отключаются большинство реализаций.
Понимание потока обратного вызова OAuth
Прежде чем погрузиться в код, давайте проясним, что мы строим. В типичном потоке кода авторизации OAuth 2.0 ваше приложение перенаправляет пользователей на сервер авторизации (например, Github или Google), где они предоставляют разрешения. Затем сервер авторизации перенаправляет обратно в ваше приложение с помощью кода авторизации.
Для веб -приложений этот перенаправление идет на публичный URL. Но для инструментов CLI и настольных приложений мы используем Localhost UR - обычноhttp: // localhost: 3000/обратный вызовПолем Провайдер OAuth перенаправляет на этот локальный адрес, и наш временный сервер захватывает код авторизации из параметров запроса.
Этот подход явно благословлен OAuth 2.0 для нативных приложений (RFC 8252) и используется крупными инструментами, такими как GitHub CLI и библиотеки Google OAuth.
Настройка базового HTTP -сервера
Первым шагом является создание HTTP -сервера, который может прослушать Localhost. Современные JavaScript Runtimes предоставляют для этого различные API, но мы можем абстрагировать их за общий интерфейс, используя объекты запроса веб -стандартов и ответа.
interface CallbackServer {
start(options: ServerOptions): Promise<void>;
waitForCallback(path: string, timeout: number): Promise<CallbackResult>;
stop(): Promise<void>;
}
function createCallbackServer(): CallbackServer {
// Runtime detection
if (typeof Bun !== "undefined") return new BunCallbackServer();
if (typeof Deno !== "undefined") return new DenoCallbackServer();
return new NodeCallbackServer();
}
Каждая реализация времени выполнения следует за одной и той же шаблоном: создайте сервер, прослушайте запросы и разрешайте обещание, когда приходит обратный вызов. Вот версия node.js, которая соединяется между узломhttp
Модуль и веб -стандарты:
class NodeCallbackServer implements CallbackServer {
private server?: http.Server;
private callbackPromise?: {
resolve: (result: CallbackResult) => void;
reject: (error: Error) => void;
};
async start(options: ServerOptions): Promise<void> {
const { createServer } = await import("node:http");
return new Promise((resolve, reject) => {
this.server = createServer(async (req, res) => {
const request = this.nodeToWebRequest(req, options.port);
const response = await this.handleRequest(request);
res.writeHead(
response.status,
Object.fromEntries(response.headers.entries()),
);
res.end(await response.text());
});
this.server.listen(options.port, options.hostname, resolve);
this.server.on("error", reject);
});
}
private nodeToWebRequest(req: http.IncomingMessage, port: number): Request {
const url = new URL(req.url!, `http://localhost:${port}`);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (typeof value === "string") {
headers.set(key, value);
}
}
return new Request(url.toString(), {
method: req.method,
headers,
});
}
}
}
Красота этого подхода в том, что после того, как мы преобразуемся в веб -стандарты, фактическая логика обработки запросов идентична во всех временах.
Захват обратного вызова OAuth
Сердцем нашего сервера является обработчик обратного вызова. Когда провайдер OAuth перенаправляет обратно, нам необходимо извлечь код авторизации (или ошибку) из параметров запроса:
private async handleRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === this.callbackPath) {
const params: CallbackResult = {};
// Extract all query parameters
for (const [key, value] of url.searchParams) {
params[key] = value;
}
// Resolve the waiting promise
if (this.callbackPromise) {
this.callbackPromise.resolve(params);
}
// Return success page to the browser
return new Response(this.generateSuccessHTML(), {
status: 200,
headers: { "Content-Type": "text/html" }
});
}
return new Response("Not Found", { status: 404 });
}
Обратите внимание, как мы захватываем все параметры запроса, а не только код авторизации. Провайдеры OAuth отправляют дополнительную информацию, такую как состояние для защиты CSRF, а ответы на ошибки включаютerror
иerror_description
поля. Наша реализация сохраняет все для максимальной гибкости.
Обработка тайм -аутов и отмены
Реальные потоки OAuth могут потерпеть неудачу во многих отношениях. Пользователи могут закрыть браузер, отказать в разрешениях или просто уйти. Наш сервер нуждается в устойчивом временном времени и обработке отмены:
async waitForCallback(path: string, timeout: number): Promise<CallbackResult> {
this.callbackPath = path;
return new Promise((resolve, reject) => {
let isResolved = false;
// Set up timeout
const timer = setTimeout(() => {
if (!isResolved) {
isResolved = true;
reject(new Error(`OAuth callback timeout after ${timeout}ms`));
}
}, timeout);
// Wrap resolve/reject to handle cleanup
const wrappedResolve = (result: CallbackResult) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timer);
resolve(result);
}
};
this.callbackPromise = {
resolve: wrappedResolve,
reject: (error) => {
if (!isResolved) {
isResolved = true;
clearTimeout(timer);
reject(error);
}
}
};
});
}
ПоддержкаAbortSignal
Включает программную отмену, необходимую для приложений с графическим интерфейсом, где пользователи могут закрыть окно в середине потока:
if (signal) {
if (signal.aborted) {
throw new Error("Operation aborted");
}
const abortHandler = () => {
this.stop();
if (this.callbackPromise) {
this.callbackPromise.reject(new Error("Operation aborted"));
}
};
signal.addEventListener("abort", abortHandler);
}
Предоставление отзывов пользователей
Когда пользователи заполняют поток OAuth, они видят страницу браузера, указывающую на успех или неудачу. Вместо пустой страницы или загадочного сообщения предоставьте четкую обратную связь с пользовательским HTML:
function generateCallbackHTML(
params: CallbackResult,
templates: Templates,
): string {
if (params.error) {
// OAuth error - show error page
return templates.errorHtml
.replace(/{{error}}/g, params.error)
.replace(/{{error_description}}/g, params.error_description || "");
}
// Success - show confirmation
return (
templates.successHtml ||
`
<html>
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
<h1>✅ Authorization successful!</h1>
<p>You can now close this window and return to your terminal.</p>
</body>
</html>
`
);
}
Для производственных приложений рассмотрите возможность добавления анимации CSS, функциональности автоматического закрытия или глубоких ссылок обратно в ваше настольное приложение.
Соображения безопасности
Хотя местные серверы по своей природе более безопасны, чем общедоступные конечные точки, несколько мер безопасности имеют решающее значение:
- Связываться только с локальным хостом: Никогда не связывайтесь с
0.0.0.0
или публичные интерфейсы. Это предотвращает атаки на основе сети:
this.server.listen(port, "localhost"); // NOT "0.0.0.0"
2Проверить параметр состояния: Параметр состояния Оаута предотвращает атаки CSRF. Создайте его перед началом потока и подтвердите его в обратном вызове:
const state = crypto.randomBytes(32).toString("base64url");
const authUrl = `${provider}/authorize?state=${state}&...`;
// In callback handler
if (params.state !== expectedState) {
throw new Error("State mismatch - possible CSRF attack");
}
3Немедленно закрыть сервер: Как только вы получите обратный вызов, выключите сервер, чтобы минимизировать поверхность атаки:
const result = await server.waitForCallback("/callback", 30000);
await server.stop(); // Always cleanup
4Используйте непредсказуемые порты, когда это возможно: Если ваш поставщик OAuth поддерживает Dynamic Redirect URI, используйте случайные высокие порты для предотвращения атак портов.
Сделать все это вместе
Вот полный пример, который связывает все вместе:
import { createCallbackServer } from "./server";
import { spawn } from "child_process";
export async function getAuthCode(authUrl: string): Promise<string> {
const server = createCallbackServer();
try {
// Start the server
await server.start({
port: 3000,
hostname: "localhost",
successHtml: "<h1>Success! You can close this window.</h1>",
errorHtml: "<h1>Error: {{error_description}}</h1>",
});
// Open the browser
const opener =
process.platform === "darwin"
? "open"
: process.platform === "win32"
? "start"
: "xdg-open";
spawn(opener, [authUrl], { detached: true });
// Wait for callback
const result = await server.waitForCallback("/callback", 30000);
if (result.error) {
throw new Error(`OAuth error: ${result.error_description}`);
}
return result.code!;
} finally {
// Always cleanup
await server.stop();
}
}
// Usage
const code = await getAuthCode(
"https://github.com/login/oauth/authorize?" +
"client_id=xxx&redirect_uri=http://localhost:3000/callback",
);
Лучшие практики и следующие шаги
Создание надежного сервера обратного вызова OAuth требует внимания к деталям, но шаблоны согласованы между реализациями. Ключевые выводы:
- Используйте API -интерфейсы веб -стандартовдля совместимости перекрестного выхода
- Обработайте все случаи ошибоквключая тайм -ауты и отмену пользователей
- Предоставьте четкие отзывы пользователейС пользовательским успехом/страницами ошибок
- Реализовать меры безопасностикак проверка состояния и обязательство локального хоста
- Очистить ресурсыВсегда останавливая сервер после использования
Этот подход Localhost обратного вызова стал де -факто стандартом для OAuth в инструментах CLI. Библиотеки любятoauth-callback
Предоставьте готовые к производству реализации с дополнительными функциями, такими как автоматическое обнаружение браузера, стойкость токена и поддержка PKCE.
Современный OAuth движется к еще лучшим решениям, таким как поток кода устройства для среды без головы и динамическая регистрация клиента для устранения предварительных секретов. Но на данный момент сервер обратного вызова Localhost остается наиболее широко поддерживаемым и удобным подходом для привлечения инструментов OAuth в командную строку.
Готовы реализовать OAuth в вашем инструменте CLI? Проверьте полныйOAuth-CallbackБиблиотека для проверенной в битве реализации, которая обрабатывает все случаи краев, обсуждаемые здесь.
Этот учебник является частью серии о современных шаблонах аутентификации. Следовать@koistyaДля получения дополнительной информации о создании безопасных, удобных для пользователя инструментов разработчиков.
Оригинал