همانطور که در فصل های قبل OAuth گفته شد، Authorization Code Grant به همراه افزونه PKCE، روشی مطمئن برای تایید اعتبار برنامههای تک صفحه (single page applications) یا برنامههای بومی (native applications) برای دسترسی به API شما است. در حقیقت این flow هنگامی استفاده میشود که یک برنامه JavaScript داریم که توانایی مخفی نگه داشتن client secret را ندارد. این برنامه در این دوره ی آموزشی همان کلاینت ما با نام consumer میباشد.
این grant در دو حالت باید مورد استفاده قرار گیرد:
1- زمانی که شما نمیتوانید تضمین کنید که رمز کلاینت (client secret) به صورت محرمانه ذخیره میشود.
2- زمانی که قصد کاهش خطراتی را دارید که Authorization Code را تهدید میکنند.
در این روش، هنگام تبادل Access Token با Authorization Code ترکیبی از code verifier و code challenge جایگزین client secret میشوند.
ایجاد یک کلاینت PKCE
قبل از این که برنامه شما بتواند توکنها را از طریق authorization code همراه با افزونه PKCE صادر کند، باید یک کلاینت با قابلیت PKCE ایجاد کنید. این کار با استفاده از دستور زیر انجام میشود. لازم به ذکر است که این کار از طریق front-end برنامه نیز قابل انجام است و تنها برای راحتی خواننده و تست پکیج از این دستور استفاده میکنیم. به مسیر روت پروژه ToDo رفته و دستور زیر را وارد میکنیم:
php artisan passport:client --public
بعد از اجرای دستور بالا تعدادی سؤال از شما پرسیده میشود و بر اساس پاسخ شما، کلاینت با قابلیت PKCE را ایجاد میکند. شکل زیر سؤالات و جوابهایی را که ما به آن دادهایم نشان میدهد:
همانطور که مشخص است، User ID، نام کلاینت و مسیر redirect بعد از درخواست authorization را از ما میپرسد و بعد از ایجاد کلاینت، Client ID و Client Secret را نشان داده است اما Client secret بدون مقدار میباشد. زیرا همانطور که قبلا ذکر شد در این درخواست، code challenge و code verifier جایگزین Client secret میشوند. اگر به دیتابیس مراجعه کنیم میتوانیم کلاینت ایجاد شده را در جدول oauth_clients ببینیم.
کار ما بر روی سرور OAuth تمام شد. فقط باید Client ID و آدرس redirect را فراموش نکنیم چون در بخش بعد به آنها نیاز داریم.
درخواست Access Token
به برنامه کلاینت یا همان consumer برمیگردیم. ابتدا باید برنامه کلاینت خود را آماده کنیم. ما نیاز داریم یک برنامه SPA داشته باشیم پس به مسیر روت برنامه consumer رفته و دستور زیر را جهت نصب پکیج laravel/ui وارد میکنیم. لاراول امکان استفاده از Bootstrap و Vue را با استفاده از این پکیج برای ما فراهم کرده است.
composer require laravel/ui
سپس هر کدام از پکیجهای زیر مجموعه آن مانند React، Bootstrap و یا Vue را میتوانیم نصب کنیم. ما برای استفاده از Vue دستور زیر را وارد میکنیم:
php artisan ui vue
در انتها دستورات زیر را به ترتیب اجرا میکنیم:
npm install
npm run dev
به مسیر resources/js/components/ رفته و یک فایل با نام index.vue را ایجاد میکنیم. در بخش template آن، کدهای زیر را مینویسیم:
<template>
<div>
<div class="col-md-9" align="center">
<div class="panel-heading">
<div>
<label>Please login through your passport account</label>
</div>
<div>
<button type="button" @click="login" class="btn btn-primary">Login with passport account</button>
</div>
</div>
</div>
</div>
</template>
// …
در بخش script برنامه کلاینت نیز کدهای زیر را مینویسیم:
// …
<script>
export default {
data() {
return {
//
}
},
methods: {
login() {
const url = 'http://localhost:8001/';
const headers = {
'Access-Control-Allow-Origin': '*',
Accept: '*/*',
'Content-Type': 'application/json'
};
axios({
json: true,
method: 'GET',
url: url,
headers: headers,
})
.then(function (response) {
console.log(response);
})
.catch(function (response) {
//handle error
console.log(response);
});
}
}
}
</script>
تا به این جا کامپوننت جدید خود را ایجاد کردیم. فایل resources/js/app.js/ را باز کرده تا کامپوننت خود
را ثبت نماییم. تکه کد زیر را در این فایل مینویسیم:
// …
Vue.component('index', require('./components/index.vue').default);
// …
حال با استفاده از تگ <index></index> میتوانیم از کامپوننت استفاده کنیم. برای این کار فایل resources/views/welcome.blade.php/ را باز کرده و کد زیر را در آن قرار میدهیم:
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Consumer') }}</title>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Consumer') }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div>
<index></index>
</div>
</div>
</body>
</html>
سپس دستورات زیر را به ترتیب در ترمینال وارد میکنیم:
npm run dev
php artisan serve --port=8001
حال آدرس localhost:8001 را در مرورگر باز میکنیم. صفحهای مشابه شکل زیر را خواهیم دید:
در انتها باید مسیرها را بنویسیم. در نوشتن مسیرها نیاز است از مواردی استفاده کنیم که ابتدا لازم است آنها را توضیح دهیم. همانطور که در بخش قبل بیان شد authorization grant، رمز Client secret را ایجاد نمیکند، به همین منظور توسعهدهندگان برای درخواست یک توکن نیاز به ایجاد ترکیبی از code verifier و code challenge دارند.
همانطور که در فصل قبل و در بخش مربوط به PKCE مطرح کردیم :
· Code verifier: باید یک رشته تصادفی بین 43 تا 128 کاراکتر باشد که حاوی حروف، اعداد و "-" ، "." ، "_" ، "~" باشد.
· Code challenge: باید یک رشته رمزگذاری شده Base64 با کاراکترهایی که در نام فایلها و urlها مجازند، باشد. کاراکتر "=" که در انتهای رشته وجود دارد باید حذف شود و هیچ شکافی (line break)، فضای خالی (whitespace) یا سایر کاراکترهای اضافی موجود نباشند.
تکه کد زیر نحوه به دست آمدن Code challenge را نشان میدهد:
$encoded = base64_encode(hash('sha256', $code_verifier, true));
$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');
اکنون باید مسیرهای خود را آماده کنیم. فایل routes/web.php/ را باز کرده و مسیرهای زیر را در آن قرار میدهیم:
Route::get('/', function () {
return view('welcome');
});
Route::get('/', function (Request $request) {
$request->session()->put('state', $state = Str::random(40));
$request->session()->put('code_verifier', $code_verifier = Str::random(128));
$codeChallenge = strtr(rtrim(
base64_encode(hash('sha256', $code_verifier, true))
, '='), '+/', '-_');
$query = http_build_query([
'client_id' => 10,
'redirect_uri' => 'http://localhost:8001/callback',
'response_type' => 'code',
'scope' => '',
'state' => $state,
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
]);
return redirect('http://localhost:8000/oauth/authorize?'.$query);
});
Route::get('/callback', function (Request $request) {
$state = $request->session()->pull('state');
$codeVerifier = $request->session()->pull('code_verifier');
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class
);
$response = (new GuzzleHttp\Client)->post('http://localhost:8000/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => 10,
'redirect_uri' => 'http://localhost:8001/callback',
'code_verifier' => $codeVerifier,
'code' => $request->code,
],
]);
return json_decode((string) $response->getBody(), true);
});
دقت داشته باشید که مسیر ها و client_id را باید با مقادیر خودتان جایگزین نمایید. اگر بر روی لوکال تست میکنید، توجه داشته باشید که در صورت استفاده از localhost، حتما مسیر را همین قرار دهید و بررسی نمایید که مقدار ذخیره شده در دیتابیس با مقدار آن در مسیر ها، یکی باشد.
بسیار خوب حالا نوبت تست میباشد. بر روی کلید Login with passport account
کلیک میکنیم.
ابتدا به آدرس http://localhost:8000/login
از سرور oauth هدایت شده و لازم است نام کاربری و پسورد را وارد کنیم. بعد از آن به آدرس redirect که به صورت زیر خواهد بود، منتقل شده و شکلی مشابه شکل زیر را مشاهده خواهیم کرد:
همانطور که مشاهده میشود نام کلاینت ما (pkce) که قبلا ایجاد کردیم در درخواست مشخص شده است.
با کلیک بر روی کلید Authorize به مسیر زیر هدایت شده و میتوانیم access token و refresh token به همراه زمان expire را مشاهده کنیم :
http://localhost:8001/callback?code=def50200cfaa6b3300a1970a151fb223bfa44d39cb449d53364b760ed3c029ed4f6d31b41e31ff24f4727dc3c671199bfd18103607df317033f7068258e60fde83eefba96c7fd25c30e276181420ab4e95860553cad67216e0ef4514f6b33a497e62677c01a72314ffaaa7a5f470c19d5ef023f605f5e0d6cbd157573f6e25d6f0af22a0e812b292f63e1c8be76d9677dbb8415096d9f2425d1369657848a60c9f3858ccb1dc53e811f0840c801cc5120770d055b1487c3b8052c80febbe17b6c4121ec5f947e7fc65cf7b1f62cb7cdb923112c6b19c7da6e58150e94b38789a840a106080e0c8a0a1dd5a8841256b2f1005ebfe6fe795814953ade9066e67ccf37b150d5d124fc8fe07dfb379f52d7b9e1589daf0a39655cf0cfb82204e345db9d5205acda5a1adb230e44d3e8e9d828c49aa325d58771bdcf7d66b123a990210afb6dc3d65901e62393edfe6466922df0db92d8992f3da2b16d2de67dc31cc507d669f5b445db8ad6266787e82e653cb4de50728520c3ff9b7aed11c294379a30261f6104d88c402b89e5885e7d1aa5956f2b0096f&state=6eEcphFIt5V3hiE3esvQpUcKd8DpKL1q9LCTvLeK
در شکل زیر پاسخ JSON برای درخواست کلاینت PKCE را نشان میدهد:
تا به این جای کار قصد ما این بود که خروجی access_token و refresh_token را به شما نشان دهیم. حال میخواهیم با استفاده از آنها به اطلاعات کاربر دسترسی داشته باشیم.
برای این کار فایل routes/web.php/ را باز کرده و مسیر callback را به صورت زیر اصلاح میکنیم:
// …
Route::get('/callback', function (Request $request) {
$state = $request->session()->pull('state');
$codeVerifier = $request->session()->pull('code_verifier');
throw_unless(
strlen($state) > 0 && $state === $request->state,
InvalidArgumentException::class
);
$response = (new GuzzleHttp\Client)->post('http://localhost:8000/oauth/token', [
'form_params' => [
'grant_type' => 'authorization_code',
'client_id' => 10,
'redirect_uri' => 'http://localhost:8001/callback',
'code_verifier' => $codeVerifier,
'code' => $request->code,
],
]);
session()->put('token', json_decode((string)$response->getBody(), true));
return redirect('/todos');
});
// …
سپس در همان فایل web.php
یک مسیر دیگر برای نمایش اطلاعات کاربر به این فایل اضافه میکنیم:
// …
Route::get('/todos', function () {
if (!session()->has('token')) {
return redirect('/');
}
$response = (new GuzzleHttp\Client)->get('http://localhost:8000/api/todos', [
'headers' => [
'Authorization' => 'Bearer ' . Session::get('token.access_token')
]
]);
return json_decode((string)$response->getBody(), true);
});
// …
مجددا تست را انجام میدهیم و این بار در خروجی، اطلاعات کاربر را مشاهده خواهیم کرد:
روش PKCE روشی معروف در OAuth میباشد که در این بخش سعی کردیم آن را با کمک مثال کاربردی که در این دوره معرفی کرده بودیم، آموزش دهیم. Laravel Passport این قابلیت را به راحتی در اختیار ما قرار داد تا بتوانیم یک کلاینت از این نوع ساخته و در Authentication Server آن را پشتیبانی کنیم. در بخشهای بعدی به روش های دیگر مجوز گرفتن به کمک Passport میپردازیم.