Laravel Passport——OAuth2 API 認證系統原始碼解析(下)

leoyang發表於2018-01-10

本文 GitBook 地址: https://www.gitbook.com/book/leoyang90/lar...

隱式授權

隱式授權類似於授權碼授權,但是它只令牌將返回給客戶端而不交換授權碼。這種授權最常用於無法安全儲存客戶端憑據的 JavaScript 或移動應用程式。透過呼叫 AuthServiceProvider 中的 enableImplicitGrant 方法來啟用這種授權:

public function boot()
{
    $this->registerPolicies();

    Passport::routes();

    Passport::enableImplicitGrant();
}

呼叫上面方法開啟授權後,開發者可以使用他們的客戶端 ID 從應用程式請求訪問令牌。接入的應用程式應該向你的應用程式的 /oauth/authorize 路由發出重定向請求,如下所示:

Route::get('/redirect', function () {
    $query = http_build_query([
        'client_id' => 'client-id',
        'redirect_uri' => 'http://example.com/callback',
        'response_type' => 'token',
        'scope' => '',
    ]);

    return redirect('http://your-app.com/oauth/authorize?'.$query);
});

首先仍然是驗證授權請求的合法性,其流程與授權碼模式基本一致:

public function validateAuthorizationRequest(ServerRequestInterface $request)
{
    $clientId = $this->getQueryStringParameter(
        'client_id',
        $request,
        $this->getServerParameter('PHP_AUTH_USER', $request)
    );
    if (is_null($clientId)) {
        throw OAuthServerException::invalidRequest('client_id');
    }

    $client = $this->clientRepository->getClientEntity(
        $clientId,
        $this->getIdentifier(),
        null,
        false
    );

    if ($client instanceof ClientEntityInterface === false) {
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
        throw OAuthServerException::invalidClient();
    }

    $redirectUri = $this->getQueryStringParameter('redirect_uri', $request);

    $scopes = $this->validateScopes(
        $this->getQueryStringParameter('scope', $request, $this->defaultScope),
        is_array($client->getRedirectUri())
            ? $client->getRedirectUri()[0]
            : $client->getRedirectUri()
    );

    // Finalize the requested scopes
    $finalizedScopes = $this->scopeRepository->finalizeScopes(
        $scopes,
        $this->getIdentifier(),
        $client
    );

    $stateParameter = $this->getQueryStringParameter('state', $request);

    $authorizationRequest = new AuthorizationRequest();
    $authorizationRequest->setGrantTypeId($this->getIdentifier());
    $authorizationRequest->setClient($client);
    $authorizationRequest->setRedirectUri($redirectUri);
    $authorizationRequest->setState($stateParameter);
    $authorizationRequest->setScopes($finalizedScopes);

    return $authorizationRequest;
}

接著,當使用者同意授權之後,就要直接返回 access_tokenLeague OAuth2 直接將令牌放入 JWT 中傳送回第三方客戶端,值得注意的是依據 OAuth2 標準,引數都是以 location hash 的形式返回的,間隔符是 #,而不是 ?:

public function __construct(\DateInterval $accessTokenTTL, $queryDelimiter = '#')
{
    $this->accessTokenTTL = $accessTokenTTL;
    $this->queryDelimiter = $queryDelimiter;
}

public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
    if ($authorizationRequest->getUser() instanceof UserEntityInterface === false) {
        throw new \LogicException('An instance of UserEntityInterface should be set on the AuthorizationRequest');
    }

    $finalRedirectUri = ($authorizationRequest->getRedirectUri() === null)
        ? is_array($authorizationRequest->getClient()->getRedirectUri())
            ? $authorizationRequest->getClient()->getRedirectUri()[0]
            : $authorizationRequest->getClient()->getRedirectUri()
        : $authorizationRequest->getRedirectUri();

    // The user approved the client, redirect them back with an access token
    if ($authorizationRequest->isAuthorizationApproved() === true) {
        $accessToken = $this->issueAccessToken(
            $this->accessTokenTTL,
            $authorizationRequest->getClient(),
            $authorizationRequest->getUser()->getIdentifier(),
            $authorizationRequest->getScopes()
        );

        $response = new RedirectResponse();
        $response->setRedirectUri(
            $this->makeRedirectUri(
                $finalRedirectUri,
                [
                    'access_token' => (string) $accessToken->convertToJWT($this->privateKey),
                    'token_type'   => 'Bearer',
                    'expires_in'   => $accessToken->getExpiryDateTime()->getTimestamp() - (new \DateTime())->getTimestamp(),
                    'state'        => $authorizationRequest->getState(),
                ],
                $this->queryDelimiter
            )
        );

        return $response;
    }

    // The user denied the client, redirect them back with an error
    throw OAuthServerException::accessDenied(
        'The user denied the request',
        $this->makeRedirectUri(
            $finalRedirectUri,
            [
                'state' => $authorizationRequest->getState(),
            ]
        )
    );
}

這個用於構建 jwt 的私鑰就是 oauth-private.key,我們知道,jwt 一般有三個部分組成:headerclaimsign, 用於 oauth2jwtclaim 主要構成有:

  • aud 客戶端 id
  • jti access_token 隨機碼
  • iat 生成時間
  • nbf 拒絕接受 jwt 時間
  • exp access_token 失效時間
  • sub 使用者 id

具體可以參考 : JSON Web Token (JWT) draft-ietf-oauth-json-web-token-32

public function convertToJWT(CryptKey $privateKey)
{
    return (new Builder())
        ->setAudience($this->getClient()->getIdentifier())
        ->setId($this->getIdentifier(), true)
        ->setIssuedAt(time())
        ->setNotBefore(time())
        ->setExpiration($this->getExpiryDateTime()->getTimestamp())
        ->setSubject($this->getUserIdentifier())
        ->set('scopes', $this->getScopes())
        ->sign(new Sha256(), new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase()))
        ->getToken();
}

public function __construct(
    Encoder $encoder = null,
    ClaimFactory $claimFactory = null
) {
    $this->encoder = $encoder ?: new Encoder();
    $this->claimFactory = $claimFactory ?: new ClaimFactory();
    $this->headers = ['typ'=> 'JWT', 'alg' => 'none'];
    $this->claims = [];
}

public function setAudience($audience, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('aud', (string) $audience, $replicateAsHeader);
}

public function setId($id, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('jti', (string) $id, $replicateAsHeader);
}

public function setIssuedAt($issuedAt, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('iat', (int) $issuedAt, $replicateAsHeader);
}

public function setNotBefore($notBefore, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('nbf', (int) $notBefore, $replicateAsHeader);
}

public function setExpiration($expiration, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('exp', (int) $expiration, $replicateAsHeader);
}

public function setSubject($subject, $replicateAsHeader = false)
{
    return $this->setRegisteredClaim('sub', (string) $subject, $replicateAsHeader);
}

public function sign(Signer $signer, $key)
{
    $signer->modifyHeader($this->headers);

    $this->signature = $signer->sign(
        $this->getToken()->getPayload(),
        $key
    );

    return $this;
}

public function getToken()
{
    $payload = [
        $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->headers)),
        $this->encoder->base64UrlEncode($this->encoder->jsonEncode($this->claims))
    ];

    if ($this->signature !== null) {
        $payload[] = $this->encoder->base64UrlEncode($this->signature);
    }

    return new Token($this->headers, $this->claims, $this->signature, $payload);
}

根據 JWT 的生成方法,簽名部分 signatureheaderclaim 進行 base64 編碼後再加密的結果。

 

客戶端模式

客戶端憑據授權適用於機器到機器的認證。例如,你可以在透過 API 執行維護任務中使用此授權。要使用這種授權,你首先需要在 app/Http/Kernel.php 的 routeMiddleware 變數中新增新的中介軟體:

protected $routeMiddleware = [
    'client' => CheckClientCredentials::class,
];

Route::get('/user', function(Request $request) {
    ...
})->middleware('client');

接下來透過向 oauth/token 介面發出請求來獲取令牌:

$response = $guzzle->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'client_credentials',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'scope' => 'your-scope',
    ],
]);

echo json_decode((string) $response->getBody(), true);

客戶端模式類似於授權碼模式的後一部分,利用客戶端 id 與客戶端密碼來獲取 access_token

public function respondToAccessTokenRequest(
    ServerRequestInterface $request,
    ResponseTypeInterface $responseType,
    \DateInterval $accessTokenTTL
) {
    // Validate request
    $client = $this->validateClient($request);
    $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));

    // Finalize the requested scopes
    $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);

    // Issue and persist access token
    $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $finalizedScopes);

    // Inject access token into response type
    $responseType->setAccessToken($accessToken);

    return $responseType;
}

類似於授權碼模式,access_token 的發放也是透過 Bearer Token 中存放 JWT。

 

密碼模式

OAuth2 密碼授權機制可以讓你自己的客戶端(如移動應用程式)郵箱地址或者使用者名稱和密碼獲取訪問令牌。如此一來你就可以安全地向自己的客戶端發出訪問令牌,而不需要遍歷整個 OAuth2 授權程式碼重定向流程。

建立密碼授權的客戶端後,就可以透過向使用者的電子郵件地址和密碼向 /oauth/token 路由發出 POST 請求來獲取訪問令牌。而該路由已經由 Passport::routes 方法註冊,因此不需要手動定義它。如果請求成功,會在服務端返回的 JSON 響應中收到一個 access_token 和 refresh_token:

$response = $http->post('http://your-app.com/oauth/token', [
    'form_params' => [
        'grant_type' => 'password',
        'client_id' => 'client-id',
        'client_secret' => 'client-secret',
        'username' => 'taylor@laravel.com',
        'password' => 'my-password',
        'scope' => '',
    ],
]);

return json_decode((string) $response->getBody(), true);

只要用使用者名稱與密碼來驗證合法性就可以發放 access_tokenrefresh_token

public function respondToAccessTokenRequest(
    ServerRequestInterface $request,
    ResponseTypeInterface $responseType,
    \DateInterval $accessTokenTTL
) {
    // Validate request
    $client = $this->validateClient($request);
    $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
    $user = $this->validateUser($request, $client);

    // Finalize the requested scopes
    $finalizedScopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());

    // Issue and persist new tokens
    $accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $finalizedScopes);
    $refreshToken = $this->issueRefreshToken($accessToken);

    // Inject tokens into response
    $responseType->setAccessToken($accessToken);
    $responseType->setRefreshToken($refreshToken);

    return $responseType;
}

protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
{
    $username = $this->getRequestParameter('username', $request);
    if (is_null($username)) {
        throw OAuthServerException::invalidRequest('username');
    }

    $password = $this->getRequestParameter('password', $request);
    if (is_null($password)) {
        throw OAuthServerException::invalidRequest('password');
    }

    $user = $this->userRepository->getUserEntityByUserCredentials(
        $username,
        $password,
        $this->getIdentifier(),
        $client
    );
    if ($user instanceof UserEntityInterface === false) {
        $this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));

        throw OAuthServerException::invalidCredentials();
    }

    return $user;
}

 

路由保護

Passport 包含一個 驗證保護機制 可以驗證請求中傳入的訪問令牌。配置 api 的看守器使用 passport 驅動程式後,只需要在需要有效訪問令牌的任何路由上指定 auth:api 中介軟體:

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

當呼叫 Passport 保護下的路由時,接入的 API 應用需要將訪問令牌作為 Bearer 令牌放在請求頭 Authorization 中。例如,使用 Guzzle HTTP 庫時:

$response = $client->request('GET', '/api/user', [
    'headers' => [
        'Accept' => 'application/json',
        'Authorization' => 'Bearer '.$accessToken,
    ],
]);

 

auth:api 中介軟體

當我們已經配置完成 Passport 的四種模式並拿到 access_token 之後,我們就可以利用令牌去資源伺服器獲取資料了。資源伺服器最常用的校驗令牌的中介軟體就是 auth:api,中介軟體是 authapi 是中介軟體的引數:


'auth' => \Illuminate\Auth\Middleware\Authenticate::class,

這個中介軟體是驗證登入狀態的常用中介軟體:


class Authenticate
{
    public function __construct(Auth $auth)
    {
        $this->auth = $auth;
    }

    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($guards);

        return $next($request);
    }

    protected function authenticate(array $guards)
    {
        if (empty($guards)) {
            return $this->auth->authenticate();
        }

        foreach ($guards as $guard) {
            if ($this->auth->guard($guard)->check()) {
                return $this->auth->shouldUse($guard);
            }
        }

        throw new AuthenticationException('Unauthenticated.', $guards);
    }
}

我們的引數 api 就是上面的 guardsAuthlaravel 自帶的登入校驗服務:


class AuthManager implements FactoryContract
{
    public function guard($name = null)
    {
        $name = $name ?: $this->getDefaultDriver();

        return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
    }

    protected function resolve($name)
    {
        $config = $this->getConfig($name);

        if (is_null($config)) {
            throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
        }

        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($name, $config);
        }

        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($name, $config);
        }

        throw new InvalidArgumentException("Auth guard driver [{$name}] is not defined.");
    }

}

文件告訴我們,若想要使用 passport 服務,我們的 config/auth 檔案需要如此配置:


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

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

可以看出,driver 就是 passport,我們在啟動 passport 服務的時候曾經註冊過一個 Guard

protected function registerGuard()
{
    Auth::extend('passport', function ($app, $name, array $config) {
        return tap($this->makeGuard($config), function ($guard) {
            $this->app->refresh('request', $guard, 'setRequest');
        });
    });
}

protected function makeGuard(array $config)
{
    return new RequestGuard(function ($request) use ($config) {
        return (new TokenGuard(
            $this->app->make(ResourceServer::class),
            Auth::createUserProvider($config['provider']),
            $this->app->make(TokenRepository::class),
            $this->app->make(ClientRepository::class),
            $this->app->make('encrypter')
        ))->user($request);
    }, $this->app['request']);
}

因此,passport 使用的就是這個 TokenGuard

class TokenGuard
{
    public function __construct(ResourceServer $server,
                                UserProvider $provider,
                                TokenRepository $tokens,
                                ClientRepository $clients,
                                Encrypter $encrypter)
    {
        $this->server = $server;
        $this->tokens = $tokens;
        $this->clients = $clients;
        $this->provider = $provider;
        $this->encrypter = $encrypter;
    }

    public function user(Request $request)
    {
        if ($request->bearerToken()) {
            return $this->authenticateViaBearerToken($request);
        } elseif ($request->cookie(Passport::cookie())) {
            return $this->authenticateViaCookie($request);
        }
    }
}

可以看到,TokenGuard 支援兩種 Token 的驗證:BearerTokencookie

我們首先看 BearerToken:

public function bearerToken()
{
    $header = $this->header('Authorization', '');

    if (Str::startsWith($header, 'Bearer ')) {
        return Str::substr($header, 7);
    }
}

protected function authenticateViaBearerToken($request)
{
    $psr = (new DiactorosFactory)->createRequest($request);

    try {
        $psr = $this->server->validateAuthenticatedRequest($psr);

        $user = $this->provider->retrieveById(
            $psr->getAttribute('oauth_user_id')
        );

        if (! $user) {
            return;
        }

        $token = $this->tokens->find(
            $psr->getAttribute('oauth_access_token_id')
        );

        $clientId = $psr->getAttribute('oauth_client_id');

        if ($this->clients->revoked($clientId)) {
            return;
        }

        return $token ? $user->withAccessToken($token) : null;
    } catch (OAuthServerException $e) {
        return Container::getInstance()->make(
            ExceptionHandler::class
        )->report($e);
    }
}

首先,需要驗證請求的合法性:

class ResourceServer
{
    public function validateAuthenticatedRequest(ServerRequestInterface $request)
    {
        return $this->getAuthorizationValidator()->validateAuthorization($request);
    }

    protected function getAuthorizationValidator()
    {
        if ($this->authorizationValidator instanceof AuthorizationValidatorInterface === false) {
            $this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository);
        }

        $this->authorizationValidator->setPublicKey($this->publicKey);

        return $this->authorizationValidator;
    }

}

BearerTokenValidator 專門用於驗證 BearerToken 的合法性:

class BearerTokenValidator implements AuthorizationValidatorInterface
{
    public function validateAuthorization(ServerRequestInterface $request)
    {
        if ($request->hasHeader('authorization') === false) {
            throw OAuthServerException::accessDenied('Missing "Authorization" header');
        }

        $header = $request->getHeader('authorization');
        $jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));

        try {
            // Attempt to parse and validate the JWT
            $token = (new Parser())->parse($jwt);
            if ($token->verify(new Sha256(), $this->publicKey->getKeyPath()) === false) {
                throw OAuthServerException::accessDenied('Access token could not be verified');
            }

            // Ensure access token hasn't expired
            $data = new ValidationData();
            $data->setCurrentTime(time());

            if ($token->validate($data) === false) {
                throw OAuthServerException::accessDenied('Access token is invalid');
            }

            // Check if token has been revoked
            if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
                throw OAuthServerException::accessDenied('Access token has been revoked');
            }

            // Return the request with additional attributes
            return $request
                ->withAttribute('oauth_access_token_id', $token->getClaim('jti'))
                ->withAttribute('oauth_client_id', $token->getClaim('aud'))
                ->withAttribute('oauth_user_id', $token->getClaim('sub'))
                ->withAttribute('oauth_scopes', $token->getClaim('scopes'));
        } catch (\InvalidArgumentException $exception) {
            // JWT couldn't be parsed so return the request as is
            throw OAuthServerException::accessDenied($exception->getMessage());
        } catch (\RuntimeException $exception) {
            //JWR couldn't be parsed so return the request as is
            throw OAuthServerException::accessDenied('Error while decoding to JSON');
        }
    }
}

透過 passport 拿到的 access_token 都是 JWT 格式的,因此首先第一步需要將 JWT 解析:

class Parser
{
    public function parse($jwt)
    {
        $data = $this->splitJwt($jwt);
        $header = $this->parseHeader($data[0]);
        $claims = $this->parseClaims($data[1]);
        $signature = $this->parseSignature($header, $data[2]);

        foreach ($claims as $name => $value) {
            if (isset($header[$name])) {
                $header[$name] = $value;
            }
        }

        if ($signature === null) {
            unset($data[2]);
        }

        return new Token($header, $claims, $signature, $data);
    }

    protected function splitJwt($jwt)
    {
        if (!is_string($jwt)) {
            throw new InvalidArgumentException('The JWT string must have two dots');
        }

        $data = explode('.', $jwt);

        if (count($data) != 3) {
            throw new InvalidArgumentException('The JWT string must have two dots');
        }

        return $data;
    }

    protected function parseHeader($data)
    {
        $header = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));

        if (isset($header['enc'])) {
            throw new InvalidArgumentException('Encryption is not supported yet');
        }

        return $header;
    }

    protected function parseClaims($data)
    {
        $claims = (array) $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data));

        foreach ($claims as $name => &$value) {
            $value = $this->claimFactory->create($name, $value);
        }

        return $claims;
    }

    protected function parseSignature(array $header, $data)
    {
        if ($data == '' || !isset($header['alg']) || $header['alg'] == 'none') {
            return null;
        }

        $hash = $this->decoder->base64UrlDecode($data);

        return new Signature($hash);
    }

}

獲得 JWT 的三個部分之後,就要驗證簽名部分是否合法:

class Token
{
    public function verify(Signer $signer, $key)
    {
        if ($this->signature === null) {
            throw new BadMethodCallException('This token is not signed');
        }

        if ($this->headers['alg'] !== $signer->getAlgorithmId()) {
            return false;
        }

        return $this->signature->verify($signer, $this->getPayload(), $key);
    }
}

驗證透過之後,就要驗證 JWT 各個部分是否合法:

$data = new ValidationData();
$data->setCurrentTime(time());

public function __construct($currentTime = null)
{
    $currentTime = $currentTime ?: time();

    $this->items = [
        'jti' => null,
        'iss' => null,
        'aud' => null,
        'sub' => null,
        'iat' => $currentTime,
        'nbf' => $currentTime,
        'exp' => $currentTime
    ];
}

public function validate(ValidationData $data)
{
    foreach ($this->getValidatableClaims() as $claim) {
        if (!$claim->validate($data)) {
            return false;
        }
    }

    return true;
}    

public function __construct(array $callbacks = [])
{
    $this->callbacks = array_merge(
        [
            'iat' => [$this, 'createLesserOrEqualsTo'],
            'nbf' => [$this, 'createLesserOrEqualsTo'],
            'exp' => [$this, 'createGreaterOrEqualsTo'],
            'iss' => [$this, 'createEqualsTo'],
            'aud' => [$this, 'createEqualsTo'],
            'sub' => [$this, 'createEqualsTo'],
            'jti' => [$this, 'createEqualsTo']
        ],
        $callbacks
    );
}

我們前面說過,

  • aud 客戶端 id
  • jti access_token 隨機碼
  • iat 生成時間
  • nbf 拒絕接受 jwt 時間
  • exp access_token 失效時間
  • sub 使用者 id

因此,JWT 的生成時間、拒絕接受時間、失效時間就會被驗證完成。

接下來,還會驗證最重要的 access_token

if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
    throw OAuthServerException::accessDenied('Access token has been revoked');
}

public function isAccessTokenRevoked($tokenId)
{
    return $this->tokenRepository->isAccessTokenRevoked($tokenId);
}

public function isAccessTokenRevoked($id)
{
    if ($token = $this->find($id)) {
        return $token->revoked;
    }

    return true;
}

接下來,TokenGuard 就會驗證 useridclientidaccess_token 的合法性:

$user = $this->provider->retrieveById(
    $psr->getAttribute('oauth_user_id')
);

if (! $user) {
    return;
}

$token = $this->tokens->find(
    $psr->getAttribute('oauth_access_token_id')
);

$clientId = $psr->getAttribute('oauth_client_id');

if ($this->clients->revoked($clientId)) {
    return;
}

return $token ? $user->withAccessToken($token) : null;

中介軟體驗證完成。

 

客戶端模式中介軟體 CheckClientCredentials

我們在上面可以看到 auth:api 中介軟體不僅驗證 access_token,還會驗證 user_id,對於客戶端模式來說,由於 JWT 中並沒有使用者資訊,因此 passport 專門存在中介軟體 CheckClientCredentials 來做非登入狀態的校驗。

class CheckClientCredentials
{
    public function handle($request, Closure $next, ...$scopes)
    {
        $psr = (new DiactorosFactory)->createRequest($request);

        try {
            $psr = $this->server->validateAuthenticatedRequest($psr);
        } catch (OAuthServerException $e) {
            throw new AuthenticationException;
        }

        $this->validateScopes($psr, $scopes);

        return $next($request);
    }
}

 

使用 JavaScript 接入 API

在構建 API 時,如果能透過 JavaScript 應用接入自己的 API 將會給開發過程帶來極大的便利。這種 API 開發方法允許你使用自己的應用程式的 API 和別人共享的 API。你的 Web 應用程式、移動應用程式、第三方應用程式以及可能在各種軟體包管理器上釋出的任何 SDK 都可能會使用相同的API。

通常,如果要從 JavaScript 應用程式中使用 API,則需要手動向應用程式傳送訪問令牌,並將其傳遞給應用程式。但是,Passport 有一個可以處理這個問題的中介軟體。將 CreateFreshApiToken 中介軟體新增到 web 中介軟體組就可以了:

'web' => [
    // Other middleware...
    \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],

Passport 的這個中介軟體將會在你所有的對外請求中新增一個 laravel_token cookie。該 cookie 將包含一個加密後的 JWT ,Passport 將用來驗證來自 JavaScript 應用程式的 API 請求。至此,你可以在不明確傳遞訪問令牌的情況下向應用程式的 API 發出請求

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

當使用上面的授權方法時,Axios 會自動帶上 X-CSRF-TOKEN 請求頭傳遞。另外,預設的 Laravel JavaScript 腳手架會讓 Axios 傳送 X-Requested-With 請求頭:

window.axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
};

 

CreateFreshApiToken 中介軟體

class CreateFreshApiToken
{
    public function handle($request, Closure $next, $guard = null)
    {
        $this->guard = $guard;

        $response = $next($request);

        if ($this->shouldReceiveFreshToken($request, $response)) {
            $response->withCookie($this->cookieFactory->make(
                $request->user($this->guard)->getKey(), $request->session()->token()
            ));
        }

        return $response;
    }

    public function make($userId, $csrfToken)
    {
        $config = $this->config->get('session');

        $expiration = Carbon::now()->addMinutes($config['lifetime']);

        return new Cookie(
            Passport::cookie(),
            $this->createToken($userId, $csrfToken, $expiration),
            $expiration,
            $config['path'],
            $config['domain'],
            $config['secure'],
            true
        );
    }

    protected function createToken($userId, $csrfToken, Carbon $expiration)
    {
        return JWT::encode([
            'sub' => $userId,
            'csrf' => $csrfToken,
            'expiry' => $expiration->getTimestamp(),
        ], $this->encrypter->getKey());
    }

    protected function shouldReceiveFreshToken($request, $response)
    {
        return $this->requestShouldReceiveFreshToken($request) &&
               $this->responseShouldReceiveFreshToken($response);
    }

    protected function requestShouldReceiveFreshToken($request)
    {
        return $request->isMethod('GET') && $request->user($this->guard);
    }

    protected function responseShouldReceiveFreshToken($response)
    {
        return $response instanceof Response && ! $this->alreadyContainsToken($response);
    }
}

這個中介軟體發出的 JWT 令牌仍然由 auth:api 來負責驗證,我們前面說過,TokenGuard 負責兩種令牌的驗證,一種是 BearerToken, 另一種就是這個 Cookie :

public function user(Request $request)
{
    if ($request->bearerToken()) {
        return $this->authenticateViaBearerToken($request);
    } elseif ($request->cookie(Passport::cookie())) {
        return $this->authenticateViaCookie($request);
    }
}

protected function authenticateViaCookie($request)
{
    try {
        $token = $this->decodeJwtTokenCookie($request);
    } catch (Exception $e) {
        return;
    }

    if (! $this->validCsrf($token, $request) ||
        time() >= $token['expiry']) {
        return;
    }

    if ($user = $this->provider->retrieveById($token['sub'])) {
        return $user->withAccessToken(new TransientToken);
    }
}

protected function decodeJwtTokenCookie($request)
{
    return (array) JWT::decode(
        $this->encrypter->decrypt($request->cookie(Passport::cookie())),
        $this->encrypter->getKey(), ['HS256']
    );
}

protected function validCsrf($token, $request)
{
    return isset($token['csrf']) && hash_equals(
        $token['csrf'], (string) $request->header('X-CSRF-TOKEN')
    );
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章