robsontenorio/laravel-keycloak-guard

🔑 Laravel 的简单 Keycloak 守卫

1.6.1 2024-05-25 19:12 UTC

README

 

Simple Keycloak Guard for Laravel

此包帮助您在基于 Keycloak 服务器生成的 JWT 令牌的基础上,在 Laravel API 上进行用户身份验证。

要求

✔️ 我正在使用 Laravel 构建一个 API。

✔️ 我将不会使用 Laravel Passport 进行身份验证,因为 Keycloak 服务器会完成这项工作。

✔️ 前端是一个独立的项目。

✔️ 前端用户将直接在 Keycloak 服务器上进行身份验证以获取 JWT 令牌。这个过程与 Laravel API 没有关系。

✔️ 前端将保留从 Keycloak 服务器获取的 JWT 令牌。

✔️ 前端使用该令牌向 Laravel API 发送请求。

💔 如果您的应用不符合要求,您可能需要查找 https://socialiteproviders.com/Keycloakhttps://github.com/Vizir/laravel-keycloak-web-guard

流程

  1. 前端用户在 Keycloak 服务器上进行身份验证

  2. 前端用户获取 JWT 令牌。

  3. 在另一个时刻,前端用户使用该令牌向 Laravel API 上的受保护端点发送请求。

  4. Laravel API(通过 Keycloak Guard)处理它。

    • 验证令牌签名。
    • 验证令牌结构。
    • 验证令牌过期时间。
    • 验证我的 API 是否允许从令牌中进行 资源访问
  5. 如果一切正常,然后在数据库中查找用户并在我的 API 上进行身份验证。

  6. 可选地,用户可以在 API 用户数据库中创建/更新。

  7. 返回响应

安装

需要此包

composer require robsontenorio/laravel-keycloak-guard

如果您正在使用 Lumen,在您的 bootstrap app 文件 bootstrap/app.php 中注册提供者。
对于外观,在您的 bootstrap app 文件 bootstrap/app.php 中取消注释 $app->withFacades();

$app->register(\KeycloakGuard\KeycloakGuardServiceProvider::class);

示例配置 (.env)

KEYCLOAK_REALM_PUBLIC_KEY=MIIBIj...         # Get it on Keycloak admin web console.
KEYCLOAK_LOAD_USER_FROM_DATABASE=false      # You can opt to not load user from database, and use that one provided from JWT token.
KEYCLOAK_APPEND_DECODED_TOKEN=true          # Append the token info to user object.
KEYCLOAK_ALLOWED_RESOURCES=my-api           # The JWT token must contain this resource `my-api`.
KEYCLOAK_LEEWAY=60                          # Optional, but solve some weird issues with timestamps from JWT token.

身份验证守卫

config/auth.php 中进行更改

'defaults' => [
    'guard' => 'api',                 # <-- This
    'passwords' => 'users',
],    
'guards' => [
    'api' => [
        'driver' => 'keycloak',       # <-- This
        'provider' => 'users',
    ],
],

路由

只需在 routes/api.php 中保护一些端点,您就完成了!

// public endpoints
Route::get('/hello', function () {
    return ':)';
});

// protected endpoints
Route::group(['middleware' => 'auth:api'], function () {
    Route::get('/protected-endpoint', 'SecretController@index');

    // more endpoints ...
});

配置

Keycloak 守卫

⚠️ 当编辑 .env 时,确保所有字符串都 已删除尾随空格。

# Publish config file

php artisan vendor:publish  --provider="KeycloakGuard\KeycloakGuardServiceProvider"

✔️ realm_public_key

必需。

Keycloak 服务器域的公钥(字符串)。

如何获取域公钥?单击“域设置”>“密钥”>“算法 RS256(或定义在 token_encryption_algorithm 配置下)”行>“公钥”按钮

✔️ token_encryption_algorithm

默认为 RS256

Keycloak 用于 JWT 令牌加密的算法(字符串)。

✔️ load_user_from_database

必需。默认为 true

如果您没有 users 表,则必须禁用此选项。

它从数据库中获取用户并将值填充到已验证用户对象中。如果启用,它将与 user_provider_credentialtoken_principal_attribute 一起工作。

✔️ user_provider_custom_retrieve_method

默认为 null

如果您有一个 users 表,并希望根据令牌对其进行更新(创建或更新用户),则可以在自定义 UserProvider 上调用自定义方法,而不是默认的 retrieveByCredentials 方法。该方法将接收完整的解码令牌作为参数,而不仅仅是凭据(默认情况)。这将允许您自定义与数据库交互的方式,在匹配并传递已验证的用户对象之前,获取包含在(有效)访问令牌中的所有信息。有关自定义 UserProvider 的更多信息,请参阅 Laravel 的文档

显然,如果您使用此功能,将忽略为 user_provider_credentialtoken_principal_attribute 定义的值。

✔️ user_provider_credential

必需。默认为 username

"users" 表中的字段,包含用户的唯一标识符(例如,用户名、电子邮件、昵称)。在身份验证时,此字段将与 token_principal_attribute 属性进行比较。

✔️ token_principal_attribute

必需。默认为 preferred_username

JWT 令牌中包含用户标识符的属性。在身份验证时,此属性将与 user_provider_credential 属性进行比较。

✔️ append_decoded_token

默认为 false

将完整的解码 JWT 令牌($user->token)附加到已验证的用户。如果您需要知道由 JWT 令牌持有的角色、组和其他用户信息,则很有用。即使选择 false,您也可以通过 Auth::token() 获取它,请参阅 API 部分。

✔️ allowed_resources

必需.

通常您的 API 应该处理一个 resource_access。但是,如果您处理多个,请使用逗号分隔的允许资源列表。在身份验证时,此属性将与 JWT 令牌中的 resource_access 属性进行比较。

✔️ ignore_resources_validation

默认为 false.

完全禁用资源验证。它将 忽略 allowed_resources 配置。

✔️ leeway

默认为 0.

您可以为在签名服务器和验证服务器之间存在时钟偏移的情况添加一个宽容度。如果您遇到像 "Cannot handle token prior to " 这样的问题,请尝试将其设置为 60(秒)。

✔️ input_key

默认为 null

默认情况下,此包始终首先查找 Bearer 令牌。此外,如果启用此选项,则将尝试从自定义请求参数获取令牌。

// keycloak.php
'input_key' => 'api_token'

// If there is no Bearer token on request it will use `api_token` request param
GET  $this->get("/foo/secret?api_token=xxxxx")
POST $this->post("/foo/secret", ["api_token" => "xxxxx"])

API

Simple Keycloak Guard 实现 Illuminate\Contracts\Auth\Guard。因此,所有 Laravel 默认方法都将可用。

默认 Laravel 方法

  • check()
  • guest()
  • user()
  • id()
  • validate()
  • setUser()

Keycloak Guard 方法

令牌

token() 从已验证用户返回完整的解码 JWT 令牌。

$token = Auth::token()  // or Auth::user()->token()

角色

hasRole('some-resource', 'some-role') 检查已验证用户是否在 resource_access 上具有角色

// Example decoded payload

'resource_access' => [
  'myapp-backend' => [
      'roles' => [
        'myapp-backend-role1',
        'myapp-backend-role2'
      ]
  ],
  'myapp-frontend' => [
    'roles' => [
      'myapp-frontend-role1',
      'myapp-frontend-role2'
    ]
  ]
]
Auth::hasRole('myapp-backend', 'myapp-backend-role1') // true
Auth::hasRole('myapp-frontend', 'myapp-frontend-role1') // true
Auth::hasRole('myapp-backend', 'myapp-frontend-role1') // false

hasAnyRole('some-resource', ['some-role1', 'some-role2']) 检查已验证用户是否在 resource_access 上具有任何角色

Auth::hasAnyRole('myapp-backend', ['myapp-backend-role1', 'myapp-backend-role3']) // true
Auth::hasAnyRole('myapp-frontend', ['myapp-frontend-role1', 'myapp-frontend-role3']) // true
Auth::hasAnyRole('myapp-backend', ['myapp-frontend-role1', 'myapp-frontend-role2']) // false

作用域

示例解码有效负载

{
    "scope": "scope-a scope-b scope-c",
}

scopes() 获取所有用户作用域

array:3 [
  0 => "scope-a"
  1 => "scope-b"
  2 => "scope-c"
]

hasScope('some-scope') 检查已验证用户是否具有作用域

Auth::hasScope('scope-a') // true
Auth::hasScope('scope-d') // false

hasAnyScope(['scope-a', 'scope-c']) 检查已验证用户是否具有任何作用域

Auth::hasAnyScope(['scope-a', 'scope-c']) // true
Auth::hasAnyScope(['scope-a', 'scope-d']) // true
Auth::hasAnyScope(['scope-f', 'scope-k']) // false

在测试中充当 Keycloak 用户

与 Laravel 中的 $this->actingAs($user) 功能类似,您可以使用此包在测试类中使用 KeycloakGuard\ActingAsKeycloakUser 特性,然后使用 actingAsKeycloakUser() 方法作为用户,并跳过 Keycloak 身份验证。

use KeycloakGuard\ActingAsKeycloakUser;

public test_a_protected_route()
{
    $this->actingAsKeycloakUser()
        ->getJson('/api/somewhere')
        ->assertOk();
}

如果您未使用 keycloak.load_user_from_database 选项,请将 keycloak.preferred_username 设置为有效的 preferred_username 以进行测试。

您还可以通过传递有效负载数组作为第二个参数来指定对令牌有效负载的精确期望。

use KeycloakGuard\ActingAsKeycloakUser;

public test_a_protected_route()
{
    $this->actingAsKeycloakUser($user, [
        'aud' => 'account',
        'exp' => 1715926026,
        'iss' => 'https://:8443/realms/master'
    ])->getJson('/api/somewhere')
      ->assertOk();
}

$user 参数接受一个字符串标识符或一个 Eloquent 模型,其标识符应与 user_provider_credential 配置中引用的属性相同。无论您在有效载荷中传递什么,都将覆盖默认声明,包括 audiatexpissazpresource_access 以及根据 token_principal_attribute 配置,是 sub 还是 preferred_username

或者,可以将有效载荷提供为类属性,这样就可以在多个测试中重用它。

use KeycloakGuard\ActingAsKeycloakUser;

protected $tokenPayload = [
    'aud' => 'account',
    'exp' => 1715926026,
    'iss' => 'https://:8443/realms/master'
];

public test_a_protected_route()
{
    $payload = [
        'exp' => 1715914352
    ];
    $this->actingAsKeycloakUser($user, $payload)
        ->getJson('/api/somewhere')
        ->assertOk();
}

优先考虑作为参数传递的声明,因此它们将覆盖类属性中的声明。$user 参数相对于 token_principal_attribute 配置中引用的声明具有最高优先级。

贡献

您可以使用 VSCODE 的 Remote Container 运行此项目。请确保您将使用内部 VSCODE 终端(在运行的容器内)。

composer install
composer test
composer test:coverage

联系

Twitter @robsontenorio