Application Security

ایمن‌سازی وب مدرن: پیاده‌سازی OAuth2 PKCE برای برنامه‌های تک‌صفحه‌ای و کلاینت‌های موبایل

برای سال‌ها، جریان کد مجوز (Authorization Code Flow) استاندارد طلایی برای ایمن‌سازی برنامه‌های وب بود. با این حال، برنامه‌های تک‌صفحه‌ای (SPA) و کلاینت‌های موبایل بومی چالشی منحصر‌به‌فرد ایجاد کردند: آن‌ها «کلاینت‌های عمومی» هستند. برخلاف برنامه‌های سمت سرور سنتی، این کلاینت‌ها نمی‌توانند یک راز کلاینت (Client Secret) را به طور ایمن ذخیره کنند. این آسیب‌پذیری راه را برای بازیگران مخرب باز کرد تا کدهای مجوز را دستکاری کرده و خود را به جای کاربران قانونی جا بزنند؛ تهدیدی که به عنوان دستکاری کد مجوز شناخته می‌شود.

پاسخ صنعت، معرفی PKCE (کلید اثبات برای تبادل کد) بود که در RFC 7636 تعریف شده است. PKCE لایه‌ای از امنیت را بدون نیاز به راز کلاینت اضافه می‌کند و آن را به استاندارد اجباری برای OAuth2 در کلاینت‌های عمومی تبدیل کرده است. این مقاله توسعه‌دهندگان متوسط تا پیشرفته را در مکانیک‌های PKCE و نحوه پیاده‌سازی امن آن راهنمایی خواهد کرد.

چرا PKCE برای کلاینت‌های عمومی غیرقابل مذاکره است

قبل از ورود به کدنویسی، درک مدل تهدید حیاتی است. در یک جریان کد مجوز استاندارد بدون PKCE، مراحل به این صورت است:

  1. کلاینت کاربر را به سرور مجوز (Authorization Server) هدایت می‌کند.
  2. کاربر احراز هویت می‌کند و اجازه دسترسی می‌دهد.
  3. سرور مجوز با یک کد مجوز به کلاینت باز می‌گردد.
  4. کلاینت کد را با استفاده از شناسه کلاینت و راز آن، برای دریافت توکن دسترسی مبادله می‌کند.

در یک برنامه SPA یا اپلیکیشن موبایل، بک‌اندی برای نگهداری راز وجود ندارد. اگر مهاجم بتواند کد مجوز را دستکاری کند (از طریق XSS، بدافزار یا شنود شبکه)، می‌تواند از آن کد برای درخواست توکن دسترسی استفاده کند و عملاً جلسه کاربر را ربوده است. PKCE این مشکل را با پیوند دادن کد مجوز به یک کلید رمزنگاری حل می‌کند که تنها کلاینت در زمان درخواست اولیه از آن آگاه است.

توضیح جریان کاری PKCE

جریان کاری PKCE دو جزء کلیدی معرفی می‌کند: code_verifier و code_challenge.

  • Code Verifier: یک رشته تصادفی رمزنگاری با آنتروپی بالا که توسط کلاینت تولید می‌شود.
  • Code Challenge: نسخه تبدیل‌شده‌ی verifier که در درخواست ورود اولیه به سرور مجوز ارسال می‌شود.

هنگامی که کلاینت بعداً کد را برای دریافت توکن مبادله می‌کند، code_verifier اصلی را ارسال می‌کند. سرور مجوز این verifier را تبدیل کرده و آن را با code_challengeای که قبلاً دریافت کرده مقایسه می‌کند. اگر با هم مطابقت داشته باشند، توکن صادر می‌شود.

پیاده‌سازی: تولید کد Verifier و Challenge

امن‌ترین روش برای تولید challenge استفاده از تبدیل S256 است. این روش شامل گرفتن هش SHA-256 از code_verifier و کدگذاری Base64 ایمن برای URL نتیجه است.

در اینجا یک پیاده‌سازی عملی جاوااسکریپت برای تولید این مقادیر در محیط مرورگر آورده شده است:

function generateRandomString(length) {
  const array = new Uint32Array(length / 2);
  window.crypto.getRandomValues(array);
  return Array.from(array, dec => ('0' + dec.toString(16)).slice(-2)).join('');
}

async function createVerifier() {
  // Generate a random string between 43 and 128 characters
  const verifier = generateRandomString(32);
  return verifier;
}

async function createChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await window.crypto.subtle.digest('SHA-256', data);
  
  // Convert to Base64 URL Encoded string
  const base64encoded = btoa(String.fromCharCode(...new Uint8Array(hash)));
  return base64encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

اجرای درخواست مجوز

پس از دریافت code_verifier و code_challenge، باید کاربر را به سرور مجوز هدایت کنید. افزودن حیاتی در اینجا پارامترهای code_challenge و code_challenge_method=S256 است.

const params = new URLSearchParams({
  client_id: 'YOUR_CLIENT_ID',
  redirect_uri: 'http://localhost:3000/callback',
  response_type: 'code',
  scope: 'openid profile email',
  code_challenge: codeChallenge,
  code_challenge_method: 'S256'
});

window.location.href = `https://auth-server.com/authorize?${params}`;

مدیریت تبادل توکن

پس از اینکه کاربر دسترسی را اعطا کرد، به redirect_uri شما با یک کد مجوز در رشته پرس‌وجوی URL هدایت می‌شود. مرحله نهایی مبادله این کد برای دریافت توکن دسترسی است. نکته حیاتی این است که باید code_verifierای که در ابتدای جریان تولید کرده بودید را ارسال کنید.

const tokenResponse = await fetch('https://auth-server.com/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type: 'authorization_code',
    code: authorizationCode, // From URL query params
    redirect_uri: 'http://localhost:3000/callback',
    client_id: 'YOUR_CLIENT_ID',
    code_verifier: codeVerifier // The original random string
  })
});

const tokens = await tokenResponse.json();

نتیجه‌گیری

پیاده‌سازی PKCE دیگر برای توسعه‌دهندگانی که برنامه‌های SPA یا اپلیکیشن‌های موبایل می‌سازند، اختیاری نیست. با افزودن چند خط منطق رمزنگاری، از کاربران خود در برابر حملات دستکاری کد مجوز محافظت می‌کنید. اگرچه راه‌اندازی اولیه نیاز به مدیریت دقیق وضعیت و ذخیره‌سازی امن code_verifier دارد، اما منافع بلندمدت آن برای امنیت برنامه قابل توجه است. با حرکت اکوسیستم به سمت استانداردهای امنیتی سخت‌گیرانه‌تر، اتخاذ PKCE تضمین می‌کند که برنامه‌های شما مقاوم، مطابق با استانداردها و مورد اعتماد کاربران باقی بمانند.

Share: