LaravelDocs(中文)

Passport

Laravel Passport 為你的應用程式提供完整的 OAuth2 伺服器實作

簡介 (Introduction)

Laravel Passport 在幾分鐘內為你的 Laravel 應用程式提供完整的 OAuth2 伺服器實作。Passport 建立在由 Andy Millington 和 Simon Hamp 維護的 League OAuth2 server 之上。

[!NOTE] 本文件假設你已經熟悉 OAuth2。如果你對 OAuth2 一無所知,在繼續之前,請考慮熟悉 OAuth2 的一般 術語 和功能。

Passport 還是 Sanctum? (Passport or Sanctum?)

在開始之前,你可能希望確定你的應用程式是否更適合使用 Laravel Passport 或 Laravel Sanctum。如果你的應用程式絕對需要支援 OAuth2,那麼你應該使用 Laravel Passport。

但是,如果你試圖驗證單頁應用程式、行動應用程式或發行 API Token,你應該使用 Laravel Sanctum。Laravel Sanctum 不支援 OAuth2;但是,它提供了更簡單的 API 身份驗證開發體驗。

安裝 (Installation)

你可以透過 install:api Artisan 指令安裝 Laravel Passport:

php artisan install:api --passport

此指令將發佈並執行建立應用程式儲存 OAuth2 用戶端和存取 Token 所需資料表所需的資料庫遷移。該指令還將建立產生安全存取 Token 所需的加密金鑰。

執行 install:api 指令後,將 Laravel\Passport\HasApiTokens Trait 和 Laravel\Passport\Contracts\OAuthenticatable 介面新增到你的 App\Models\User 模型中。此 Trait 將為你的模型提供一些輔助方法,允許你檢查已驗證使用者的 Token 和 Scope:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}

最後,在你的應用程式的 config/auth.php 設定檔中,你應該定義一個 api 身份驗證 Guard 並將 driver 選項設定為 passport。這將指示你的應用程式在驗證傳入的 API 請求時使用 Passport 的 TokenGuard

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],
],

部署 Passport (Deploying Passport)

首次將 Passport 部署到應用程式的伺服器時,你可能需要執行 passport:keys 指令。此指令會產生 Passport 產生存取 Token 所需的加密金鑰。產生的金鑰通常不會保留在原始碼控制中:

php artisan passport:keys

如有必要,你可以定義 Passport 金鑰的載入路徑。你可以使用 Passport::loadKeysFrom 方法來完成此操作。通常,應從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法呼叫此方法:

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::loadKeysFrom(__DIR__.'/../secrets/oauth');
}

從環境變數載入金鑰 (Loading Keys From the Environment)

或者,你可以使用 vendor:publish Artisan 指令發佈 Passport 的設定檔:

php artisan vendor:publish --tag=passport-config

發佈設定檔後,你可以透過將應用程式的加密金鑰定義為環境變數來載入它們:

PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
<private key here>
-----END RSA PRIVATE KEY-----"

PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
<public key here>
-----END PUBLIC KEY-----"

升級 Passport (Upgrading Passport)

升級到 Passport 的新主要版本時,請務必仔細閱讀 升級指南

設定 (Configuration)

Token 生命周期 (Token Lifetimes)

預設情況下,Passport 發行長期有效的存取 Token,有效期為一年。如果你想設定更長/更短的 Token 生命周期,可以使用 tokensExpireInrefreshTokensExpireInpersonalAccessTokensExpireIn 方法。這些方法應從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫:

use Carbon\CarbonInterval;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::tokensExpireIn(CarbonInterval::days(15));
    Passport::refreshTokensExpireIn(CarbonInterval::days(30));
    Passport::personalAccessTokensExpireIn(CarbonInterval::months(6));
}

[!WARNING] Passport 資料庫表上的 expires_at 欄位是唯讀的,僅用於顯示目的。發行 Token 時,Passport 將過期資訊儲存在簽名和加密的 Token 中。如果你需要使 Token 無效,你應該 撤銷它

覆寫預設模型 (Overriding Default Models)

你可以透過定義自己的模型並繼承相應的 Passport 模型來自由擴充 Passport 內部使用的模型:

use Laravel\Passport\Client as PassportClient;

class Client extends PassportClient
{
    // ...
}

定義模型後,你可以透過 Laravel\Passport\Passport 類別指示 Passport 使用你的自訂模型。通常,你應該在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中通知 Passport 有關你的自訂模型:

use App\Models\Passport\AuthCode;
use App\Models\Passport\Client;
use App\Models\Passport\DeviceCode;
use App\Models\Passport\RefreshToken;
use App\Models\Passport\Token;
use Laravel\Passport\Passport;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::useTokenModel(Token::class);
    Passport::useRefreshTokenModel(RefreshToken::class);
    Passport::useAuthCodeModel(AuthCode::class);
    Passport::useClientModel(Client::class);
    Passport::useDeviceCodeModel(DeviceCode::class);
}

覆寫路由 (Overriding Routes)

有時你可能希望自訂 Passport 定義的路由。為此,你首先需要在應用程式的 AppServiceProviderregister 方法中新增 Passport::ignoreRoutes 來忽略 Passport 註冊的路由:

use Laravel\Passport\Passport;

/**
 * Register any application services.
 */
public function register(): void
{
    Passport::ignoreRoutes();
}

然後,你可以將 Passport 在 其路由檔案 中定義的路由複製到應用程式的 routes/web.php 檔案中,並根據你的喜好進行修改:

Route::group([
    'as' => 'passport.',
    'prefix' => config('passport.path', 'oauth'),
    'namespace' => '\Laravel\Passport\Http\Controllers',
], function () {
    // Passport routes...
});

授權碼授權 (Authorization Code Grant)

使用授權碼進行 OAuth2 是大多數開發人員熟悉的 OAuth2 方式。使用授權碼時,用戶端應用程式會將使用者重新導向到你的伺服器,使用者將在伺服器上批准或拒絕向用戶端發行存取 Token 的請求。

首先,我們需要指示 Passport 如何回傳我們的「授權」視圖。

所有授權視圖的渲染邏輯都可以使用 Laravel\Passport\Passport 類別提供的適當方法進行自訂。通常,你應該從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫此方法:

use Inertia\Inertia;
use Laravel\Passport\Passport;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    // 透過提供視圖名稱...
    Passport::authorizationView('auth.oauth.authorize');

    // 透過提供閉包...
    Passport::authorizationView(
        fn ($parameters) => Inertia::render('Auth/OAuth/Authorize', [
            'request' => $parameters['request'],
            'authToken' => $parameters['authToken'],
            'client' => $parameters['client'],
            'user' => $parameters['user'],
            'scopes' => $parameters['scopes'],
        ])
    );
}

Passport 將自動定義回傳此視圖的 /oauth/authorize 路由。你的 auth.oauth.authorize 樣板應包含一個向 passport.authorizations.approve 路由發出 POST 請求以批准授權的表單,以及一個向 passport.authorizations.deny 路由發出 DELETE 請求以拒絕授權的表單。passport.authorizations.approvepassport.authorizations.deny 路由需要 stateclient_idauth_token 欄位。

管理用戶端 (Managing Clients)

開發需要與你的應用程式 API 互動的應用程式的開發人員需要透過建立「用戶端」向你註冊他們的應用程式。通常,這包括提供其應用程式的名稱以及你的應用程式在使用者批准其授權請求後可以重新導向到的 URI。

第一方用戶端 (First-Party Clients)

建立用戶端最簡單的方法是使用 passport:client Artisan 指令。此指令可用於建立第一方用戶端或測試你的 OAuth2 功能。當你執行 passport:client 指令時,Passport 會提示你提供有關用戶端的更多資訊,並為你提供用戶端 ID 和 Secret:

php artisan passport:client

如果你想為你的用戶端允許多個重新導向 URI,你可以在 passport:client 指令提示輸入 URI 時使用逗號分隔的列表來指定它們。任何包含逗號的 URI 都應進行 URI 編碼:

https://third-party-app.com/callback,https://example.com/oauth/redirect

第三方用戶端 (Third-Party Clients)

由於你的應用程式的使用者將無法使用 passport:client 指令,你可以使用 Laravel\Passport\ClientRepository 類別的 createAuthorizationCodeGrantClient 方法為給定使用者註冊用戶端:

use App\Models\User;
use Laravel\Passport\ClientRepository;

$user = User::find($userId);

// 建立屬於給定使用者的 OAuth 應用程式用戶端...
$client = app(ClientRepository::class)->createAuthorizationCodeGrantClient(
    user: $user,
    name: 'Example App',
    redirectUris: ['https://third-party-app.com/callback'],
    confidential: false,
    enableDeviceFlow: true
);

// 檢索屬於使用者的所有 OAuth 應用程式用戶端...
$clients = $user->oauthApps()->get();

createAuthorizationCodeGrantClient 方法回傳 Laravel\Passport\Client 的實例。你可以向使用者顯示 $client->id 作為用戶端 ID,顯示 $client->plainSecret 作為用戶端 Secret。

請求 Token (Requesting Tokens)

重新導向以進行授權 (Redirecting for Authorization)

建立用戶端後,開發人員可以使用其用戶端 ID 和 Secret 向你的應用程式請求授權碼和存取 Token。首先,消費應用程式應向你的應用程式的 /oauth/authorize 路由發出重新導向請求,如下所示:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => 'user:read orders:create',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

prompt 參數可用於指定 Passport 應用程式的身份驗證行為。

如果 prompt 值為 none,如果使用者尚未透過 Passport 應用程式進行身份驗證,Passport 將始終拋出身份驗證錯誤。如果值為 consent,即使之前已授予消費應用程式所有 Scope,Passport 也將始終顯示授權批准畫面。當值為 login 時,Passport 應用程式將始終提示使用者重新登入應用程式,即使他們已經有現有的 Session。

如果未提供 prompt 值,則僅當使用者之前未授權消費應用程式存取請求的 Scope 時,才會提示使用者進行授權。

[!NOTE] 請記住,/oauth/authorize 路由已由 Passport 定義。你不需要手動定義此路由。

批准請求 (Approving the Request)

收到授權請求時,Passport 將根據 prompt 參數的值(如果存在)自動回應,並可能向使用者顯示一個樣板,允許他們批准或拒絕授權請求。如果他們批准請求,他們將被重新導向回消費應用程式指定的 redirect_uriredirect_uri 必須與建立用戶端時指定的 redirect URL 相符。

有時你可能希望跳過授權提示,例如授權第一方用戶端時。你可以透過 繼承 Client 模型 並定義 skipsAuthorization 方法來完成此操作。如果 skipsAuthorization 回傳 true,則用戶端將被批准,使用者將立即被重新導向回 redirect_uri,除非消費應用程式在重新導向以進行授權時明確設定了 prompt 參數:

<?php

namespace App\Models\Passport;

use Illuminate\Contracts\Auth\Authenticatable;
use Laravel\Passport\Client as BaseClient;

class Client extends BaseClient
{
    /**
     * Determine if the client should skip the authorization prompt.
     *
     * @param  \Laravel\Passport\Scope[]  $scopes
     */
    public function skipsAuthorization(Authenticatable $user, array $scopes): bool
    {
        return $this->firstParty();
    }
}

將授權碼轉換為存取 Token (Converting Authorization Codes to Access Tokens)

如果使用者批准授權請求,他們將被重新導向回消費應用程式。消費者應首先根據重新導向之前儲存的值驗證 state 參數。如果 state 參數相符,則消費者應向你的應用程式發出 POST 請求以請求存取 Token。該請求應包含使用者批准授權請求時你的應用程式發行的授權碼:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

Route::get('/callback', function (Request $request) {
    $state = $request->session()->pull('state');

    throw_unless(
        strlen($state) > 0 && $state === $request->state,
        InvalidArgumentException::class,
        'Invalid state value.'
    );

    $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'your-client-id',
        'client_secret' => 'your-client-secret',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'code' => $request->code,
    ]);

    return $response->json();
});

/oauth/token 路由將回傳包含 access_tokenrefresh_tokenexpires_in 屬性的 JSON 回應。expires_in 屬性包含存取 Token 過期前的秒數。

[!NOTE] 與 /oauth/authorize 路由一樣,/oauth/token 路由由 Passport 為你定義。無需手動定義此路由。

管理 Token (Managing Tokens)

你可以使用 Laravel\Passport\HasApiTokens Trait 的 tokens 方法檢索使用者已授權的 Token。例如,這可用於為你的使用者提供一個儀表板,以追蹤他們與第三方應用程式的連線:

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;

$user = User::find($userId);

// 檢索使用者的所有有效 Token...
$tokens = $user->tokens()
    ->where('revoked', false)
    ->where('expires_at', '>', Date::now())
    ->get();

// 檢索使用者與第三方 OAuth 應用程式用戶端的所有連線...
$connections = $tokens->load('client')
    ->reject(fn (Token $token) => $token->client->firstParty())
    ->groupBy('client_id')
    ->map(fn (Collection $tokens) => [
        'client' => $tokens->first()->client,
        'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(),
        'tokens_count' => $tokens->count(),
    ])
    ->values();

重新整理 Token (Refreshing Tokens)

如果你的應用程式發行短期有效的存取 Token,使用者將需要透過發行存取 Token 時提供給他們的重新整理 Token 來重新整理他們的存取 Token:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'refresh_token',
    'refresh_token' => 'the-refresh-token',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret', // 僅機密用戶端需要...
    'scope' => 'user:read orders:create',
]);

return $response->json();

/oauth/token 路由將回傳包含 access_tokenrefresh_tokenexpires_in 屬性的 JSON 回應。expires_in 屬性包含存取 Token 過期前的秒數。

撤銷 Token (Revoking Tokens)

你可以使用 Laravel\Passport\Token 模型上的 revoke 方法撤銷 Token。你可以使用 Laravel\Passport\RefreshToken 模型上的 revoke 方法撤銷 Token 的重新整理 Token:

use Laravel\Passport\Passport;
use Laravel\Passport\Token;

$token = Passport::token()->find($tokenId);

// 撤銷存取 Token...
$token->revoke();

// 撤銷 Token 的重新整理 Token...
$token->refreshToken?->revoke();

// 撤銷使用者的所有 Token...
User::find($userId)->tokens()->each(function (Token $token) {
    $token->revoke();
    $token->refreshToken?->revoke();
});

清除 Token (Purging Tokens)

當 Token 被撤銷或過期時,你可能希望將它們從資料庫中清除。Passport 包含的 passport:purge Artisan 指令可以為你執行此操作:

# 清除已撤銷和已過期的 Token、授權碼和裝置碼...
php artisan passport:purge

# 僅清除過期超過 6 小時的 Token...
php artisan passport:purge --hours=6

# 僅清除已撤銷的 Token、授權碼和裝置碼...
php artisan passport:purge --revoked

# 僅清除已過期的 Token、授權碼和裝置碼...
php artisan passport:purge --expired

你也可以在應用程式的 routes/console.php 檔案中設定 排程任務,以按排程自動修剪你的 Token:

use Illuminate\Support\Facades\Schedule;

Schedule::command('passport:purge')->hourly();

帶有 PKCE 的授權碼授權 (Authorization Code Grant With PKCE)

帶有「程式碼交換證明金鑰」(PKCE) 的授權碼授權是一種驗證單頁應用程式或行動應用程式以存取 API 的安全方法。當你無法保證用戶端 Secret 將被機密儲存時,或者為了減輕授權碼被攻擊者攔截的威脅時,應使用此授權。在將授權碼交換為存取 Token 時,「程式碼驗證器」和「程式碼挑戰」的組合將取代用戶端 Secret。

建立用戶端 (Creating the Client)

在你的應用程式可以透過帶有 PKCE 的授權碼授權發行 Token 之前,你需要建立一個啟用 PKCE 的用戶端。你可以使用帶有 --public 選項的 passport:client Artisan 指令來執行此操作:

php artisan passport:client --public

請求 Token (Requesting Tokens)

程式碼驗證器和程式碼挑戰 (Code Verifier and Code Challenge)

由於此授權不提供用戶端 Secret,因此開發人員需要產生程式碼驗證器和程式碼挑戰的組合才能請求 Token。

程式碼驗證器應為 43 到 128 個字元之間的隨機字串,包含字母、數字和 "-"".""_""~" 字元,如 RFC 7636 規範 中所定義。

程式碼挑戰應為帶有 URL 和檔案名稱安全字元的 Base64 編碼字串。應刪除尾隨的 '=' 字元,並且不應存在換行符、空格或其他其他字元。

$encoded = base64_encode(hash('sha256', $codeVerifier, true));

$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');

重新導向以進行授權 (Redirecting for Authorization)

建立用戶端後,你可以使用用戶端 ID 以及產生的程式碼驗證器和程式碼挑戰向你的應用程式請求授權碼和存取 Token。首先,消費應用程式應向你的應用程式的 /oauth/authorize 路由發出重新導向請求:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $request->session()->put(
        'code_verifier', $codeVerifier = Str::random(128)
    );

    $codeChallenge = strtr(rtrim(
        base64_encode(hash('sha256', $codeVerifier, true))
    , '='), '+/', '-_');

    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => 'user:read orders:create',
        'state' => $state,
        'code_challenge' => $codeChallenge,
        'code_challenge_method' => 'S256',
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

將授權碼轉換為存取 Token (Converting Authorization Codes to Access Tokens)

如果使用者批准授權請求,他們將被重新導向回消費應用程式。消費者應根據重新導向之前儲存的值驗證 state 參數,就像在標準授權碼授權中一樣。

如果 state 參數相符,則消費者應向你的應用程式發出 POST 請求以請求存取 Token。該請求應包含使用者批准授權請求時你的應用程式發行的授權碼以及最初產生的程式碼驗證器:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

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 = Http::asForm()->post('https://passport-app.test/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'code_verifier' => $codeVerifier,
        'code' => $request->code,
    ]);

    return $response->json();
});

裝置授權 (Device Authorization Grant)

OAuth2 裝置授權允許無瀏覽器或輸入受限的裝置(如電視和遊戲機)透過交換「裝置碼」來取得存取 Token。使用裝置流程時,裝置用戶端將指示使用者使用輔助裝置(如電腦或智慧型手機)連線到你的伺服器,在那裡他們將輸入提供的「使用者碼」,並批准或拒絕存取請求。

首先,我們需要指示 Passport 如何回傳我們的「使用者碼」和「授權」視圖。

所有授權視圖的渲染邏輯都可以使用 Laravel\Passport\Passport 類別提供的適當方法進行自訂。通常,你應該從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫此方法。

use Inertia\Inertia;
use Laravel\Passport\Passport;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    // 透過提供視圖名稱...
    Passport::deviceUserCodeView('auth.oauth.device.user-code');
    Passport::deviceAuthorizationView('auth.oauth.device.authorize');

    // 透過提供閉包...
    Passport::deviceUserCodeView(
        fn ($parameters) => Inertia::render('Auth/OAuth/Device/UserCode')
    );

    Passport::deviceAuthorizationView(
        fn ($parameters) => Inertia::render('Auth/OAuth/Device/Authorize', [
            'request' => $parameters['request'],
            'authToken' => $parameters['authToken'],
            'client' => $parameters['client'],
            'user' => $parameters['user'],
            'scopes' => $parameters['scopes'],
        ])
    );

    // ...
}

Passport 將自動定義回傳這些視圖的路由。你的 auth.oauth.device.user-code 樣板應包含一個向 passport.device.authorizations.authorize 路由發出 GET 請求的表單。passport.device.authorizations.authorize 路由需要 user_code 查詢參數。

你的 auth.oauth.device.authorize 樣板應包含一個向 passport.device.authorizations.approve 路由發出 POST 請求以批准授權的表單,以及一個向 passport.device.authorizations.deny 路由發出 DELETE 請求以拒絕授權的表單。passport.device.authorizations.approvepassport.device.authorizations.deny 路由需要 stateclient_idauth_token 欄位。

建立裝置授權用戶端 (Creating a Device Authorization Grant Client) (Creating a Device Code Grant Client)

在你的應用程式可以透過裝置授權發行 Token 之前,你需要建立一個啟用裝置流程的用戶端。你可以使用帶有 --device 選項的 passport:client Artisan 指令來執行此操作。此指令將建立一個第一方裝置流程啟用用戶端,並為你提供用戶端 ID 和 Secret:

php artisan passport:client --device

此外,你可以使用 ClientRepository 類別上的 createDeviceAuthorizationGrantClient 方法註冊屬於給定使用者的第三方用戶端:

use App\Models\User;
use Laravel\Passport\ClientRepository;

$user = User::find($userId);

$client = app(ClientRepository::class)->createDeviceAuthorizationGrantClient(
    user: $user,
    name: 'Example Device',
    confidential: false,
);

請求 Token (Requesting Tokens)

請求裝置碼 (Requesting a Device Code)

建立用戶端後,開發人員可以使用其用戶端 ID 向你的應用程式請求裝置碼。首先,消費裝置應向你的應用程式的 /oauth/device/code 路由發出 POST 請求以請求裝置碼:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/device/code', [
    'client_id' => 'your-client-id',
    'scope' => 'user:read orders:create',
]);

return $response->json();

這將回傳包含 device_codeuser_codeverification_uriintervalexpires_in 屬性的 JSON 回應。expires_in 屬性包含裝置碼過期前的秒數。interval 屬性包含消費裝置在輪詢 /oauth/token 路由以避免速率限制錯誤時應等待的秒數。

[!NOTE] 請記住,/oauth/device/code 路由已由 Passport 定義。你不需要手動定義此路由。

顯示驗證 URI 和使用者碼 (Displaying the Verification URI and User Code)

獲得裝置碼請求後,消費裝置應指示使用者使用另一台裝置並造訪提供的 verification_uri 並輸入 user_code 以批准授權請求。

輪詢 Token 請求 (Polling Token Request)

由於使用者將使用單獨的裝置來授予(或拒絕)存取權限,因此消費裝置應輪詢你的應用程式的 /oauth/token 路由以確定使用者何時回應了請求。消費裝置應使用請求裝置碼時 JSON 回應中提供的最小輪詢 interval,以避免速率限制錯誤:

use Illuminate\Support\Facades\Http;
use Illuminate\Support\Sleep;

$interval = 5;

do {
    Sleep::for($interval)->seconds();

    $response = Http::asForm()->post('https://passport-app.test/oauth/token', [
        'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
        'client_id' => 'your-client-id',
        'client_secret' => 'your-client-secret', // 僅機密用戶端需要...
        'device_code' => 'the-device-code',
    ]);

    if ($response->json('error') === 'slow_down') {
        $interval += 5;
    }
} while (in_array($response->json('error'), ['authorization_pending', 'slow_down']));

return $response->json();

如果使用者批准了授權請求,這將回傳包含 access_tokenrefresh_tokenexpires_in 屬性的 JSON 回應。expires_in 屬性包含存取 Token 過期前的秒數。

密碼授權 (Password Grant)

[!WARNING] 我們不再建議使用密碼授權 Token。相反,你應該選擇 OAuth2 Server 目前建議的授權類型

OAuth2 密碼授權允許你的其他第一方用戶端(例如行動應用程式)使用電子郵件地址/使用者名稱和密碼取得存取 Token。這允許你安全地向第一方用戶端發行存取 Token,而無需要求使用者完成整個 OAuth2 授權碼重新導向流程。

要啟用密碼授權,請在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫 enablePasswordGrant 方法:

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::enablePasswordGrant();
}

建立密碼授權用戶端 (Creating a Password Grant Client)

在你的應用程式可以透過密碼授權發行 Token 之前,你需要建立一個密碼授權用戶端。你可以使用帶有 --password 選項的 passport:client Artisan 指令來執行此操作。

php artisan passport:client --password

請求 Token (Requesting Tokens)

啟用授權並建立密碼授權用戶端後,你可以透過向 /oauth/token 路由發出帶有使用者電子郵件地址和密碼的 POST 請求來請求存取 Token。請記住,此路由已由 Passport 註冊,因此無需手動定義。如果請求成功,你將從伺服器的 JSON 回應中收到 access_tokenrefresh_token

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret', // 僅機密用戶端需要...
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => 'user:read orders:create',
]);

return $response->json();

[!NOTE] 請記住,預設情況下存取 Token 是長期有效的。但是,如果需要,你可以自由 設定最大存取 Token 生命周期

請求所有 Scope (Requesting All Scopes)

使用密碼授權或用戶端憑證授權時,你可能希望授權 Token 擁有應用程式支援的所有 Scope。你可以透過請求 * Scope 來完成此操作。如果你請求 * Scope,Token 實例上的 can 方法將始終回傳 true。此 Scope 只能指派給使用 passwordclient_credentials 授權發行的 Token:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'password',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret', // 僅機密用戶端需要...
    'username' => 'taylor@laravel.com',
    'password' => 'my-password',
    'scope' => '*',
]);

自訂使用者提供者 (Customizing the User Provider)

如果你的應用程式使用多個 身份驗證使用者提供者,你可以透過在透過 artisan passport:client --password 指令建立用戶端時提供 --provider 選項來指定密碼授權用戶端使用哪個使用者提供者。給定的提供者名稱應與應用程式的 config/auth.php 設定檔中定義的有效提供者相符。然後,你可以 使用中介軟體保護你的路由,以確保只有來自 Guard 指定提供者的使用者才能獲得授權。

自訂使用者名稱欄位 (Customizing the Username Field)

使用密碼授權進行身份驗證時,Passport 將使用你的可驗證模型的 email 屬性作為「使用者名稱」。但是,你可以透過在模型上定義 findForPassport 方法來自訂此行為:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\Bridge\Client;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * Find the user instance for the given username.
     */
    public function findForPassport(string $username, Client $client): User
    {
        return $this->where('username', $username)->first();
    }
}

自訂密碼驗證 (Customizing the Password Validation)

使用密碼授權進行身份驗證時,Passport 將使用模型的 password 屬性來驗證給定的密碼。如果你的模型沒有 password 屬性,或者你想自訂密碼驗證邏輯,可以在模型上定義 validateForPassportPasswordGrant 方法:

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Laravel\Passport\Contracts\OAuthenticatable;
use Laravel\Passport\HasApiTokens;

class User extends Authenticatable implements OAuthenticatable
{
    use HasApiTokens, Notifiable;

    /**
     * Validate the password of the user for the Passport password grant.
     */
    public function validateForPassportPasswordGrant(string $password): bool
    {
        return Hash::check($password, $this->password);
    }
}

隱式授權 (Implicit Grant)

[!WARNING] 我們不再建議使用隱式授權 Token。相反,你應該選擇 OAuth2 Server 目前建議的授權類型

隱式授權類似於授權碼授權;但是,Token 會在不交換授權碼的情況下回傳給用戶端。此授權最常用於無法安全儲存用戶端憑證的 JavaScript 或行動應用程式。要啟用授權,請在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫 enableImplicitGrant 方法:

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::enableImplicitGrant();
}

在你的應用程式可以透過隱式授權發行 Token 之前,你需要建立一個隱式授權用戶端。你可以使用帶有 --implicit 選項的 passport:client Artisan 指令來執行此操作。

php artisan passport:client --implicit

啟用授權並建立隱式用戶端後,開發人員可以使用其用戶端 ID 向你的應用程式請求存取 Token。消費應用程式應向你的應用程式的 /oauth/authorize 路由發出重新導向請求,如下所示:

use Illuminate\Http\Request;
use Illuminate\Support\Str;

Route::get('/redirect', function (Request $request) {
    $request->session()->put('state', $state = Str::random(40));

    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'token',
        'scope' => 'user:read orders:create',
        'state' => $state,
        // 'prompt' => '', // "none", "consent", or "login"
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

[!NOTE] 請記住,/oauth/authorize 路由已由 Passport 定義。你不需要手動定義此路由。

用戶端憑證授權 (Client Credentials Grant)

用戶端憑證授權適用於機器對機器的身份驗證。例如,你可以在透過 API 執行維護任務的排程任務中使用此授權。

在你的應用程式可以透過用戶端憑證授權發行 Token 之前,你需要建立一個用戶端憑證授權用戶端。你可以使用 passport:client Artisan 指令的 --client 選項來執行此操作:

php artisan passport:client --client

接下來,將 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中介軟體指派給路由:

use Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner;

Route::get('/orders', function (Request $request) {
    // 存取 Token 有效且用戶端是資源擁有者...
})->middleware(EnsureClientIsResourceOwner::class);

要將路由的存取權限限制為特定 Scope,你可以向 using 方法提供所需 Scope 的列表:

Route::get('/orders', function (Request $request) {
    // 存取 Token 有效,用戶端是資源擁有者,並且具有 "servers:read" 和 "servers:create" Scope...
})->middleware(EnsureClientIsResourceOwner::using('servers:read', 'servers:create'));

檢索 Token (Retrieving Tokens)

要使用此授權類型檢索 Token,請向 oauth/token 端點發出請求:

use Illuminate\Support\Facades\Http;

$response = Http::asForm()->post('https://passport-app.test/oauth/token', [
    'grant_type' => 'client_credentials',
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret',
    'scope' => 'servers:read servers:create',
]);

return $response->json()['access_token'];

個人存取 Token (Personal Access Tokens)

有時,你的使用者可能希望向自己發行存取 Token,而無需經過典型的授權碼重新導向流程。允許使用者透過你的應用程式的 UI 向自己發行 Token 對於允許使用者試驗你的 API 很有用,或者可以作為一般發行存取 Token 的更簡單方法。

[!NOTE] 如果你的應用程式主要使用 Passport 發行個人存取 Token,請考慮使用 Laravel Sanctum,這是 Laravel 用於發行 API 存取 Token 的輕量級第一方函式庫。

建立個人存取用戶端 (Creating a Personal Access Client)

在你的應用程式可以發行個人存取 Token 之前,你需要建立一個個人存取用戶端。你可以透過執行帶有 --personal 選項的 passport:client Artisan 指令來執行此操作。如果你已經執行了 passport:install 指令,則無需執行此指令:

php artisan passport:client --personal

自訂使用者提供者 (Customizing the User Provider)

如果你的應用程式使用多個 身份驗證使用者提供者,你可以透過在透過 artisan passport:client --personal 指令建立用戶端時提供 --provider 選項來指定個人存取授權用戶端使用哪個使用者提供者。給定的提供者名稱應與應用程式的 config/auth.php 設定檔中定義的有效提供者相符。然後,你可以 使用中介軟體保護你的路由,以確保只有來自 Guard 指定提供者的使用者才能獲得授權。

管理個人存取 Token (Managing Personal Access Tokens)

建立個人存取用戶端後,你可以使用 App\Models\User 模型實例上的 createToken 方法為給定使用者發行 Token。createToken 方法接受 Token 名稱作為其第一個參數,並接受可選的 Scope 陣列作為其第二個參數:

use App\Models\User;
use Illuminate\Support\Facades\Date;
use Laravel\Passport\Token;

$user = User::find($userId);

// 建立沒有 Scope 的 Token...
$token = $user->createToken('My Token')->accessToken;

// 建立帶有 Scope 的 Token...
$token = $user->createToken('My Token', ['user:read', 'orders:create'])->accessToken;

// 建立帶有所有 Scope 的 Token...
$token = $user->createToken('My Token', ['*'])->accessToken;

// 檢索屬於使用者的所有有效個人存取 Token...
$tokens = $user->tokens()
    ->with('client')
    ->where('revoked', false)
    ->where('expires_at', '>', Date::now())
    ->get()
    ->filter(fn (Token $token) => $token->client->hasGrantType('personal_access'));

保護路由 (Protecting Routes)

透過中介軟體 (Via Middleware)

Passport 包含一個 身份驗證 Guard,用於驗證傳入請求的存取 Token。將 api Guard 設定為使用 passport 驅動程式後,你只需要在任何需要有效存取 Token 的路由上指定 auth:api 中介軟體:

Route::get('/user', function () {
    // 只有 API 驗證的使用者才能存取此路由...
})->middleware('auth:api');

[!WARNING] 如果你使用 用戶端憑證授權,你應該使用 Laravel\Passport\Http\Middleware\EnsureClientIsResourceOwner 中介軟體 來保護你的路由,而不是 auth:api 中介軟體。

多個身份驗證 Guard (Multiple Authentication Guards)

如果你的應用程式驗證可能使用完全不同的 Eloquent 模型的不同類型的使用者,你可能需要為應用程式中的每種使用者提供者類型定義 Guard 設定。這允許你保護針對特定使用者提供者的請求。例如,給定 config/auth.php 設定檔中的以下 Guard 設定:

'guards' => [
    'api' => [
        'driver' => 'passport',
        'provider' => 'users',
    ],

    'api-customers' => [
        'driver' => 'passport',
        'provider' => 'customers',
    ],
],

以下路由將使用 api-customers Guard(使用 customers 使用者提供者)來驗證傳入請求:

Route::get('/customer', function () {
    // ...
})->middleware('auth:api-customers');

[!NOTE] 有關在 Passport 中使用多個使用者提供者的更多資訊,請參閱 個人存取 Token 文件密碼授權文件

傳遞存取 Token (Passing the Access Token)

呼叫受 Passport 保護的路由時,你的應用程式的 API 消費者應在其請求的 Authorization 標頭中將其存取 Token 指定為 Bearer Token。例如,使用 Http Facade 時:

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'Accept' => 'application/json',
    'Authorization' => "Bearer $accessToken",
])->get('https://passport-app.test/api/user');

return $response->json();

Token Scope (Token Scopes)

Scope 允許你的 API 用戶端在請求存取帳戶的授權時請求一組特定的權限。例如,如果你正在建立一個電子商務應用程式,並非所有 API 消費者都需要下訂單的能力。相反,你可以允許消費者僅請求存取訂單發貨狀態的授權。換句話說,Scope 允許你的應用程式的使用者限制第三方應用程式可以代表他們執行的操作。

定義 Scope (Defining Scopes)

你可以在應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中使用 Passport::tokensCan 方法定義 API 的 Scope。tokensCan 方法接受 Scope 名稱和 Scope 描述的陣列。Scope 描述可以是任何你想要的內容,並將顯示在授權批准畫面上給使用者:

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::tokensCan([
        'user:read' => 'Retrieve the user info',
        'orders:create' => 'Place orders',
        'orders:read:status' => 'Check order status',
    ]);
}

預設 Scope (Default Scope)

如果用戶端未請求任何特定 Scope,你可以設定 Passport 伺服器使用 defaultScopes 方法將預設 Scope 附加到 Token。通常,你應該從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫此方法:

use Laravel\Passport\Passport;

Passport::tokensCan([
    'user:read' => 'Retrieve the user info',
    'orders:create' => 'Place orders',
    'orders:read:status' => 'Check order status',
]);

Passport::defaultScopes([
    'user:read',
    'orders:create',
]);

將 Scope 指派給 Token (Assigning Scopes to Tokens)

請求授權碼時 (When Requesting Authorization Codes)

使用授權碼授權請求存取 Token 時,消費者應將其所需的 Scope 指定為 scope 查詢字串參數。scope 參數應為以空格分隔的 Scope 列表:

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'your-client-id',
        'redirect_uri' => 'https://third-party-app.com/callback',
        'response_type' => 'code',
        'scope' => 'user:read orders:create',
    ]);

    return redirect('https://passport-app.test/oauth/authorize?'.$query);
});

發行個人存取 Token 時 (When Issuing Personal Access Tokens)

如果你使用 App\Models\User 模型的 createToken 方法發行個人存取 Token,你可以將所需 Scope 的陣列作為第二個參數傳遞給該方法:

$token = $user->createToken('My Token', ['orders:create'])->accessToken;

檢查 Scope (Checking Scopes)

Passport 包含兩個中介軟體,可用於驗證傳入請求是否已使用授予給定 Scope 的 Token 進行身份驗證。

檢查所有 Scope (Check For All Scopes)

可以將 Laravel\Passport\Http\Middleware\CheckToken 中介軟體指派給路由,以驗證傳入請求的存取 Token 是否具有所有列出的 Scope:

use Laravel\Passport\Http\Middleware\CheckToken;

Route::get('/orders', function () {
    // 存取 Token 同時具有 "orders:read" 和 "orders:create" Scope...
})->middleware(['auth:api', CheckToken::using('orders:read', 'orders:create')]);

檢查任何 Scope (Check for Any Scopes)

可以將 Laravel\Passport\Http\Middleware\CheckTokenForAnyScope 中介軟體指派給路由,以驗證傳入請求的存取 Token 是否具有列出的 至少一個 Scope:

use Laravel\Passport\Http\Middleware\CheckTokenForAnyScope;

Route::get('/orders', function () {
    // 存取 Token 具有 "orders:read" 或 "orders:create" Scope...
})->middleware(['auth:api', CheckTokenForAnyScope::using('orders:read', 'orders:create')]);

檢查 Token 實例上的 Scope (Checking Scopes on a Token Instance)

一旦存取 Token 驗證的請求進入你的應用程式,你仍然可以使用已驗證 App\Models\User 實例上的 tokenCan 方法檢查 Token 是否具有給定 Scope:

use Illuminate\Http\Request;

Route::get('/orders', function (Request $request) {
    if ($request->user()->tokenCan('orders:create')) {
        // ...
    }
});

其他 Scope 方法 (Additional Scope Methods)

scopeIds 方法將回傳所有定義的 ID / 名稱的陣列:

use Laravel\Passport\Passport;

Passport::scopeIds();

scopes 方法將回傳所有定義的 Scope 作為 Laravel\Passport\Scope 實例的陣列:

Passport::scopes();

scopesFor 方法將回傳與給定 ID / 名稱相符的 Laravel\Passport\Scope 實例的陣列:

Passport::scopesFor(['user:read', 'orders:create']);

你可以使用 hasScope 方法確定是否已定義給定 Scope:

Passport::hasScope('orders:create');

SPA 身份驗證 (SPA Authentication)

在建立 API 時,能夠從你的 JavaScript 應用程式使用你自己的 API 非常有用。這種 API 開發方法允許你自己的應用程式使用與你與世界分享的相同的 API。你的 Web 應用程式、行動應用程式、第三方應用程式以及你可能在各種套件管理器上發布的任何 SDK 都可以使用相同的 API。

通常,如果你想從你的 JavaScript 應用程式使用你的 API,你需要手動將存取 Token 發送到應用程式,並將其隨每個請求傳遞給你的應用程式。但是,Passport 包含一個可以為你處理此問題的中介軟體。你只需要將 CreateFreshApiToken 中介軟體附加到你的應用程式的 bootstrap/app.php 檔案中的 web 中介軟體群組:

use Laravel\Passport\Http\Middleware\CreateFreshApiToken;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [
        CreateFreshApiToken::class,
    ]);
})

[!WARNING] 你應該確保 CreateFreshApiToken 中介軟體是你中介軟體堆疊中列出的最後一個中介軟體。

此中介軟體將附加一個 laravel_token cookie 到你的傳出回應。此 cookie 包含一個加密的 JWT,Passport 將使用它來驗證來自你的 JavaScript 應用程式的 API 請求。JWT 的生命週期等於你的 session.lifetime 設定值。現在,由於瀏覽器將自動隨所有後續請求發送 cookie,你可以向你的應用程式的 API 發出請求,而無需明確傳遞存取 Token:

axios.get("/api/user").then((response) => {
  console.log(response.data);
});

如果需要,你可以使用 Passport::cookie 方法自訂 laravel_token cookie 的名稱。通常,你應該從應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中呼叫此方法:

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Passport::cookie('custom_name');
}

CSRF 保護 (CSRF Protection)

使用此身份驗證方法時,你需要確保請求中包含有效的 CSRF Token 標頭。骨架應用程式和所有入門套件中包含的預設 Laravel JavaScript 鷹架包含一個 Axios 實例,該實例將自動使用加密的 XSRF-TOKEN cookie 值在同源請求上發送 X-XSRF-TOKEN 標頭。

[!NOTE] 如果你選擇發送 X-CSRF-TOKEN 標頭而不是 X-XSRF-TOKEN,你需要使用 csrf_token() 提供的未加密 Token。

事件 (Events)

Passport 會在發行存取 Token 和重新整理 Token 時觸發事件。你可以使用這些事件來修剪資料庫中的其他已撤銷 Token。你可以將監聽器附加到應用程式的 App\Providers\AppServiceProvider 類別的 boot 方法中的這些事件:

use Laravel\Passport\Events\AccessTokenCreated;
use Laravel\Passport\Events\RefreshTokenCreated;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Event::listen(
        AccessTokenCreated::class,
        [RevokeOldTokens::class, 'handle']
    );

    Event::listen(
        RefreshTokenCreated::class,
        [PruneOldTokens::class, 'handle']
    );
}

測試 (Testing)

Passport 的 actingAs 方法可用於將目前經過身份驗證的使用者以及其 Scope 指定為給定使用者。提供給 actingAs 方法的第一個參數是使用者實例,第二個參數是應授予使用者 Token 的 Scope 陣列:

use App\Models\User;
use Laravel\Passport\Passport;

test('orders can be created', function () {
    Passport::actingAs(
        User::factory()->create(),
        ['orders:create']
    );

    $response = $this->post('/api/orders');

    $response->assertStatus(201);
});
use App\Models\User;
use Laravel\Passport\Passport;

public function test_orders_can_be_created(): void
{
    Passport::actingAs(
        User::factory()->create(),
        ['create-orders']
    );

    $response = $this->post('/api/orders');

    $response->assertStatus(201);
}

作為用戶端 (Acting as Client)

Passport 的 actingAsClient 方法可用於將目前經過身份驗證的用戶端以及其 Scope 指定為給定用戶端。提供給 actingAsClient 方法的第一個參數是用戶端實例,第二個參數是應授予用戶端 Token 的 Scope 陣列:

use Laravel\Passport\Client;
use Laravel\Passport\Passport;

test('servers can be retrieved', function () {
    Passport::actingAsClient(
        Client::factory()->create(),
        ['servers:read']
    );

    $response = $this->get('/api/servers');

    $response->assertStatus(200);
});
use Laravel\Passport\Client;
use Laravel\Passport\Passport;

public function test_servers_can_be_retrieved(): void
{
    Passport::actingAsClient(
        Client::factory()->create(),
        ['servers:read']
    );

    $response = $this->get('/api/servers');

    $response->assertStatus(200);
}