stevewaffles/jwt-auth

CakePHP 的 JWT 身份验证插件。

安装: 0

依赖者: 0

建议者: 0

安全: 0

星级: 0

关注者: 0

分支: 2

类型:cakephp-plugin

dev-master 2024-01-21 19:14 UTC

This package is not auto-updated.

Last update: 2024-09-16 20:36:50 UTC


README

Latest Version on Packagist Build Coverage Status MixerApi CakePHP Minimum PHP Version

这是一个为 CakePHP 提供支持的 JWT 身份验证库,支持 HMAC(HS256 或 HS512)和 RSA(RS256 或 RS512)算法,并使用 JSON Web Keys。在开始之前,您应该确定哪种 签名算法 最适合您的需求。本库的目标是使两种算法都变得简单易用。

有关替代方法,请参阅 admad/cakephp-jwt-auth

安装

您可以使用 composer 将此插件安装到您的 CakePHP 应用程序中。

composer require mixerapi/jwt-auth

然后加载插件

bin/cake plugin load MixerApi/JwtAuth

配置

接下来,创建一个配置文件(例如 config/mixerapi_jwtauth.php),并将其加载到您的应用程序中。

# in config/bootstrap.php
Configure::load('mixerapi_jwtauth');

alg

alg 字符串是必需的,必须为 HS256、HS512、RS256 或 RS512。

secret

当使用 HMAC 时,secret 字符串是必需的。密钥不应提交到您的 VCS,且长度至少为 32 个字符。您可以使用 openssl 或 gpg 等工具生成强密码。

openssl rand -base64 24
gpg --armor --gen-random 1 24

keys

当使用 RSA 时,keys 数组是必需的。密钥不应提交到您的 VCS,且长度至少为 2048 位。您可以使用 openssl 生成公钥/私钥对。

openssl genrsa -out config/keys/1/private.pem 2048
openssl rsa -in config/keys/1/private.pem -out config/keys/1/public.pem -pubout

服务提供者

建议使用 JwtAuthServiceProvider 自动注入依赖项。

# in src/Application.php

public function services(ContainerInterface $container): void
{
    /** @var \League\Container\Container $container */
    $container->addServiceProvider(new \MixerApi\JwtAuth\JwtAuthServiceProvider());
}

身份验证

您需要配置 CakePHP 身份验证 以使用此库。快速入门中记录了多种实现方式。请参阅 mixerapi demo 以获取完整示例。

确保加载 CakePHP Authentication.Component(通常在您的 AppController 中)。

以下示例支持使用表单和密码进行身份验证的 HMAC 和 RSA。无论您如何实现身份验证,都建议使用 \MixerApi\JwtAuth\Configuration\Configuration 从您的 MixerApi.JwtAuth 配置文件 config/mixerapi_jwtauth.php 中提取值。这将验证您的配置,然后再将其应用到您的应用程序的身份验证中。

# in src/Application.php

public function getAuthenticationService(ServerRequestInterface $request): \Authentication\AuthenticationServiceInterface
{
    $fields = [
        \Authentication\Identifier\IdentifierInterface::CREDENTIAL_USERNAME => 'email',
        \Authentication\Identifier\IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
    ];

    $config = new \MixerApi\JwtAuth\Configuration\Configuration();
    $service = new \Authentication\AuthenticationService();

    $service->loadAuthenticator('Authentication.Form', [
        'fields' => $fields,
        'loginUrl' => '/admin/auth/login'
    ]);

    $service->loadIdentifier('Authentication.JwtSubject');

    if (str_starts_with(haystack: $config->getAlg(), needle: 'HS')) {
        $service->loadAuthenticator('Authentication.Jwt', [
            'secretKey' => $config->getSecret(),
            'algorithm' => $config->getAlg(),
        ]);
    } else if (str_starts_with(haystack: $config->getAlg(), needle: 'RS')) {
        $jsonKeySet = \Cake\Cache\Cache::remember('jwkset', function() {
            return json_encode((new \MixerApi\JwtAuth\Jwk\JwkSet)->getKeySet());
        });

        /*
         * Caching is optional, you may also set the jwks key to the return value of (new JwkSet)->getKeySet()
         */
        $service->loadAuthenticator('Authentication.Jwt', [
            'jwks' => json_decode($jsonKeySet, true),
            'algorithm' => $config->getAlg(),
        ]);
    }

    $service->loadIdentifier('Authentication.Password', ['fields' => $fields]);

    return $service;
}

定义您的 JWT

在您的 User 实体中实现 JwtEntityInterface。这将用于生成 JWT,例如

namespace App\Model\Entity;

use Cake\ORM\Entity;
use MixerApi\JwtAuth\Jwt\Jwt;
use MixerApi\JwtAuth\Jwt\JwtEntityInterface;
use MixerApi\JwtAuth\Jwt\JwtInterface;

class User extends Entity implements JwtEntityInterface
{
    /**
     * @inheritDoc
     */
    public function getJwt(): JwtInterface
    {
        return new Jwt(
            exp: time() + 60 * 60 * 24,
            sub: $this->get('id'),
            iss: 'mixerapi',
            aud: 'mixerapi-client',
            nbf: null,
            iat: time(),
            jti: \Cake\Utility\Text::uuid(),
            claims: [
                'user' => [
                    'email' => $this->get('email')
                ]
            ]
        );
    }
}

JSON Web Keys

使用 RSA 对令牌进行签名时使用公钥/私钥对。如果您使用 HMAC,则可以跳过此部分。

构建密钥

我们将密钥存储在 config/keys/1/,但您可以将它们存储在任何地方。密钥不应存储在版本控制中,例如

# in config/mixerapi_jwtauth.php

return [
    'MixerApi.JwtAuth' => [
        'alg' => 'RS256',
        'keys' => [
            [
                'kid' => '1',
                'public' => file_get_contents(CONFIG . 'keys' . DS . '1' . DS . 'public.pem'),
                'private' => file_get_contents(CONFIG . 'keys' . DS . '1' . DS . 'private.pem'),
            ]
        ]
    ]
];

JWK Set 控制器

有关 JSON Web Keys 的更多信息,请参阅 此处。让我们创建一个端点来公开您的 JWK Set。

use Cake\Controller\Controller;
use Cake\Event\EventInterface;
use MixerApi\JwtAuth\Jwk\JwkSetInterface;

class JwksController extends Controller
{
    public function beforeFilter(EventInterface $event)
    {
        parent::beforeFilter($event);
        $this->Authentication->allowUnauthenticated(['index']);
    }

    public function index(JwkSetInterface $jwkSet)
    {
        $this->set('data', $jwkSet->getKeySet());
        $this->viewBuilder()->setOption('serialize', 'data');
    }
}

在您的 config/routes.php 文件中为您的控制器添加一个路由。

示例响应

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "1",
      "n": "wk865HbUKadJU-Mh-Iv2Z_30ZOMclkK1cbuiTVkINy_R9oHoAht2DS788q_Sll38dtTB4bzptd0u6k4cJd6Lj6nVQTe1uRyuAU47tqitiJmEXX_2SHIRv6aj4vygIfqr1FtQMHPlBW7r4q840H5mh_Z-E_a7d27QbtJ3eYNEiFow6LLvl17_7bdaenlwccY0j-PY1GzL7UwG8uHBZ78ZOcvu_GgaYC5suRrJrV_6_Qu6lySXObDaajr6Foz0m-z4Aj7KA8KmAiM_Rw_Yqm_KqPT3YBGj83TxeEiMPkrMYry123hFQYm09EO2Az9lGjr-PQc6SR08SDqZ3zbwe9iam55dzVZ-vQF3ASnZpBHyIDhCI7PFShceFI1Sv0RW7-Tl0uM2jQa1RyEpFle1xc0RxSFZium0aGMnFuE2W9JDERPw47wFZx2kSk1nB6PDK6XPLJLi_db0VrP5m5z2HDWeYVmsuAVFm6-l1PjiGH4G1TpuYfPKP2P8K-kveo1Ddm14IJSWfcACeAF_gx644Ua_IJ8wS98dQqE-R-jzfEv7aLBacP5_thCUbHfCRrAgtM5lBAM_1tfQ4XsOLnFWkl4arm3TzN2wCjjuqxipgwpUtY_SN6SXhJW4MW2qHVKtHtXl9haF5gEDBL7twDsFozYZCc5k0d85EgfJ5Jn7ZSAgwXk",
      "e": "AQAB"
    }
  ]
}

您可以在配置文件 MixerApi.JwtAuth.keys 中添加/删除密钥,作为您的密钥轮换策略的一部分。

注意,如果您没有使用依赖注入

    public function index()
    {
        $this->set('data', (new JwkSet)->getKeySet());
        $this->viewBuilder()->setOption('serialize', 'data');
    }

登录控制器

以下示例将执行身份验证,创建我们之前定义的 JWT,并将其返回给请求者。

use Cake\Controller\Controller;
use MixerApi\JwtAuth\JwtAuthenticatorInterface;

public function LoginController extends Controller
{
    public function beforeFilter(EventInterface $event)
    {
        parent::beforeFilter($event);
        $this->Authentication->allowUnauthenticated(['login']);
    }

    public function login(JwtAuthenticatorInterface $jwtAuth)
    {
        try {
            return $this->response->withStringBody($jwtAuth->authenticate($this->Authentication));
        } catch (UnauthenticatedException $e) {
            return $this->response->withStringBody($e->getMessage())->withStatus(401);
        }
    }
}

在您的 config/routes.php 文件中为控制器添加一个路由。

这将构建我们在用户实体中定义的JWT。

{
  "iss": "mixerapi",
  "sub": "5e28e9ed-f3e1-4eb2-aa88-8d618f4021ee",
  "aud": "api-client",
  "exp": 1651972707,
  "jti": "a1f6f5ec-748d-4a1c-9d0e-f8e19ec7f9b2",
  "user": {
    "email": "test@example.com"
  }
}

注意,如果您没有使用依赖注入

    public function login()
    {
        try {
            return $this->response->withStringBody(
                (new \MixerApi\JwtAuth\JwtAuthenticator)->authenticate($this->Authentication)
            );
        } catch (UnauthenticatedException $e) {
            return $this->response->withStringBody($e->getMessage())->withStatus(401);
        }
    }

或者,如果您更喜欢自己处理身份验证,可以传递一个JwtInterface的实例,例如

    public function login(JwtAuthenticatorInterface $jwtAuth)
    {
        try {
            $result = $this->Authentication->getResult();
            if (!$result->isValid()) {
                throw new UnauthenticatedException();
            }
            return $this->response->withStringBody($jwtAuth->authenticate($result->getData()->getJwt()));
        } catch (UnauthenticatedException $e) {
            return $this->response->withStringBody($e->getMessage())->withStatus(401);
        }
    }

安全

该库内置了一些安全措施

弱HMAC密钥

使用HMAC签名的JWT可以使用像JWT Tool这样的工具进行暴力破解。一旦破解,JWT可以被修改。尽管如此,这个库通过要求最小密钥长度为32个字符来减轻这种风险,尽管您可能希望考虑使用64个字符,如果安全性比速度和令牌大小更重要。生成强随机密钥并确保其安全是您自己的责任。

弱RSA密钥

弱密钥同样可以被破解。这个库要求最小密钥长度为2048位。根据您的安全需求,您可能希望考虑使用4096位的密钥长度。保护您的密钥是您自己的责任。

Alg None Bypass

通过要求单个有效算法来缓解alg=none签名绕过漏洞。在firebase/php-jwt库中还有额外的保护措施,该库应该保持最新。

RS/HS256公钥不匹配漏洞

通过要求单个有效算法来缓解。在firebase/php-jwt库中还有额外的保护措施,该库应该保持最新。