Skip to content

Add initial implementation of OAuth2 server components#875

Merged
huangdijia merged 13 commits intofriendsofhyperf:mainfrom
zds-s:patch-oauth2-server
Jul 18, 2025
Merged

Add initial implementation of OAuth2 server components#875
huangdijia merged 13 commits intofriendsofhyperf:mainfrom
zds-s:patch-oauth2-server

Conversation

@zds-s
Copy link
Copy Markdown
Contributor

@zds-s zds-s commented Jul 15, 2025

Summary by CodeRabbit

新功能

  • 集成 OAuth2 服务器组件,支持多种授权方式(授权码、客户端凭证、密码、刷新令牌、隐式)及设备码授权。
  • 提供丰富的命令行工具,包括客户端创建、删除、更新,密钥对生成,过期令牌清理及客户端列表查看。
  • 新增 OAuth2 相关数据库迁移、配置文件、实体、模型、事件、监听器及中间件,支持多存储后端和灵活配置。
  • 支持自定义作用域、客户端和用户管理,增强扩展性和集成能力。
  • 引入基于事件的授权和令牌请求处理,支持更灵活的业务逻辑扩展。
  • 增加密钥生成支持多种算法及密钥保护选项。

文档

  • 新增 OAuth2 服务器模块 README 及 MIT 许可证说明。

测试

  • 增加命令行工具的单元测试,覆盖客户端管理、密钥生成、令牌清理、列表及更新等核心功能。

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jul 15, 2025

## Walkthrough

本次变更为项目引入了完整的 OAuth2 服务器组件,包括依赖配置、数据库迁移、核心实体、模型、命令行工具、控制器、事件、工厂、仓储、管理器、中间件、值对象、枚举、转换器、监听器及相关测试。通过 composer.json 配置,集成了 league/oauth2-server 及相关依赖,并实现了与 Hyperf 框架的深度集成。

## Changes

| 文件/分组                                                         | 变更摘要                                                         |
|-------------------------------------------------------------------|------------------------------------------------------------------|
| composer.json, src/oauth2-server/composer.json                     | 添加 OAuth2 相关依赖、autoload 配置、包元数据及 Hyperf 配置注册   |
| src/oauth2-server/.gitattributes, LICENSE, README.md               | 新增归档忽略、MIT 协议、README                                   |
| src/oauth2-server/databases/..._create_oauth_server.php            | 新增 OAuth2 相关表的数据库迁移脚本                               |
| src/oauth2-server/publish/oauth2-server.php                        | 新增 OAuth2 服务器配置文件,包含密钥、授权、作用域等配置         |
| src/oauth2-server/src/ConfigProvider.php                           | 新增配置提供者,注册命令、依赖、发布资源                         |
| src/oauth2-server/src/Controller/*.php                             | 新增授权与令牌控制器                                             |
| src/oauth2-server/src/Command/*.php                                | 新增清理、创建、删除、列出、更新客户端及生成密钥对等命令         |
| src/oauth2-server/src/Entity/*.php                                 | 新增 OAuth2 实体类(Token、Client、User、Scope、DeviceCode 等)  |
| src/oauth2-server/src/Enums/*.php                                  | 新增加密类型、授权类型、设备码状态枚举                           |
| src/oauth2-server/src/Event/*.php                                  | 新增事件类(授权请求解析、Token 请求解析、Scope 解析、用户解析等)|
| src/oauth2-server/src/Factory/*.php                                | 新增授权服务器、资源服务器、配置工厂                             |
| src/oauth2-server/src/Interfaces/*.php                             | 新增配置接口、安全接口                                           |
| src/oauth2-server/src/Listener/*.php                               | 新增启动监听器,自动配置迁移路径和作用域                         |
| src/oauth2-server/src/Manager/*.php                                | 新增访问令牌、授权码、客户端、刷新令牌、作用域、设备码管理器接口及实现 |
| src/oauth2-server/src/Manager/EloquentORM/*.php                    | 新增基于 Eloquent ORM 的实体管理器实现                           |
| src/oauth2-server/src/Manager/InMemory/ScopeManager.php            | 新增内存作用域管理器实现                                         |
| src/oauth2-server/src/Middleware/ResourceServerMiddleware.php      | 新增资源服务器中间件                                             |
| src/oauth2-server/src/Model/*.php                                  | 新增 OAuth2 数据模型及接口、属性转换器                           |
| src/oauth2-server/src/Repository/*.php                             | 新增 OAuth2 相关仓储实现                                         |
| src/oauth2-server/src/ValueObject/*.php                            | 新增 Grant、RedirectUri、Scope 等值对象                          |
| src/oauth2-server/src/Converter/*.php                              | 新增客户端转换器、作用域转换器及接口                             |
| tests/Oauth2Server/Command/*.php                                   | 新增命令行相关单元测试                                           |
| tests/Oauth2Server/InMemoryScopeManagerTest.php                    | 新增内存作用域管理器测试                                         |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant 用户
    participant 控制器
    participant 授权服务器
    participant 事件分发器
    participant 仓储/管理器
    participant 数据库

    用户->>控制器: 发起授权/令牌请求
    控制器->>授权服务器: 转发请求
    授权服务器->>仓储/管理器: 校验客户端/用户/作用域
    仓储/管理器->>数据库: 查询/保存实体
    授权服务器->>事件分发器: 触发事件(如授权解析、Token 解析)
    事件分发器-->>授权服务器: 监听器处理,可能修改响应
    授权服务器->>控制器: 返回响应
    控制器->>用户: 返回授权/令牌结果

Poem

兔子挥爪写新篇,OAuth2 服务器上线前。
控制器、命令、事件全,
数据模型巧如仙。
迁移脚本布新表,
密钥生成命令妙。
测试齐全笑开颜,
代码跃动似春天!

 (_/)
( •_•)
/ >🍃


<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 PHPStan (2.1.17)</summary>
> 
> Note: Using configuration file /phpstan.neon.dist.
> At least one path must be specified to analyse.
> 
> 
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=friendsofhyperf/components&utm_content=875):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@zds-s zds-s marked this pull request as draft July 15, 2025 11:39
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 40

♻️ Duplicate comments (1)
src/oauth2-server/src/Model/AuthorizationCode.php (1)

91-95: getClient 方法存在相同问题

AccessToken 模型中的相同问题,firstOrFail() 可能抛出异常。建议采用一致的异常处理方式。

🧹 Nitpick comments (21)
src/oauth2-server/README.md (1)

1-2: 补充 README 的使用与配置指引

当前 README 仅有标题,无法帮助使用者快速了解组件的安装、配置步骤及常见授权流程示例。建议至少补充以下内容:

  • 安装方式 (composer require …)
  • 数据库迁移与配置发布命令
  • 授权码 / 密码 / 客户端凭证等典型流程示例
  • 生成密钥对、创建客户端等 CLI 指令说明
  • 与 Hyperf 框架集成的配置片段

这将极大提升可用性与社区贡献度。

src/oauth2-server/src/Interfaces/SecurityInterface.php (1)

14-18: 避免命名冲突,建议调整别名或接口命名

文件同时存在 FriendsOfHyperf\Oauth2\Server\Model\UserInterfaceLeague\OAuth2\Server\Entities\UserEntityInterface,这里将后者直接 as UserInterface 容易造成阅读与类型提示混淆。

-use League\OAuth2\Server\Entities\UserEntityInterface as UserInterface;
+use League\OAuth2\Server\Entities\UserEntityInterface as OAuthUserInterface;
...
-interface SecurityInterface
-{
-    public function getUser(): ?UserInterface;
-}
+interface SecurityInterface
+{
+    public function getUser(): ?OAuthUserInterface;
+}

或考虑把当前接口重命名为 SecurityUserResolverInterface 等更具语义的名称,以进一步降低歧义。

src/oauth2-server/src/Event/AuthorizationRequestResolveEventFactory.php (1)

22-30: 考虑改进trait使用模式的清晰度

当前实现将依赖项分配给来自trait的属性,这降低了代码的可读性和可维护性。建议在构造函数参数中添加可见性修饰符,或者考虑更明确的实现方式。

建议改进实现:

     public function __construct(
-        SecurityInterface $security,
-        ClientManagerInterface $clientManager,
-        ScopeConverterInterface $scopeConverter
+        private SecurityInterface $security,
+        private ClientManagerInterface $clientManager, 
+        private ScopeConverterInterface $scopeConverter
     ) {
-        $this->clientManager = $clientManager;
-        $this->scopeConverter = $scopeConverter;
-        $this->security = $security;
     }

或者,如果trait需要特定的属性名称,请考虑添加文档说明这些属性的来源。

src/oauth2-server/src/Manager/InMemory/ScopeManager.php (1)

24-26: 考虑移除空的构造函数

空的构造函数没有提供任何功能,可以考虑移除以简化代码。

-    public function __construct()
-    {
-    }
src/oauth2-server/src/Repository/UserRepository.php (1)

54-58: 简化条件逻辑

当前的 null 检查和返回逻辑可以简化。

-        if ($user === null) {
-            return null;
-        }
-
-        return $user;
+        return $user;
src/oauth2-server/publish/oauth2-server.php (2)

32-33: 修复注释中的截断 URL

第 32 行的注释包含截断的 URL,这可能会误导开发者。

-        # The value should be a valid interval: http://ph
+        # The value should be a valid interval: http://php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters

65-68: 添加默认作用域配置指导

空的作用域配置可能需要更明确的文档说明如何配置。

     'scopes' => [
         # Scopes that you wish to utilize in your application.
         # This should be a simple array of strings.
+        # Example: ['read', 'write', 'admin']
         'available' => [],
         # Scopes that will be assigned when no scope given.
         # This should be a simple array of strings.
+        # Example: ['read']
         'default' => [],
     ],
src/oauth2-server/src/Listener/LeagueOAuth2ServerListener.php (1)

38-42: 移除未使用的参数或添加使用说明

process 方法声明了 $event 参数但未使用。虽然这是接口要求的,但建议添加注释说明或使用 @SuppressWarnings 注解来抑制静态分析警告。

-public function process(object $event): void
+/**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+public function process(object $event): void
src/oauth2-server/src/Event/AuthorizationRequestResolveEventFactoryTrait.php (1)

20-23: @internal 注释与实际使用不一致

此 trait 标记为 @internal,但被公共类 AuthorizationRequestResolveEventFactory 使用。这可能会造成混淆。

建议移除 @internal 注释,或将使用此 trait 的工厂类也标记为 internal。

src/oauth2-server/src/Controller/TokenController.php (1)

29-29: 考虑使用更描述性的方法名

方法名 index 对于OAuth2令牌端点来说不够描述性。建议使用更明确的名称,如 issueTokenhandleTokenRequest

src/oauth2-server/src/Command/ListClientsCommand.php (1)

90-98: 可以优化数组映射逻辑

当前的数组映射使用静态匿名函数,可以简化为更简洁的形式。

-            ->addGrantCriteria(...array_map(static function (string $grant): Grant {
-                return new Grant($grant);
-            }, $grantStrings))
-            ->addRedirectUriCriteria(...array_map(static function (string $redirectUri): RedirectUri {
-                return new RedirectUri($redirectUri);
-            }, $redirectUriStrings))
-            ->addScopeCriteria(...array_map(static function (string $scope): Scope {
-                return new Scope($scope);
-            }, $scopeStrings));
+            ->addGrantCriteria(...array_map(Grant::class, $grantStrings))
+            ->addRedirectUriCriteria(...array_map(RedirectUri::class, $redirectUriStrings))
+            ->addScopeCriteria(...array_map(Scope::class, $scopeStrings));
src/oauth2-server/src/Entity/Client.php (1)

33-36: 数组参数类型注释不一致

PHPDoc 注释声明参数为 string[],但方法参数类型为 array。建议保持一致性。

    /**
-     * @param string[] $redirectUri
+     * @param array<string> $redirectUri
     */
    public function setRedirectUri(array $redirectUri): void
src/oauth2-server/src/Factory/ResourceServerFactory.php (1)

38-38: 可能的容器污染

BearerTokenValidator 实例设置回容器中可能会导致意外的副作用,特别是在多次调用工厂方法时。

-        $this->container->set(BearerTokenValidator::class, $bearerTokenValidator);
+        // 移除容器设置,让容器管理实例生命周期
src/oauth2-server/src/ConfigProvider.php (1)

88-88: 修正注释中的拼写错误

将 "factroy" 改为 "factory"。

-// factroy
+// factory
src/oauth2-server/src/Command/CreateClientCommand.php (1)

119-119: 考虑使用更安全的标识符生成方法

使用 MD5 哈希生成客户端标识符可能不是最佳选择。建议使用更现代的方法。

-$identifier = (string) $input->getArgument('identifier') ?: hash('md5', random_bytes(16));
+$identifier = (string) $input->getArgument('identifier') ?: bin2hex(random_bytes(16));
src/oauth2-server/src/Command/UpdateClientCommand.php (1)

105-125: 集合更新逻辑实现巧妙!

通过过滤和合并的方式处理添加/删除操作,验证了冲突的值。第 121 行的注释应该是 @var

-        /* @var list<T> */
+        /** @var list<T> */
src/oauth2-server/src/Model/ClientInterface.php (1)

60-60: 考虑将工厂方法移至独立的工厂类

在接口中定义实例工厂方法 newClientInstance 不太常见。建议考虑将此方法移至专门的工厂类或使用静态工厂方法。

src/oauth2-server/src/Repository/ScopeRepository.php (2)

55-77: finalizeScopes 方法需要处理未使用的参数

方法实现整体正确,但是 $authCodeId 参数未被使用。如果该参数是为了符合接口定义,建议添加注释说明;如果确实不需要,可以考虑移除。

 public function finalizeScopes(
     array $scopes,
     string $grantType,
     ClientEntityInterface $clientEntity,
-    string|int|null $userIdentifier = null,
-    ?string $authCodeId = null,
+    string|int|null $userIdentifier = null,
+    ?string $authCodeId = null, // 参数保留以符合接口定义
 ): array {

85-110: setupScopes 方法逻辑正确但可以优化

方法正确实现了 scope 验证逻辑,但可以考虑性能优化。当前实现在每次循环中都调用 array_mapin_array,可以预先计算。

 private function setupScopes(ClientInterface $client, array $requestedScopes): array
 {
     $clientScopes = $client->getScopes();
 
     if (empty($clientScopes)) {
         return $requestedScopes;
     }
 
     if (empty($requestedScopes)) {
         return $clientScopes;
     }
 
+    $clientScopesAsStrings = array_map('strval', $clientScopes);
     $finalizedScopes = [];
-    $clientScopesAsStrings = array_map('strval', $clientScopes);
 
     foreach ($requestedScopes as $requestedScope) {
         $requestedScopeAsString = (string) $requestedScope;
         if (! \in_array($requestedScopeAsString, $clientScopesAsStrings, true)) {
             throw OAuthServerException::invalidScope($requestedScopeAsString);
         }
 
         $finalizedScopes[] = $requestedScope;
     }
 
     return $finalizedScopes;
 }
src/oauth2-server/src/Event/AuthorizationRequestResolveEvent.php (1)

91-94: 方法名称可能引起混淆

isAuthorizationApproved() 方法直接委托给 $authorizationRequest,但这可能与当前事件的 $authorizationResolution 状态不一致。建议明确区分原始请求状态和事件解析状态。

考虑重命名方法或添加文档说明:

+    /**
+     * Returns the original authorization approval status from the request.
+     * This may differ from the current event's authorization resolution.
+     */
     public function isAuthorizationApproved(): bool
     {
         return $this->authorizationRequest->isAuthorizationApproved();
     }
src/oauth2-server/src/Factory/AuthorizationServerFactory.php (1)

86-97: 潜在的容器覆盖问题

在启用授权码授权时,代码先使用 make() 创建实例,然后使用 set() 将其存储回容器。这种模式在其他授权类型(如隐式授权)中也有使用,但对于密码授权和客户端凭据授权却没有。建议保持一致性。

考虑统一处理所有授权类型的实例化和容器注册方式,或者在注释中解释为什么某些授权类型需要特殊处理。

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0cb04db and 694be89.

📒 Files selected for processing (72)
  • composer.json (5 hunks)
  • src/oauth2-server/.gitattributes (1 hunks)
  • src/oauth2-server/LICENSE (1 hunks)
  • src/oauth2-server/README.md (1 hunks)
  • src/oauth2-server/composer.json (1 hunks)
  • src/oauth2-server/databases/2025_07_15_044853_create_oauth_server.php (1 hunks)
  • src/oauth2-server/publish/oauth2-server.php (1 hunks)
  • src/oauth2-server/src/Command/ClearExpiredTokensCommand.php (1 hunks)
  • src/oauth2-server/src/Command/CreateClientCommand.php (1 hunks)
  • src/oauth2-server/src/Command/DeleteClientCommand.php (1 hunks)
  • src/oauth2-server/src/Command/GenerateKeyPairCommand.php (1 hunks)
  • src/oauth2-server/src/Command/ListClientsCommand.php (1 hunks)
  • src/oauth2-server/src/Command/UpdateClientCommand.php (1 hunks)
  • src/oauth2-server/src/ConfigProvider.php (1 hunks)
  • src/oauth2-server/src/Controller/AuthorizationController.php (1 hunks)
  • src/oauth2-server/src/Controller/TokenController.php (1 hunks)
  • src/oauth2-server/src/Converter/ScopeConverter.php (1 hunks)
  • src/oauth2-server/src/Converter/ScopeConverterInterface.php (1 hunks)
  • src/oauth2-server/src/Entity/AccessToken.php (1 hunks)
  • src/oauth2-server/src/Entity/AuthCode.php (1 hunks)
  • src/oauth2-server/src/Entity/Client.php (1 hunks)
  • src/oauth2-server/src/Entity/RefreshToken.php (1 hunks)
  • src/oauth2-server/src/Entity/Scope.php (1 hunks)
  • src/oauth2-server/src/Entity/User.php (1 hunks)
  • src/oauth2-server/src/Enums/EncryptionKeyType.php (1 hunks)
  • src/oauth2-server/src/Enums/OAuth2Grants.php (1 hunks)
  • src/oauth2-server/src/Event/AuthorizationRequestResolveEvent.php (1 hunks)
  • src/oauth2-server/src/Event/AuthorizationRequestResolveEventFactory.php (1 hunks)
  • src/oauth2-server/src/Event/AuthorizationRequestResolveEventFactoryTrait.php (1 hunks)
  • src/oauth2-server/src/Event/PreSaveClientEvent.php (1 hunks)
  • src/oauth2-server/src/Event/ScopeResolveEvent.php (1 hunks)
  • src/oauth2-server/src/Event/TokenRequestResolveEvent.php (1 hunks)
  • src/oauth2-server/src/Event/UserResolveEvent.php (1 hunks)
  • src/oauth2-server/src/Factory/AuthorizationServerFactory.php (1 hunks)
  • src/oauth2-server/src/Factory/ConfigFactory.php (1 hunks)
  • src/oauth2-server/src/Factory/ResourceServerFactory.php (1 hunks)
  • src/oauth2-server/src/Interfaces/ConfigInterface.php (1 hunks)
  • src/oauth2-server/src/Interfaces/SecurityInterface.php (1 hunks)
  • src/oauth2-server/src/Listener/LeagueOAuth2ServerListener.php (1 hunks)
  • src/oauth2-server/src/Manager/AccessTokenManagerInterface.php (1 hunks)
  • src/oauth2-server/src/Manager/AuthorizationCodeManagerInterface.php (1 hunks)
  • src/oauth2-server/src/Manager/ClientFilter.php (1 hunks)
  • src/oauth2-server/src/Manager/ClientManagerInterface.php (1 hunks)
  • src/oauth2-server/src/Manager/ElquentORM/AccessTokenManager.php (1 hunks)
  • src/oauth2-server/src/Manager/ElquentORM/AuthorizationCodeManager.php (1 hunks)
  • src/oauth2-server/src/Manager/ElquentORM/ClientManager.php (1 hunks)
  • src/oauth2-server/src/Manager/ElquentORM/RefreshTokenManager.php (1 hunks)
  • src/oauth2-server/src/Manager/InMemory/ScopeManager.php (1 hunks)
  • src/oauth2-server/src/Manager/RefreshTokenManagerInterface.php (1 hunks)
  • src/oauth2-server/src/Manager/ScopeManagerInterface.php (1 hunks)
  • src/oauth2-server/src/Middleware/ResourceServerMiddleware.php (1 hunks)
  • src/oauth2-server/src/Model/AccessToken.php (1 hunks)
  • src/oauth2-server/src/Model/AccessTokenInterface.php (1 hunks)
  • src/oauth2-server/src/Model/AuthorizationCode.php (1 hunks)
  • src/oauth2-server/src/Model/AuthorizationCodeInterface.php (1 hunks)
  • src/oauth2-server/src/Model/Casts/GrantsCast.php (1 hunks)
  • src/oauth2-server/src/Model/Casts/RedirectsCast.php (1 hunks)
  • src/oauth2-server/src/Model/Casts/ScopesCast.php (1 hunks)
  • src/oauth2-server/src/Model/Client.php (1 hunks)
  • src/oauth2-server/src/Model/ClientInterface.php (1 hunks)
  • src/oauth2-server/src/Model/RefreshToken.php (1 hunks)
  • src/oauth2-server/src/Model/RefreshTokenInterface.php (1 hunks)
  • src/oauth2-server/src/Model/UserInterface.php (1 hunks)
  • src/oauth2-server/src/Repository/AccessTokenRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/AuthCodeRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/ClientRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/RefreshTokenRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/ScopeRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/UserRepository.php (1 hunks)
  • src/oauth2-server/src/ValueObject/Grant.php (1 hunks)
  • src/oauth2-server/src/ValueObject/RedirectUri.php (1 hunks)
  • src/oauth2-server/src/ValueObject/Scope.php (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (19)
src/oauth2-server/src/Entity/Scope.php (1)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/ValueObject/Scope.php (4)
src/oauth2-server/src/Entity/Scope.php (1)
  • Scope (18-22)
src/oauth2-server/src/Model/AccessTokenInterface.php (1)
  • __toString (19-19)
src/oauth2-server/src/ValueObject/Grant.php (1)
  • __toString (21-24)
src/oauth2-server/src/ValueObject/RedirectUri.php (1)
  • __toString (26-29)
src/oauth2-server/src/Manager/ScopeManagerInterface.php (6)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Manager/AccessTokenManagerInterface.php (2)
  • find (18-18)
  • save (20-20)
src/oauth2-server/src/Manager/AuthorizationCodeManagerInterface.php (2)
  • find (18-18)
  • save (20-20)
src/oauth2-server/src/Manager/ClientManagerInterface.php (2)
  • find (22-22)
  • save (18-18)
src/oauth2-server/src/Manager/InMemory/ScopeManager.php (2)
  • find (28-31)
  • save (33-36)
src/oauth2-server/src/Manager/RefreshTokenManagerInterface.php (2)
  • find (18-18)
  • save (20-20)
src/oauth2-server/src/Interfaces/ConfigInterface.php (1)
src/oauth2-server/src/Factory/ConfigFactory.php (1)
  • get (24-27)
src/oauth2-server/src/Listener/LeagueOAuth2ServerListener.php (6)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Factory/ConfigFactory.php (2)
  • __construct (19-22)
  • get (24-27)
src/oauth2-server/src/Interfaces/ConfigInterface.php (1)
  • get (16-16)
src/oauth2-server/src/Manager/ElquentORM/AuthorizationCodeManager.php (1)
  • save (26-29)
src/oauth2-server/src/Manager/InMemory/ScopeManager.php (1)
  • save (33-36)
src/oauth2-server/src/Manager/ScopeManagerInterface.php (1)
  • save (20-20)
src/oauth2-server/src/Manager/ClientManagerInterface.php (2)
src/oauth2-server/src/Manager/ElquentORM/ClientManager.php (4)
  • save (31-37)
  • remove (39-42)
  • find (44-47)
  • list (52-66)
src/oauth2-server/src/Manager/ClientFilter.php (1)
  • ClientFilter (18-98)
src/oauth2-server/src/Manager/ElquentORM/RefreshTokenManager.php (5)
src/oauth2-server/src/Model/RefreshToken.php (1)
  • RefreshToken (29-83)
src/oauth2-server/src/Manager/ElquentORM/AccessTokenManager.php (3)
  • find (27-33)
  • save (35-41)
  • clearExpired (43-51)
src/oauth2-server/src/Manager/ElquentORM/AuthorizationCodeManager.php (3)
  • find (21-24)
  • save (26-29)
  • clearExpired (31-36)
src/oauth2-server/src/Manager/RefreshTokenManagerInterface.php (3)
  • find (18-18)
  • save (20-20)
  • clearExpired (22-22)
src/helpers/src/Functions.php (1)
  • now (371-374)
src/oauth2-server/src/Model/UserInterface.php (8)
src/oauth2-server/src/Model/AccessTokenInterface.php (1)
  • getIdentifier (21-21)
src/oauth2-server/src/Model/AuthorizationCode.php (1)
  • getIdentifier (76-79)
src/oauth2-server/src/Model/AuthorizationCodeInterface.php (1)
  • getIdentifier (21-21)
src/oauth2-server/src/Model/AccessToken.php (1)
  • getIdentifier (71-74)
src/oauth2-server/src/Model/RefreshToken.php (1)
  • getIdentifier (57-60)
src/oauth2-server/src/Model/Client.php (1)
  • getIdentifier (59-62)
src/oauth2-server/src/Model/RefreshTokenInterface.php (1)
  • getIdentifier (20-20)
src/oauth2-server/src/Model/ClientInterface.php (1)
  • getIdentifier (23-23)
src/oauth2-server/databases/2025_07_15_044853_create_oauth_server.php (2)
src/oauth2-server/src/Controller/AuthorizationController.php (1)
  • index (32-56)
src/oauth2-server/src/Controller/TokenController.php (1)
  • index (29-43)
src/oauth2-server/src/Converter/ScopeConverterInterface.php (3)
src/oauth2-server/src/Entity/Scope.php (1)
  • Scope (18-22)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Converter/ScopeConverter.php (4)
  • toDomain (20-23)
  • toDomainArray (30-35)
  • toLeague (37-43)
  • toLeagueArray (50-55)
src/oauth2-server/src/Model/AccessTokenInterface.php (8)
src/oauth2-server/src/ValueObject/Scope.php (2)
  • Scope (14-25)
  • __toString (21-24)
src/oauth2-server/src/Model/AuthorizationCode.php (7)
  • __toString (66-69)
  • getIdentifier (76-79)
  • getUserIdentifier (86-89)
  • getClient (91-95)
  • getScopes (97-100)
  • isRevoked (102-105)
  • revoke (107-112)
src/oauth2-server/src/Model/AuthorizationCodeInterface.php (7)
  • __toString (19-19)
  • getIdentifier (21-21)
  • getUserIdentifier (25-25)
  • getClient (27-27)
  • getScopes (32-32)
  • isRevoked (34-34)
  • revoke (36-36)
src/oauth2-server/src/Model/AccessToken.php (8)
  • __toString (61-64)
  • getIdentifier (71-74)
  • getExpiry (76-79)
  • getUserIdentifier (81-84)
  • getClient (86-90)
  • getScopes (92-95)
  • isRevoked (97-100)
  • revoke (102-107)
src/oauth2-server/src/Model/RefreshToken.php (5)
  • __toString (47-50)
  • getIdentifier (57-60)
  • getExpiry (62-65)
  • isRevoked (72-75)
  • revoke (77-82)
src/oauth2-server/src/Model/RefreshTokenInterface.php (5)
  • __toString (18-18)
  • getIdentifier (20-20)
  • getExpiry (22-22)
  • isRevoked (26-26)
  • revoke (28-28)
src/oauth2-server/src/Model/ClientInterface.php (2)
  • getIdentifier (23-23)
  • getScopes (46-46)
src/oauth2-server/src/Model/UserInterface.php (1)
  • getIdentifier (16-16)
src/oauth2-server/src/Manager/ClientFilter.php (5)
src/oauth2-server/src/ValueObject/Grant.php (1)
  • Grant (14-25)
src/oauth2-server/src/ValueObject/RedirectUri.php (1)
  • RedirectUri (16-30)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Model/Client.php (3)
  • getGrants (85-88)
  • getRedirectUris (74-77)
  • getScopes (96-99)
src/oauth2-server/src/Model/ClientInterface.php (3)
  • getGrants (39-39)
  • getRedirectUris (32-32)
  • getScopes (46-46)
src/oauth2-server/src/Converter/ScopeConverter.php (3)
src/oauth2-server/src/Entity/Scope.php (1)
  • Scope (18-22)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Converter/ScopeConverterInterface.php (4)
  • toDomain (20-20)
  • toDomainArray (27-27)
  • toLeague (29-29)
  • toLeagueArray (36-36)
src/oauth2-server/src/Manager/ElquentORM/AccessTokenManager.php (3)
src/oauth2-server/src/Model/AccessToken.php (1)
  • AccessToken (33-108)
src/helpers/src/Functions.php (1)
  • now (371-374)
src/oauth2-server/src/Interfaces/ConfigInterface.php (1)
  • get (16-16)
src/oauth2-server/src/Model/RefreshTokenInterface.php (3)
src/oauth2-server/src/Model/AccessTokenInterface.php (5)
  • __toString (19-19)
  • getIdentifier (21-21)
  • getExpiry (23-23)
  • isRevoked (34-34)
  • revoke (36-36)
src/oauth2-server/src/Model/AuthorizationCodeInterface.php (4)
  • __toString (19-19)
  • getIdentifier (21-21)
  • isRevoked (34-34)
  • revoke (36-36)
src/oauth2-server/src/Model/RefreshToken.php (6)
  • __toString (47-50)
  • getIdentifier (57-60)
  • getExpiry (62-65)
  • getAccessToken (67-70)
  • isRevoked (72-75)
  • revoke (77-82)
src/oauth2-server/src/Manager/ElquentORM/ClientManager.php (10)
src/oauth2-server/src/Event/PreSaveClientEvent.php (3)
  • PreSaveClientEvent (17-33)
  • __construct (19-22)
  • getClient (24-27)
src/oauth2-server/src/Manager/ClientFilter.php (5)
  • ClientFilter (18-98)
  • hasFilters (91-97)
  • getGrants (70-73)
  • getRedirectUris (78-81)
  • getScopes (86-89)
src/oauth2-server/src/Model/Client.php (4)
  • Client (36-142)
  • getGrants (85-88)
  • getRedirectUris (74-77)
  • getScopes (96-99)
src/oauth2-server/src/ValueObject/Grant.php (1)
  • Grant (14-25)
src/oauth2-server/src/ValueObject/RedirectUri.php (1)
  • RedirectUri (16-30)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/compoships/src/Database/Query/Builder.php (1)
  • whereIn (18-38)
src/oauth2-server/src/Model/Casts/GrantsCast.php (1)
  • get (20-31)
src/oauth2-server/src/Model/Casts/RedirectsCast.php (1)
  • get (20-31)
src/oauth2-server/src/Model/Casts/ScopesCast.php (1)
  • get (20-31)
src/oauth2-server/src/Repository/RefreshTokenRepository.php (6)
src/oauth2-server/src/Entity/RefreshToken.php (1)
  • RefreshToken (18-22)
src/oauth2-server/src/Model/RefreshToken.php (6)
  • RefreshToken (29-83)
  • getIdentifier (57-60)
  • revoke (77-82)
  • isRevoked (72-75)
  • accessToken (52-55)
  • getAccessToken (67-70)
src/oauth2-server/src/Manager/AccessTokenManagerInterface.php (2)
  • find (18-18)
  • save (20-20)
src/oauth2-server/src/Manager/RefreshTokenManagerInterface.php (2)
  • find (18-18)
  • save (20-20)
src/oauth2-server/src/Model/AccessTokenInterface.php (3)
  • getIdentifier (21-21)
  • revoke (36-36)
  • isRevoked (34-34)
src/oauth2-server/src/Model/RefreshTokenInterface.php (4)
  • getIdentifier (20-20)
  • revoke (28-28)
  • isRevoked (26-26)
  • getAccessToken (24-24)
src/oauth2-server/src/Event/AuthorizationRequestResolveEvent.php (6)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Controller/AuthorizationController.php (1)
  • __construct (24-30)
src/oauth2-server/src/Event/TokenRequestResolveEvent.php (3)
  • __construct (19-22)
  • getResponse (24-27)
  • setResponse (29-32)
src/oauth2-server/src/Event/ScopeResolveEvent.php (3)
  • __construct (20-26)
  • getClient (43-46)
  • getScopes (28-31)
src/oauth2-server/src/Interfaces/SecurityInterface.php (1)
  • getUser (18-18)
src/oauth2-server/src/Model/ClientInterface.php (1)
  • getScopes (46-46)
src/oauth2-server/src/Factory/AuthorizationServerFactory.php (4)
src/oauth2-server/src/Factory/ConfigFactory.php (2)
  • __construct (19-22)
  • get (24-27)
src/oauth2-server/src/Factory/ResourceServerFactory.php (2)
  • __construct (22-27)
  • __invoke (29-44)
src/oauth2-server/src/ConfigProvider.php (1)
  • __invoke (58-102)
src/oauth2-server/src/Interfaces/ConfigInterface.php (1)
  • get (16-16)
🪛 PHPMD (2.15.0)
src/oauth2-server/src/Listener/LeagueOAuth2ServerListener.php

38-38: Avoid unused parameters such as '$event'. (Unused Code Rules)

(UnusedFormalParameter)

src/oauth2-server/src/Model/Casts/ScopesCast.php

20-20: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)

src/oauth2-server/src/Model/Casts/GrantsCast.php

20-20: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)

src/oauth2-server/src/Model/Casts/RedirectsCast.php

20-20: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)

src/oauth2-server/src/Repository/ScopeRepository.php

60-60: Avoid unused parameters such as '$authCodeId'. (Unused Code Rules)

(UnusedFormalParameter)

🔇 Additional comments (61)
src/oauth2-server/.gitattributes (1)

1-4: 慎重忽略 tests 目录以避免发布包缺少示例

tests 目录标记为 export-ignore 会导致通过 composer install --prefer-dist 获得的发布包缺少测试用例与示例,降低二次开发者参考价值。若体积并不大,可考虑仅忽略大型 fixture,而保留核心测试文件。

src/oauth2-server/LICENSE (1)

3-4: 确认版权年份与主体信息的一致性

许可证中的年份为 2023,而本 PR 创建于 2025,且作者署名 D.J.Hwang 与仓库所有者 zds-s 不一致。请确认:

  1. 年份是否需要更新为当前年份或范围(2023-2025)。
  2. 署名是否应为本项目实际版权主体,避免潜在法律歧义。
src/oauth2-server/src/Enums/EncryptionKeyType.php (1)

14-18: 补充注释说明各枚举含义并考虑命名风格统一

  • 建议在每个 case 上方增加一行注释,说明 plaindefuse 两种模式的适用场景,便于 IDE 悬浮提示。
  • 项目其它枚举(若有)若使用 UPPER_SNAKE 命名风格,可保持一致性,如 PLAINDEFUSE,以提升可读性。
    [ suggest_nitpick ]
src/oauth2-server/src/Model/UserInterface.php (1)

14-17: 接口设计一致性良好 👍

方法签名与其它实体接口 (AccessTokenInterface 等) 保持一致,方便在仓库或服务中做统一处理。

src/oauth2-server/src/Entity/User.php (1)

14-20: 实现简洁,符合 League 接口要求

类只需要 EntityTrait 即可满足 UserEntityInterfaceidentifier 要求,设计合理。

src/oauth2-server/src/Entity/AuthCode.php (1)

14-24: 实现符合 OAuth2 规范

组合三大 trait 已涵盖 AuthCode 所需字段与行为,代码整洁。

src/oauth2-server/src/Entity/Scope.php (1)

18-22: 实现正确,遵循了 OAuth2 服务器实体的最佳实践。

该类正确实现了 ScopeEntityInterface 接口并使用了合适的 traits,结构简洁明了。使用 final 关键字也是实体类的好习惯。

src/oauth2-server/src/Entity/RefreshToken.php (1)

18-22: 实现正确,与其他实体类保持一致。

该类遵循了与 Scope 实体相同的实现模式,正确使用了 RefreshTokenEntityInterface 和相关的 traits。

src/oauth2-server/src/Interfaces/ConfigInterface.php (1)

14-17: 接口设计简洁明了,符合单一职责原则。

该接口为配置检索提供了清晰的抽象,方法签名合理,支持默认值回退机制。

src/oauth2-server/src/Manager/ScopeManagerInterface.php (1)

16-21: 接口设计合理,遵循了一致的管理器模式。

该接口提供了标准的 CRUD 操作方法,使用 ValueObject\Scope 用于业务逻辑层是合适的选择,与其他管理器接口保持一致。

src/oauth2-server/src/Entity/AccessToken.php (1)

19-24: 实现正确,适当使用了访问令牌特有的 traits。

该类正确实现了 AccessTokenEntityInterface 接口,合理使用了三个 traits,其中 TokenEntityTrait 为访问令牌提供了特定的令牌功能。

src/oauth2-server/src/Event/TokenRequestResolveEvent.php (1)

17-33: 实现看起来很好!

这个事件类设计合理,遵循了标准的事件模式。使用final关键字和严格的类型声明是好的实践。getter/setter模式允许监听器修改响应,这对于OAuth2令牌请求处理流程很有用。

src/oauth2-server/src/Factory/ConfigFactory.php (1)

17-28: 配置工厂实现优秀!

这个工厂类采用了清晰的适配器模式,通过自动添加'oauth2-server.'前缀来简化OAuth2配置的访问。使用readonly属性和构造函数注入遵循了现代PHP最佳实践。

src/oauth2-server/src/Event/PreSaveClientEvent.php (1)

17-33: 事件类实现规范!

这个客户端预保存事件类的设计与其他事件类保持一致,使用了合适的封装和类型安全。允许在保存客户端之前进行修改的设计对于实现客户端管理生命周期的钩子很有用。

src/oauth2-server/src/Manager/RefreshTokenManagerInterface.php (1)

16-23: 接口设计良好,符合标准的管理器模式。

该接口定义了刷新令牌管理的核心操作,方法签名清晰,返回类型恰当。

src/oauth2-server/src/Enums/OAuth2Grants.php (1)

14-40: 枚举实现规范,完全符合 OAuth2 标准。

每个授权类型都有对应的 RFC 文档链接,便于理解和维护。字符串类型的枚举值与 OAuth2 规范完全一致。

src/oauth2-server/src/Manager/AccessTokenManagerInterface.php (1)

16-23: 接口设计与其他管理器接口保持一致。

遵循了相同的方法模式,有利于代码的统一性和可维护性。

src/oauth2-server/src/Manager/AuthorizationCodeManagerInterface.php (1)

16-23: 接口设计延续了一致的管理器模式。

与访问令牌和刷新令牌管理器接口保持相同的方法结构,确保了 API 的一致性。

src/oauth2-server/src/Model/Casts/GrantsCast.php (1)

20-20: 静态分析的未使用参数警告是误报。

这些参数是由 CastsAttributes 接口要求的,即使在当前实现中未使用也不能移除。

Also applies to: 33-33

src/oauth2-server/src/Event/ScopeResolveEvent.php (1)

18-52: 实现正确,设计合理

事件类的设计很好:

  • 正确使用 readonly 属性用于不可变的上下文信息
  • 允许修改 scopes 数组以支持事件处理过程中的作用域解析
  • 类型声明完整且准确
src/oauth2-server/src/Model/Casts/ScopesCast.php (1)

18-42: 类型转换实现正确

ScopesCast 类正确实现了作用域数据的双向转换:

  • get 方法正确处理 JSON 字符串和数组输入
  • set 方法正确将 Scope 对象转换为字符串并编码为 JSON
  • 静态分析关于未使用参数的警告是误报,这些参数是 CastsAttributes 接口的必需部分
src/oauth2-server/src/Model/Casts/RedirectsCast.php (1)

18-42: 重定向 URI 类型转换实现正确

RedirectsCast 类正确实现了重定向 URI 数据的双向转换:

  • get 方法正确处理 JSON 字符串和数组输入
  • set 方法将单个 RedirectUri 包装在数组中,这确保了存储格式的一致性
  • 静态分析关于未使用参数的警告是误报,这些参数是接口契约的必需部分
src/oauth2-server/composer.json (2)

1-52: 整体包配置结构良好

包配置结构完整,包含了必要的元数据、依赖项和自动加载配置。作者信息、支持链接和许可证信息都已正确配置。


29-29: 确认 league/oauth2-server 版本约束

经核实,截至 2025 年 7 月中旬,league/oauth2-server 最新稳定版本为 9.2.0,composer.json 中使用的 "^9.2" 约束已涵盖该版本(>=9.2.0 <10.0.0),无需做出调整。

src/oauth2-server/src/Command/DeleteClientCommand.php (1)

19-27: 命令类实现良好

命令类结构清晰,使用了适当的依赖注入和命名规范。构造函数正确调用了父类构造函数。

src/oauth2-server/src/Repository/UserRepository.php (1)

23-29: 依赖注入实现良好

使用 readonly 属性和构造函数注入,符合现代 PHP 最佳实践。

src/oauth2-server/src/Event/UserResolveEvent.php (2)

23-29: 构造函数实现良好

使用 readonly 属性进行依赖注入,确保了事件数据的不可变性,符合事件对象的最佳实践。


31-49: getter 方法实现良好

所有 getter 方法都正确返回了相应的属性值,方法命名清晰明确。

src/oauth2-server/publish/oauth2-server.php (4)

48-48: 考虑隐式授权的安全性

隐式授权 (implicit grant) 被禁用是一个好的安全实践,因为它存在已知的安全漏洞。


20-22: 敏感信息使用环境变量

私钥和密码短语正确地从环境变量中获取,这是处理敏感信息的最佳实践。


33-39: 令牌生命周期设置合理

访问令牌 1 小时,授权代码 10 分钟,刷新令牌 1 个月的生命周期设置是合理的默认值。


25-27: 验证 EncryptionKeyType 枚举支持的值

已在 src/oauth2-server/src/Enums/EncryptionKeyType.php 中确认枚举定义如下:

  • case Plain = 'plain'
  • case Defuse = 'defuse'

默认值 'plain' 和备用值 'defuse' 均已正确支持,无需额外修改。

src/oauth2-server/src/Listener/LeagueOAuth2ServerListener.php (1)

44-54: 实现正确且符合最佳实践

迁移路径配置和作用域初始化逻辑实现得当,正确使用了依赖注入和值对象模式。

src/oauth2-server/src/Model/AccessTokenInterface.php (1)

17-37: 接口设计良好

AccessTokenInterface 接口设计清晰,包含了 OAuth2 访问令牌所需的所有必要方法。使用了适当的类型提示和 PHPDoc 注解。

src/oauth2-server/src/Controller/AuthorizationController.php (1)

37-42: PKCE 安全检查实现得当

正确实现了对 'plain' 代码挑战方法的安全检查,防止客户端使用不安全的 PKCE 方法,除非明确允许。

composer.json (1)

18-18: OAuth2 服务器集成配置正确

所有变更都符合集成 OAuth2 服务器组件的要求:

  • 正确添加了 ext-openssl 扩展依赖(OAuth2 加密操作所需)
  • 适当地将 league/oauth2-server 添加到开发依赖
  • PSR-4 自动加载映射和配置提供者注册都设置正确

Also applies to: 97-97, 152-152, 208-208, 293-293

src/oauth2-server/src/Manager/ElquentORM/AuthorizationCodeManager.php (1)

21-36: Eloquent 实现符合最佳实践

除了命名空间拼写错误外,AuthorizationCodeManager 的实现正确:

  • find 方法适当地使用 Eloquent 的查找功能
  • save 方法正确委托给模型的保存方法
  • clearExpired 方法正确使用 Carbon 进行时间比较并删除过期记录
src/oauth2-server/src/Model/RefreshTokenInterface.php (1)

16-29: 接口设计良好!

RefreshTokenInterface 的设计遵循了OAuth2规范,提供了完整的令牌管理方法。使用 self 返回类型支持流式接口调用,这是一个很好的设计选择。

src/oauth2-server/src/Converter/ScopeConverterInterface.php (1)

18-37: 接口设计良好

接口定义清晰,方法签名合理,PHPDoc 注释完整。使用 list<Type> 注释明确表示数组类型,这是很好的实践。

src/oauth2-server/src/Model/AuthorizationCodeInterface.php (1)

17-37: 接口设计符合OAuth2标准

接口方法定义完整,涵盖了OAuth2授权码的核心功能。使用 DateTimeInterface 而不是具体实现类是良好的实践。revoke() 方法返回 self 支持链式调用。

src/oauth2-server/src/Entity/Client.php (2)

25-28: ClientTrait 中的 name 属性可正常访问,无需修改
League\OAuth2\Server\Entities\Traits\ClientTrait 在 trait 中将 $name 声明为 protected,因此在 Client 类的 setName() 方法中访问 $this->name 是有效的。无需对属性可见性进行调整。

Likely an incorrect or invalid review comment.


38-41: 错误的审核建议

经检查仓库中并不存在名为 ClientTrait 的 trait,且 $isConfidential 属性实际上是在 src/oauth2-server/src/Entity/Client.php 类内部定义并使用的,无需在不存在的 trait 中验证其可访问性。

无需对现有 setConfidential() 方法或属性访问做任何修改。

Likely an incorrect or invalid review comment.

src/oauth2-server/src/Repository/AuthCodeRepository.php (1)

78-89: 代码实现良好!

buildAuthorizationCode 方法正确地将 OAuth2 实体转换为领域模型,使用了 tap 函数使代码更加简洁。

src/oauth2-server/src/Manager/ClientFilter.php (1)

18-98: 过滤器实现清晰且符合最佳实践

ClientFilter 类设计良好:

  • 使用了值对象模式
  • 提供了流式接口(fluent interface)
  • 类型声明准确
  • 具有良好的封装性
src/oauth2-server/src/Command/CreateClientCommand.php (1)

91-114: 命令实现符合 Symfony Console 最佳实践

代码结构清晰,错误处理得当,输出格式友好。使用 SymfonyStyle 提供了良好的用户体验。

src/oauth2-server/src/Converter/ScopeConverter.php (1)

18-56: 实现正确且简洁!

转换器类正确实现了 ScopeConverterInterface 接口,提供了 League OAuth2 Server 实体和领域模型之间的双向转换。使用了恰当的类型注解 (list<>) 有助于静态分析。

src/oauth2-server/src/Repository/AccessTokenRepository.php (2)

37-50: Token 创建逻辑正确!

getNewToken 方法正确处理了用户标识符的空值检查,并妥善地添加了所有作用域。


52-63: 唯一性约束检查实现良好!

在持久化新 Token 前检查是否已存在相同标识符的 Token,避免了重复问题。

src/oauth2-server/src/Command/UpdateClientCommand.php (2)

36-56: 命令处理逻辑实现良好!

错误处理恰当,使用了 Symfony Style 组件提供友好的用户界面反馈。


78-95: 激活状态验证逻辑完善!

正确验证了冲突的选项(同时指定 activate 和 deactivate),避免了逻辑错误。

src/oauth2-server/src/Model/ClientInterface.php (1)

18-61: 接口设计清晰完整!

接口定义了 OAuth2 客户端的完整契约,使用了值对象和恰当的类型注解。所有方法都遵循了良好的命名规范。

src/oauth2-server/src/Model/RefreshToken.php (1)

29-83: 模型实现规范且完整!

RefreshToken 模型正确使用了 UUID 主键,定义了恰当的属性转换和关联关系。revoke() 方法遵循了流式接口模式。

src/oauth2-server/src/Manager/ElquentORM/AccessTokenManager.php (1)

27-33: 修正反转的逻辑判断

find() 方法的逻辑看起来是反的。当配置指示需要持久化访问令牌时(persistAccessToken() 返回 true),应该执行查找操作;当不需要持久化时,才应该返回 null

建议修改为:

 public function find(string $identifier): ?AccessTokenInterface
 {
-    if (! $this->persistAccessToken()) {
+    if ($this->persistAccessToken()) {
-        return null;
+        return AccessToken::find($identifier);
     }
-    return AccessToken::find($identifier);
+    return null;
 }

Likely an incorrect or invalid review comment.

src/oauth2-server/src/Repository/ScopeRepository.php (2)

29-35: 构造函数实现正确

依赖注入实现得当,所有依赖项都通过 readonly 属性正确注入。


37-46: getScopeEntityByIdentifier 方法实现合理

方法逻辑简洁明了,正确处理了未找到 scope 的情况,并使用转换器将领域模型转换为 League OAuth2 实体。

src/oauth2-server/src/Manager/ElquentORM/ClientManager.php (1)

31-37: save 方法实现正确

正确实现了事件驱动的保存逻辑,在保存前派发事件允许监听器修改客户端实体。

src/oauth2-server/src/Model/AccessToken.php (2)

33-59: 模型定义规范且合理

模型正确实现了 AccessTokenInterface,使用了 UUID 主键,属性类型转换配置得当,特别是 ScopesCast 自定义转换器的使用。


76-79: getExpiry 方法实现正确

正确将 Carbon 对象转换为不可变的 DateTimeInterface,这是良好的实践,避免了意外的时间修改。

src/oauth2-server/src/Model/Client.php (3)

36-57: 模型定义完善且规范

模型正确实现了 ClientInterface,使用了 UUID 主键,自定义类型转换器配置得当,特别是对值对象的转换处理。


118-121: isConfidential 方法实现正确

正确实现了客户端机密性判断逻辑,检查 secret 是否为 null 或空字符串。


134-141: newClientInstance 方法使用了正确的关键字

使用 static 关键字而不是 self 是正确的,这支持了继承场景下的后期静态绑定。

Comment thread src/oauth2-server/src/ValueObject/Scope.php Outdated
Comment thread src/oauth2-server/src/ValueObject/Grant.php
Comment thread src/oauth2-server/src/ValueObject/RedirectUri.php Outdated
Comment thread src/oauth2-server/src/Model/Casts/GrantsCast.php
Comment thread src/oauth2-server/src/Model/Casts/GrantsCast.php
Comment thread src/oauth2-server/src/Model/AuthorizationCode.php
Comment thread src/oauth2-server/src/Model/AuthorizationCode.php
Comment thread src/oauth2-server/src/Event/AuthorizationRequestResolveEvent.php
Comment thread src/oauth2-server/src/Factory/AuthorizationServerFactory.php
Comment thread src/oauth2-server/src/Factory/AuthorizationServerFactory.php
@huangdijia
Copy link
Copy Markdown
Contributor

PR #875 代码审查:OAuth2 服务器组件实现

概述

此PR为FriendsOfHyperf组件仓库添加了一个全面的OAuth2服务器实现,包括:

  • 完整的OAuth2服务器,支持授权码、客户端凭证、密码、刷新令牌和隐式授权
  • OAuth2实体的数据库迁移(访问令牌、刷新令牌、客户端、授权码)
  • 客户端管理和令牌清理的CLI命令
  • 配置管理和事件系统

代码质量分析

架构

  • 优秀:结构良好,关注点分离恰当 - 管理器、命令、模型和值对象
  • 优秀:遵循Hyperf框架约定和模式
  • 优秀:正确使用接口和依赖注入
  • 优秀:使用成熟的OAuth2库(league/oauth2-server)

代码组织

  • 优秀:清晰的命名空间结构 FriendsOfHyperf\Oauth2\Server
  • 优秀:合适的composer.json设置和自动加载
  • 优秀:遵循PSR-4自动加载标准

实现质量

  • 优秀:全面的CLI命令,带有适当的错误处理
  • 优秀:一致使用类型提示和返回类型
  • 优秀:正确使用枚举作为配置值
  • 优秀:事件驱动架构,带有适当的扩展点

安全性考虑

⚠️ 关键安全问题

  1. 默认加密密钥:配置使用环境变量但缺少对必需安全密钥的验证
  2. 密钥生成:命令生成RSA密钥但缺少适当的熵验证
  3. 明文PKCE:允许明文PKCE挑战的选项可能存在安全风险
  4. 令牌存储:没有提及安全令牌存储或静态加密

🔒 安全建议

  1. 添加验证以确保私钥/公钥配置正确
  2. 为加密密钥实现安全默认值
  3. 为OAuth2端点添加速率限制
  4. 考虑添加令牌黑名单机制
  5. 实现适当的作用域验证

数据库架构审查

迁移结构

  • 优秀:适当的表结构,带有合适的索引
  • 优秀:使用适当的列类型和约束
  • 优秀:包含created_at/updated_at时间戳

⚠️ 潜在问题

  1. 无外键约束:表缺少适当的外键关系
  2. 索引优化:经常查询的列可以受益于复合索引
  3. 令牌清理:没有过期令牌的自动清理机制(仅手动命令)

CLI命令实现

命令质量

  • 优秀:全面的客户端管理命令集
  • 优秀:正确使用Symfony Console组件
  • 优秀:清晰的帮助文本和参数描述
  • 优秀:适当的错误处理和用户反馈

改进建议

  1. 输入验证:某些命令缺少全面的输入验证
  2. 批量操作:无批量客户端操作支持
  3. 导出/导入:缺少客户端导出/导入功能

配置管理

⚠️ 配置问题

  1. 缺少验证:对必需配置值缺少验证
  2. 默认值:某些关键值缺少安全默认值
  3. 环境变量:过度依赖环境变量而没有回退方案

测试覆盖

  • 缺失:此PR中未包含测试文件
  • 关键:OAuth2功能需要全面测试
  • 建议:为管理器、命令添加单元测试,以及集成测试

性能考虑

  • 优秀:使用适当的数据库索引
  • 关注:未提及连接池或缓存策略
  • 建议:考虑为频繁访问的令牌实现令牌缓存

依赖关系

  • 优秀:使用成熟的OAuth2库(league/oauth2-server ^9.2)
  • 优秀:最小的外部依赖
  • 优秀:与Hyperf 3.1兼容

具体代码问题

  1. src/oauth2-server/src/Command/GenerateKeyPairCommand.php:197:在diff中被截断 - 需要完整审查
  2. 迁移文件:使用datetimes()方法 - 验证这对目标数据库是否正确
  3. 配置文件:配置中的DateInterval对象可能导致序列化问题

建议

高优先级

  1. 添加全面的测试套件
  2. 实现适当的安全验证
  3. 向数据库架构添加外键约束
  4. 完成被截断的GenerateKeyPairCommand

中优先级

  1. 添加速率限制中间件
  2. 实现令牌缓存
  3. 添加客户端管理的批量操作
  4. 改进配置验证

低优先级

  1. 添加导出/导入功能
  2. 实现OAuth2服务器指标
  3. 添加管理Web界面

结论

这是一个实质性且架构良好的OAuth2实现,遵循了良好的实践。但是,在合并之前需要安全加固、全面测试和完成被截断的代码。基础是坚实的,但需要为生产使用进行完善。

状态:合并批准前需要重大更改。


📝 这个OAuth2服务器组件实现总体上展现了良好的架构设计,但在安全性和测试覆盖方面需要进一步完善。建议作者重点关注上述高优先级问题。

…tokens, improve error handling in client retrieval, and enhance redirect URI validation
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (5)
src/oauth2-server/src/Model/Casts/GrantsCast.php (2)

20-31: 仍需增强错误处理和类型安全性。

当前实现缺少对 JSON 解码失败和无效数据的处理,这些问题在之前的评审中已经指出但尚未解决。

 public function get($model, string $key, $value, array $attributes)
 {
+    if ($value === null) {
+        return [];
+    }
+
     if (is_string($value)) {
-        $value = Json::decode($value);
+        try {
+            $value = Json::decode($value);
+        } catch (\Throwable $e) {
+            return [];
+        }
     }

     if (is_array($value)) {
-        return array_map(fn (string $grant) => new Grant($grant), $value);
+        return array_map(function ($grant) {
+            if (!is_string($grant)) {
+                throw new \InvalidArgumentException('Grant must be a string');
+            }
+            return new Grant($grant);
+        }, $value);
     }

     return [];
 }

33-42: set 方法仍缺乏输入验证。

该方法需要验证输入数据的有效性,确保只接受预期的数据类型,这是之前评审中提到的问题。

 public function set($model, string $key, $value, array $attributes)
 {
+    if ($value === null) {
+        return Json::encode([]);
+    }
+
     if (is_array($value)) {
-        $value = array_map(fn (Grant $grant) => (string) $grant, $value);
+        $value = array_map(function ($grant) {
+            if (!$grant instanceof Grant) {
+                throw new \InvalidArgumentException('Array must contain only Grant objects');
+            }
+            return (string) $grant;
+        }, $value);
     } elseif ($value instanceof Grant) {
         $value = [(string) $value];
+    } else {
+        throw new \InvalidArgumentException('Value must be a Grant object or array of Grant objects');
     }
+
     return Json::encode($value);
 }
src/oauth2-server/src/Command/DeleteClientCommand.php (1)

47-52: 确认提示实现良好

删除确认提示已经正确实现,避免了意外删除客户端的风险。--force 选项的处理也很合理。

src/oauth2-server/src/Command/GenerateKeyPairCommand.php (2)

54-56: 处理配置值可能为 null 的情况

配置值可能返回 null,但属性被声明为 string 类型。这可能导致类型错误。

建议修改为:

-$this->secretKey = $config->get('authorization_server.private_key', '');
-$this->publicKey = $config->get('resource_server.public_key', '');
+$this->secretKey = (string) $config->get('authorization_server.private_key', '');
+$this->publicKey = (string) $config->get('resource_server.public_key', '');

159-165: 提供更有意义的错误信息

与第 153 行类似,当 openssl_error_string() 返回 false 时,应提供有意义的错误信息。

建议修改为:

-throw new RuntimeException(openssl_error_string() ?: '');
+throw new RuntimeException(openssl_error_string() ?: 'Failed to export private key');
-throw new RuntimeException(openssl_error_string() ?: '');
+throw new RuntimeException(openssl_error_string() ?: 'Failed to get public key details');
🧹 Nitpick comments (7)
src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php (1)

19-37: 考虑添加日志记录以改善可观测性

作为 OAuth2 的核心组件,建议添加适当的日志记录来跟踪授权码的操作,这对于安全审计和故障排查很重要。

考虑在类中注入日志器并在关键操作点添加日志:

use Psr\Log\LoggerInterface;

final class AuthorizationCodeManager implements AuthorizationCodeManagerInterface
{
    public function __construct(
        private ?LoggerInterface $logger = null
    ) {}

    public function save(AuthorizationCodeInterface $authCode): void
    {
        try {
            $authCode->save();
            $this->logger?->info('Authorization code saved', ['id' => $authCode->getId()]);
        } catch (\Exception $e) {
            $this->logger?->error('Failed to save authorization code', [
                'id' => $authCode->getId(),
                'error' => $e->getMessage()
            ]);
            throw new \RuntimeException('Failed to save authorization code: ' . $e->getMessage(), 0, $e);
        }
    }
}
src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php (2)

21-24: find 方法实现正确,但建议增加参数验证

find 方法的实现符合接口要求,使用 Eloquent 的 find 方法简洁明了。但建议考虑添加输入参数验证以提高健壮性。

 public function find(string $identifier): ?RefreshTokenInterface
 {
+    if (empty($identifier)) {
+        return null;
+    }
     return RefreshToken::find($identifier);
 }

19-37: 整体架构良好,但需要与其他管理器保持一致性

RefreshTokenManager 的实现整体上遵循了良好的设计原则,但与项目中其他管理器的实现存在不一致性。特别是:

  1. AccessTokenManager 有 persistAccessToken() 条件检查
  2. ClientManager 有事件分发机制
  3. 当前实现相对简单,可能需要根据业务需求补充相应功能

建议统一各个管理器的实现模式,确保代码库的一致性和可维护性。

src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php (1)

43-51: 建议使用辅助函数并添加错误处理

可以使用项目中的 now() 辅助函数来保持一致性,同时需要添加数据库操作的错误处理。

参考 src/helpers/src/Functions.php 中的 now() 函数,建议进行以下改进:

 public function clearExpired(): int
 {
     if (! $this->persistAccessToken()) {
         return 0;
     }
-    return AccessToken::query()
-        ->where('expires_at', '<', Carbon::now())
-        ->delete();
+    try {
+        return AccessToken::query()
+            ->where('expires_at', '<', now())
+            ->delete();
+    } catch (\Exception $e) {
+        throw new \RuntimeException('Failed to clear expired access tokens: ' . $e->getMessage(), 0, $e);
+    }
 }
tests/Oauth2Server/Command/CreateClientCommandTest.php (1)

163-320: 建议重构重复的 mock 设置代码。

虽然测试覆盖很全面,但存在大量重复的 mock 设置代码。建议提取共用的 mock 设置逻辑到辅助方法中,提高代码可维护性。

private function setupBasicClientMocks(): void
{
    $this->clientManager->shouldReceive('save')->once();
    $this->client->shouldReceive('newClientInstance')->once()->andReturnSelf();
    $this->client->shouldReceive('setActive')->once()->andReturnSelf();
    $this->client->shouldReceive('setAllowPlainTextPkce')->once()->andReturnSelf();
    $this->client->shouldReceive('setRedirectUris')->once()->andReturnSelf();
    $this->client->shouldReceive('setGrants')->once()->andReturnSelf();
    $this->client->shouldReceive('setScopes')->once()->andReturnSelf();
    $this->client->shouldReceive('getIdentifier')->once()->andReturn('custom-id');
    $this->client->shouldReceive('getSecret')->once()->andReturn(null);
}
src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (1)

56-77: 考虑优化查询性能

当前的过滤实现对每个条件使用 orWhereJsonContains,这在大数据集上可能会有性能问题。建议:

  1. 考虑为 JSON 字段添加索引
  2. 对于频繁查询的场景,可以考虑使用缓存
src/oauth2-server/src/Model/Client.php (1)

134-141: 工厂方法实现正确,考虑添加参数验证。

使用static关键字支持子类继承,方法签名符合接口要求。建议添加参数验证以确保数据完整性:

 public function newClientInstance(string $name, string $identifier, ?string $secret): ClientInterface
 {
+    if (empty($name) || empty($identifier)) {
+        throw new \InvalidArgumentException('Client name and identifier cannot be empty');
+    }
+    
     return new static([
         'name' => $name,
         'id' => $identifier,
         'secret' => $secret,
+        'active' => true, // 设置合理的默认值
     ]);
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 694be89 and ce2e3b9.

📒 Files selected for processing (32)
  • src/oauth2-server/databases/2025_07_15_044853_create_oauth_server.php (1 hunks)
  • src/oauth2-server/src/Command/ClearExpiredTokensCommand.php (1 hunks)
  • src/oauth2-server/src/Command/CreateClientCommand.php (1 hunks)
  • src/oauth2-server/src/Command/DeleteClientCommand.php (1 hunks)
  • src/oauth2-server/src/Command/GenerateKeyPairCommand.php (1 hunks)
  • src/oauth2-server/src/ConfigProvider.php (1 hunks)
  • src/oauth2-server/src/Controller/AuthorizationController.php (1 hunks)
  • src/oauth2-server/src/Controller/TokenController.php (1 hunks)
  • src/oauth2-server/src/Event/AuthorizationRequestResolveEvent.php (1 hunks)
  • src/oauth2-server/src/Event/UserResolveEvent.php (1 hunks)
  • src/oauth2-server/src/Factory/AuthorizationServerFactory.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php (1 hunks)
  • src/oauth2-server/src/Manager/InMemory/ScopeManager.php (1 hunks)
  • src/oauth2-server/src/Middleware/ResourceServerMiddleware.php (1 hunks)
  • src/oauth2-server/src/Model/AccessToken.php (1 hunks)
  • src/oauth2-server/src/Model/AuthorizationCode.php (1 hunks)
  • src/oauth2-server/src/Model/Casts/GrantsCast.php (1 hunks)
  • src/oauth2-server/src/Model/Client.php (1 hunks)
  • src/oauth2-server/src/Model/ClientInterface.php (1 hunks)
  • src/oauth2-server/src/Repository/AccessTokenRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/ClientRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/UserRepository.php (1 hunks)
  • src/oauth2-server/src/ValueObject/Grant.php (1 hunks)
  • src/oauth2-server/src/ValueObject/RedirectUri.php (1 hunks)
  • src/oauth2-server/src/ValueObject/Scope.php (1 hunks)
  • tests/Oauth2Server/Command/ClearExpiredTokensCommandTest.php (1 hunks)
  • tests/Oauth2Server/Command/CreateClientCommandTest.php (1 hunks)
  • tests/Oauth2Server/Command/DeleteClientCommandTest.php (1 hunks)
  • tests/Oauth2Server/Command/GenerateKeyPairCommand.php (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/oauth2-server/src/ValueObject/Scope.php
  • src/oauth2-server/src/Command/ClearExpiredTokensCommand.php
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/oauth2-server/src/Event/UserResolveEvent.php
  • src/oauth2-server/src/Repository/UserRepository.php
  • src/oauth2-server/src/ConfigProvider.php
  • src/oauth2-server/src/ValueObject/RedirectUri.php
  • src/oauth2-server/src/Middleware/ResourceServerMiddleware.php
  • src/oauth2-server/src/Manager/InMemory/ScopeManager.php
  • src/oauth2-server/src/Repository/AccessTokenRepository.php
  • src/oauth2-server/src/Controller/TokenController.php
  • src/oauth2-server/src/Command/CreateClientCommand.php
  • src/oauth2-server/src/Controller/AuthorizationController.php
  • src/oauth2-server/src/Repository/ClientRepository.php
  • src/oauth2-server/databases/2025_07_15_044853_create_oauth_server.php
  • src/oauth2-server/src/Model/AccessToken.php
  • src/oauth2-server/src/Event/AuthorizationRequestResolveEvent.php
  • src/oauth2-server/src/Factory/AuthorizationServerFactory.php
  • src/oauth2-server/src/Model/AuthorizationCode.php
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/oauth2-server/src/ValueObject/Grant.php (2)
src/oauth2-server/src/Event/UserResolveEvent.php (1)
  • __construct (23-29)
src/oauth2-server/src/ValueObject/Scope.php (2)
  • __construct (16-19)
  • __toString (21-24)
src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php (4)
src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php (3)
  • find (27-33)
  • save (35-41)
  • clearExpired (43-51)
src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php (3)
  • find (21-24)
  • save (26-29)
  • clearExpired (31-36)
src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (2)
  • find (44-47)
  • save (31-37)
src/helpers/src/Functions.php (1)
  • now (371-374)
src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php (4)
src/oauth2-server/src/Model/AccessToken.php (1)
  • AccessToken (34-115)
src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php (3)
  • find (21-24)
  • save (26-29)
  • clearExpired (31-36)
src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php (3)
  • find (21-24)
  • save (26-29)
  • clearExpired (31-36)
src/helpers/src/Functions.php (1)
  • now (371-374)
src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (6)
src/oauth2-server/src/Event/PreSaveClientEvent.php (1)
  • PreSaveClientEvent (17-33)
src/oauth2-server/src/Manager/ClientFilter.php (2)
  • ClientFilter (18-98)
  • hasFilters (91-97)
src/oauth2-server/src/Model/Client.php (4)
  • Client (36-142)
  • getGrants (85-88)
  • getRedirectUris (74-77)
  • getScopes (96-99)
src/oauth2-server/src/ValueObject/Grant.php (2)
  • Grant (17-31)
  • __construct (19-25)
src/oauth2-server/src/ValueObject/RedirectUri.php (2)
  • RedirectUri (16-54)
  • __construct (18-24)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Model/Client.php (7)
src/oauth2-server/src/Model/Casts/GrantsCast.php (1)
  • GrantsCast (18-42)
src/oauth2-server/src/Model/Casts/RedirectsCast.php (1)
  • RedirectsCast (18-42)
src/oauth2-server/src/Model/Casts/ScopesCast.php (1)
  • ScopesCast (18-42)
src/oauth2-server/src/ValueObject/Grant.php (1)
  • Grant (17-31)
src/oauth2-server/src/ValueObject/RedirectUri.php (1)
  • RedirectUri (16-54)
src/oauth2-server/src/ValueObject/Scope.php (1)
  • Scope (14-25)
src/oauth2-server/src/Model/ClientInterface.php (15)
  • getIdentifier (23-23)
  • getName (25-25)
  • getSecret (27-27)
  • getRedirectUris (32-32)
  • setRedirectUris (34-34)
  • getGrants (39-39)
  • setGrants (41-41)
  • getScopes (46-46)
  • setScopes (48-48)
  • isActive (50-50)
  • setActive (52-52)
  • isConfidential (54-54)
  • isPlainTextPkceAllowed (56-56)
  • setAllowPlainTextPkce (58-58)
  • newClientInstance (60-60)
🪛 PHPMD (2.15.0)
src/oauth2-server/src/Model/Casts/GrantsCast.php

20-20: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


20-20: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$model'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$key'. (Unused Code Rules)

(UnusedFormalParameter)


33-33: Avoid unused parameters such as '$attributes'. (Unused Code Rules)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test on PHP 8.3 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.2 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.3 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.1 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.1 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.2 with Swoole 6.0.2
🔇 Additional comments (17)
src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php (1)

21-24: 添加输入验证以提高安全性

find 方法缺少对 $identifier 参数的验证,这可能导致安全风险。建议添加输入验证来确保标识符格式正确且安全。

 public function find(string $identifier): ?AuthorizationCodeInterface
 {
+    if (empty($identifier) || !is_string($identifier)) {
+        return null;
+    }
+    
+    // 验证标识符格式(例如:长度、字符集)
+    if (strlen($identifier) < 10 || strlen($identifier) > 255) {
+        return null;
+    }
+    
     return AuthorizationCode::find($identifier);
 }

Likely an incorrect or invalid review comment.

src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php (2)

20-25: 构造函数实现良好

使用了 readonly 关键字确保配置接口的不可变性,依赖注入模式正确。


53-56: 配置访问实现正确

正确使用了默认值,确保配置缺失时的安全回退行为。

tests/Oauth2Server/Command/DeleteClientCommandTest.php (2)

25-37: 测试结构良好,但建议保持 mock 设置的一致性。

测试用例覆盖了各种场景,mock 生命周期管理得当。不过注意到在不同测试中 mock 的设置方式略有不同,建议保持一致性。


81-101: 测试逻辑正确,验证了强制删除功能。

该测试正确验证了 --force 选项跳过确认的行为,mock 期望设置合理。

tests/Oauth2Server/Command/ClearExpiredTokensCommandTest.php (2)

25-41: 测试设置完善,涵盖了多种场景。

beforeEach 和 afterEach 的设置合理,正确管理了依赖注入和 mock 生命周期。


43-98: 测试用例覆盖全面,验证了各种清理场景。

测试用例很好地覆盖了:

  • 默认清理所有类型的令牌
  • 选择性清理特定类型
  • 多种令牌类型组合
  • 无令牌清理的情况

每个测试都正确验证了相应管理器方法的调用次数和输出内容。

tests/Oauth2Server/Command/CreateClientCommandTest.php (3)

25-38: 测试设置良好,依赖管理得当。

beforeEach 和 afterEach 正确设置了 mock 和容器清理,确保测试隔离。


48-78: 基础客户端创建测试覆盖完整。

测试正确验证了最小参数下的客户端创建流程,mock 期望设置合理。


149-161: 公共客户端验证逻辑测试正确。

正确测试了公共客户端不能设置密钥的业务规则,错误处理和退出码验证都很到位。

src/oauth2-server/src/Model/ClientInterface.php (1)

18-61: 接口设计良好

接口定义清晰全面,使用值对象确保类型安全,setter 方法返回 self 支持链式调用,符合最佳实践。

tests/Oauth2Server/Command/GenerateKeyPairCommand.php (1)

24-144: 测试覆盖全面

测试用例涵盖了各种场景,包括默认行为、错误处理、各种选项和密码短语功能。测试结构清晰,使用了适当的 setup 和 teardown。

src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (1)

31-37: 事件驱动设计良好

在保存客户端之前分发 PreSaveClientEvent 事件,允许监听器修改客户端数据,提供了良好的扩展性。

src/oauth2-server/src/Model/Client.php (4)

36-57: 类结构设计合理,实现了完整的OAuth2客户端模型。

模型正确继承了Hyperf的Model类并实现了ClientInterface接口,使用UUID作为主键是OAuth2客户端的最佳实践。自定义类型转换器的配置正确,能够在存储层的JSON数据和领域对象之间进行无缝转换。


59-77: Getter方法实现正确且简洁。

所有的访问器方法都正确返回了相应的属性值,getIdentifier()使用getKey()符合Eloquent模型的最佳实践。


79-105: Setter方法设计优秀,支持流畅的API调用。

使用变长参数语法允许灵活设置多个值对象,返回ClientInterface类型而非具体实现类符合依赖倒置原则,支持方法链式调用提升了API的易用性。


123-126: 注意plain text PKCE的安全风险。

允许明文PKCE挑战可能带来安全风险。建议在生产环境中默认禁用此功能,并在文档中明确说明启用此选项的安全影响。

考虑添加验证逻辑来确保只有在安全的环境下才允许启用plain text PKCE:

 public function isPlainTextPkceAllowed(): bool
 {
+    // 在生产环境中建议禁用plain text PKCE
+    if (config('app.env') === 'production' && $this->allow_plain_text_pkce) {
+        // 记录警告日志
+    }
     return $this->allow_plain_text_pkce;
 }

Comment thread src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php
Comment thread src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php
Comment thread src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php
Comment thread src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php
Comment thread src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php
Comment thread src/oauth2-server/src/ValueObject/Grant.php
Comment thread src/oauth2-server/src/Manager/EloquentORM/ClientManager.php Outdated
zds-s added 3 commits July 16, 2025 16:07
…, authorization code, client, refresh token, and update client command managers
…nts in ListClientsCommand for grant, redirect URI, and scope options
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (1)

89-90: 修正返回类型注解

返回类型注解中使用了 redirect_uris,但实际代码中使用的是 redirects

建议修改为:

-     * @return array{grants?: list<Grant>, redirect_uris?: list<RedirectUri>, scopes?: list<Scope>}
+     * @return array{grants?: list<Grant>, redirects?: list<RedirectUri>, scopes?: list<Scope>}

Also applies to: 106-107

🧹 Nitpick comments (4)
tests/Oauth2Server/Command/ListClientsCommandTest.php (1)

143-143: 修正变量命名错误

变量名 $g1 看起来像是拼写错误,应该改为 $scope 以保持代码的可读性和一致性。

-            $scopes = array_map(fn (Scope $g1) => (string) $g1, $filter->getScopes());
+            $scopes = array_map(fn (Scope $scope) => (string) $scope, $filter->getScopes());
src/oauth2-server/src/Command/UpdateClientCommand.php (2)

42-42: 保持 sprintf 函数调用的一致性

代码中对 sprintf 函数的调用不一致,有些地方使用了 \sprintf(第42行和第114行),而其他地方使用了 sprintf。建议统一使用其中一种形式。

建议统一使用不带反斜杠的形式:

-            $io->error(\sprintf('OAuth2 client identified as "%s" does not exist.', $input->getArgument('identifier')));
+            $io->error(sprintf('OAuth2 client identified as "%s" does not exist.', $input->getArgument('identifier')));
-            throw new RuntimeException(\sprintf('Cannot specify "%s" in either "--%s" and "--%s".', implode('", "', $colliding), $addArgument, $removeArgument));
+            throw new RuntimeException(sprintf('Cannot specify "%s" in either "--%s" and "--%s".', implode('", "', $colliding), $addArgument, $removeArgument));

Also applies to: 114-114


64-65: 修正选项描述中的语法错误

在移除操作的描述中,应该使用 "from the client" 而不是 "to the client"。

-            ->addOption('remove-redirect-uri', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Remove allowed redirect uri to the client.', [])
+            ->addOption('remove-redirect-uri', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Remove allowed redirect uri from the client.', [])

-            ->addOption('remove-grant-type', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Remove allowed grant type to the client.', [])
+            ->addOption('remove-grant-type', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Remove allowed grant type from the client.', [])

-            ->addOption('remove-scope', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Remove allowed scope to the client.', [])
+            ->addOption('remove-scope', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Remove allowed scope from the client.', [])

Also applies to: 67-67, 70-70

src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (1)

42-43: 修正注释格式错误

第42行有一个重复的注释标记 // //,应该修正为单个注释标记。

-        // // @phpstan-ignore-next-line
+        // @phpstan-ignore-next-line
         $client->delete();
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce2e3b9 and 1927bd8.

📒 Files selected for processing (11)
  • src/oauth2-server/src/Command/ListClientsCommand.php (1 hunks)
  • src/oauth2-server/src/Command/UpdateClientCommand.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php (1 hunks)
  • src/oauth2-server/src/Repository/ClientRepository.php (1 hunks)
  • src/oauth2-server/src/ValueObject/Grant.php (1 hunks)
  • src/oauth2-server/src/ValueObject/RedirectUri.php (1 hunks)
  • tests/Oauth2Server/Command/ListClientsCommandTest.php (1 hunks)
  • tests/Oauth2Server/Command/UpdateClientCommandTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/oauth2-server/src/ValueObject/Grant.php
  • src/oauth2-server/src/Manager/EloquentORM/AuthorizationCodeManager.php
  • src/oauth2-server/src/Manager/EloquentORM/AccessTokenManager.php
  • src/oauth2-server/src/Repository/ClientRepository.php
  • src/oauth2-server/src/ValueObject/RedirectUri.php
  • src/oauth2-server/src/Command/ListClientsCommand.php
  • src/oauth2-server/src/Manager/EloquentORM/RefreshTokenManager.php
🧰 Additional context used
🧬 Code Graph Analysis (2)
tests/Oauth2Server/Command/UpdateClientCommandTest.php (3)
src/oauth2-server/src/Command/UpdateClientCommand.php (1)
  • UpdateClientCommand (26-124)
src/oauth2-server/src/ValueObject/Grant.php (2)
  • Grant (17-31)
  • __toString (27-30)
src/oauth2-server/src/ValueObject/RedirectUri.php (2)
  • RedirectUri (16-54)
  • __toString (26-29)
src/oauth2-server/src/Manager/EloquentORM/ClientManager.php (6)
src/oauth2-server/src/Event/PreSaveClientEvent.php (1)
  • PreSaveClientEvent (17-33)
src/oauth2-server/src/Manager/ClientFilter.php (2)
  • ClientFilter (18-98)
  • hasFilters (91-97)
src/oauth2-server/src/Model/Client.php (4)
  • Client (36-142)
  • getGrants (85-88)
  • getRedirectUris (74-77)
  • getScopes (96-99)
src/oauth2-server/src/ValueObject/Grant.php (2)
  • Grant (17-31)
  • __construct (19-25)
src/oauth2-server/src/ValueObject/RedirectUri.php (2)
  • RedirectUri (16-54)
  • __construct (18-24)
src/oauth2-server/src/ValueObject/Scope.php (2)
  • Scope (14-25)
  • __construct (16-19)

Comment thread tests/Oauth2Server/Command/UpdateClientCommandTest.php
Comment thread tests/Oauth2Server/Command/UpdateClientCommandTest.php
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
tests/Oauth2Server/Command/UpdateClientCommandTest.php (1)

157-161: 修正测试逻辑错误

当抛出异常时,后续的 expect 语句不会被执行。第158-161行的断言是无法到达的代码。

建议移除无法执行的断言:

     $output = new BufferedOutput();
     $this->expectExceptionMessage('Cannot specify "' . $uri . '" in either "--add-redirect-uri" and "--remove-redirect-uri"');
-    $exitCode = $this->command->run($input, $output);
-    expect($exitCode)->toBe(1)
-        ->and($output->fetch())->toContain('Cannot specify "' . $uri . '" in either "--add-redirect-uri" and "--remove-redirect-uri"');
+    $this->command->run($input, $output);
🧹 Nitpick comments (3)
tests/Oauth2Server/Command/UpdateClientCommandTest.php (3)

178-181: 简化 Mockery 匹配器的使用

使用 withArgs 方法可以更简洁地表达参数匹配逻辑,提高代码可读性。

-    $this->client->shouldReceive('setGrants')->once()
-        ->with(Mockery::on(function (...$arg) use ($newGrant) {
-            return count($arg) === 1
-                && $arg[0]->__toString() === $newGrant;
-        }));
+    $this->client->shouldReceive('setGrants')->once()
+        ->withArgs(function (...$grants) use ($newGrant) {
+            expect($grants)->toHaveCount(1)
+                ->and((string) $grants[0])->toBe($newGrant);
+            return true;
+        });

238-241: 统一匹配器使用风格

为了保持代码一致性,建议使用与其他测试相同的 withArgs 方法风格。

-    $this->client->shouldReceive('setScopes')->once()
-        ->with(Mockery::on(function (...$arg) use ($newScope) {
-            return count($arg) === 1
-                && $arg[0]->__toString() === $newScope;
-        }))->andReturnSelf();
+    $this->client->shouldReceive('setScopes')->once()
+        ->withArgs(function (...$scopes) use ($newScope) {
+            expect($scopes)->toHaveCount(1)
+                ->and((string) $scopes[0])->toBe($newScope);
+            return true;
+        })->andReturnSelf();

325-325: 删除不必要的交互模式设置

在单元测试中设置交互模式是不必要的,因为测试环境通常不需要用户交互。

-    $input->setInteractive(false);
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d68a4c and f698152.

📒 Files selected for processing (1)
  • tests/Oauth2Server/Command/UpdateClientCommandTest.php (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tests/Oauth2Server/Command/UpdateClientCommandTest.php (3)
src/oauth2-server/src/Command/UpdateClientCommand.php (1)
  • UpdateClientCommand (26-124)
src/oauth2-server/src/ValueObject/Grant.php (2)
  • Grant (17-31)
  • __toString (27-30)
src/oauth2-server/src/ValueObject/RedirectUri.php (2)
  • RedirectUri (16-54)
  • __toString (26-29)
🔇 Additional comments (1)
tests/Oauth2Server/Command/UpdateClientCommandTest.php (1)

43-346: 测试覆盖度良好

测试套件全面覆盖了 UpdateClientCommand 的各种功能场景,包括:

  • 参数验证
  • 客户端更新成功流程
  • 重定向 URI 的添加和删除
  • 授权类型的添加和删除
  • 作用域的添加和删除
  • 客户端激活和停用
  • 错误处理和冲突检测

测试逻辑清晰,模拟设置合理,能够有效验证命令的行为。

Comment thread tests/Oauth2Server/Command/UpdateClientCommandTest.php
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/oauth2-server/README.md (4)

52-67: 示例缺少 DateInterval 引用,复制即用会报错
代码片段中直接实例化 new DateInterval(),但顶部未 use DateInterval;,用户复制粘贴到实际项目时会导致 IDE 或 CS 报未导入类。


83-84: 注释被截断导致可读性差
# The value should be a valid interval: http://ph 这一行被截断,链接失效且语义不完整,请补全至官方手册完整 URL。


34-40: 提醒配置私钥文件权限,减少泄露风险
建议在生成密钥示例后补一句:
“生成的 oauth-private.key 应设置 chmod 600 并确保不被提交到 VCS”。
有助于新手避免把私钥暴露到仓库或镜像中。


127-134: 命令列表缺少 oauth2:update-client
前文功能介绍提到“client management (create/delete/list)”,代码中实则包含 UpdateClientCommand,但表格未列出,容易让读者误以为缺少更新功能。

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f698152 and 55b8480.

📒 Files selected for processing (1)
  • src/oauth2-server/README.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test on PHP 8.3 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.1 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.1 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.2 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.2 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.3 with Swoole 5.1.7

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

♻️ Duplicate comments (2)
src/oauth2-server/src/Repository/ClientRepository.php (1)

76-76: 之前的审查建议已被采纳

很好!代码已经使用了严格比较模式 in_array($grant, $grants, true)

src/oauth2-server/src/Factory/AuthorizationServerFactory.php (1)

123-135: 未修复的逻辑错误:缺少启用检查且隐式授权设置错误

此代码段存在之前评审中指出的问题仍未修复:

  1. 直接从容器获取授权类型实例时没有检查它们是否已启用,可能导致运行时错误
  2. 为 ImplicitGrant 设置刷新令牌 TTL 是错误的,因为隐式授权不支持刷新令牌

建议修复:

 private function configureGrants(): void
 {
     $refreshTokenTTL = $this->config->get('authorization_server.refresh_token_ttl');
-    $this->container->get(PasswordGrant::class)
-        ->setRefreshTokenTTL($refreshTokenTTL);
-    $this->container->get(RefreshTokenGrant::class)
-        ->setRefreshTokenTTL($refreshTokenTTL);
-    $this->container->get(AuthCodeGrant::class)
-        ->setRefreshTokenTTL($refreshTokenTTL);
+    
+    if ($this->config->get('authorization_server.enable_password_grant') && $this->container->has(PasswordGrant::class)) {
+        $this->container->get(PasswordGrant::class)
+            ->setRefreshTokenTTL($refreshTokenTTL);
+    }
+    
+    if ($this->config->get('authorization_server.enable_refresh_token_grant') && $this->container->has(RefreshTokenGrant::class)) {
+        $this->container->get(RefreshTokenGrant::class)
+            ->setRefreshTokenTTL($refreshTokenTTL);
+    }
+    
+    if ($this->config->get('authorization_server.enable_auth_code_grant') && $this->container->has(AuthCodeGrant::class)) {
+        $this->container->get(AuthCodeGrant::class)
+            ->setRefreshTokenTTL($refreshTokenTTL);
+    }
+    
     if (! $this->config->get('authorization_server.require_code_challenge_for_public_clients')) {
-        $this->container->get(AuthCodeGrant::class)->disableRequireCodeChallengeForPublicClients();
+        if ($this->config->get('authorization_server.enable_auth_code_grant') && $this->container->has(AuthCodeGrant::class)) {
+            $this->container->get(AuthCodeGrant::class)->disableRequireCodeChallengeForPublicClients();
+        }
     }
-    if ($this->config->get('authorization_server.enable_implicit_grant')) {
-        $this->container->get(ImplicitGrant::class)
-            ->setRefreshTokenTTL($refreshTokenTTL);
-    }
+    // 移除隐式授权的刷新令牌配置,因为它不支持刷新令牌
 }
🧹 Nitpick comments (4)
src/oauth2-server/src/Repository/ClientRepository.php (1)

57-59: 考虑增强客户端密钥验证的安全性

当前实现允许非机密客户端绕过密钥验证。建议添加更明确的逻辑分离。

-if (! $client->isConfidential() || hash_equals((string) $client->getSecret(), (string) $clientSecret)) {
-    return true;
-}
+// 非机密客户端不需要验证密钥
+if (! $client->isConfidential()) {
+    return true;
+}
+
+// 机密客户端必须提供正确的密钥
+if ($clientSecret === null || ! hash_equals((string) $client->getSecret(), (string) $clientSecret)) {
+    return false;
+}
+
+return true;
src/oauth2-server/src/Model/DeviceCodeInterface.php (1)

26-26: 考虑返回 Scope 对象数组而非混合类型

为了类型安全,建议明确指定返回类型为 Scope[]

-public function getScopes(): array;
+/** @return Scope[] */
+public function getScopes(): array;
src/oauth2-server/src/Repository/DeviceCodeRepository.php (1)

46-46: 参数名称可能引起混淆

参数名 $deviceCodeEntity 实际上是一个字符串,而不是实体对象。建议重命名以提高代码可读性。

-public function getDeviceCodeEntityByDeviceCode(string $deviceCodeEntity): ?DeviceCodeEntityInterface
+public function getDeviceCodeEntityByDeviceCode(string $deviceCode): ?DeviceCodeEntityInterface
 {
-    $deviceCodeModel = $this->manager->findByDeviceCode($deviceCodeEntity);
+    $deviceCodeModel = $this->manager->findByDeviceCode($deviceCode);
src/oauth2-server/src/Model/Device.php (1)

166-170: 日期转换方法不一致

setLastPoll 方法使用 Carbon::instance()setExpiry 方法使用 Carbon::createFromImmutable(),应保持一致性。

统一日期转换方法:

 public function setLastPoll(DateTimeImmutable $lastPoll): DeviceCodeInterface
 {
-    $this->last_poll_at = Carbon::instance($lastPoll);
+    $this->last_poll_at = Carbon::createFromImmutable($lastPoll);
     return $this;
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55b8480 and 11c698e.

📒 Files selected for processing (13)
  • src/oauth2-server/src/ConfigProvider.php (1 hunks)
  • src/oauth2-server/src/Controller/AuthorizationController.php (1 hunks)
  • src/oauth2-server/src/Converter/ClientConverter.php (1 hunks)
  • src/oauth2-server/src/Converter/ClientConverterInterface.php (1 hunks)
  • src/oauth2-server/src/Entity/DeviceCode.php (1 hunks)
  • src/oauth2-server/src/Enums/DeviceCodeStatus.php (1 hunks)
  • src/oauth2-server/src/Factory/AuthorizationServerFactory.php (1 hunks)
  • src/oauth2-server/src/Manager/DeviceCodeManagerInterface.php (1 hunks)
  • src/oauth2-server/src/Manager/EloquentORM/DeviceCodeManager.php (1 hunks)
  • src/oauth2-server/src/Model/Device.php (1 hunks)
  • src/oauth2-server/src/Model/DeviceCodeInterface.php (1 hunks)
  • src/oauth2-server/src/Repository/ClientRepository.php (1 hunks)
  • src/oauth2-server/src/Repository/DeviceCodeRepository.php (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • src/oauth2-server/src/Entity/DeviceCode.php
  • src/oauth2-server/src/Enums/DeviceCodeStatus.php
  • src/oauth2-server/src/Converter/ClientConverterInterface.php
  • src/oauth2-server/src/Manager/DeviceCodeManagerInterface.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/oauth2-server/src/Controller/AuthorizationController.php
  • src/oauth2-server/src/ConfigProvider.php
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/oauth2-server/src/Manager/EloquentORM/DeviceCodeManager.php (2)
src/oauth2-server/src/Model/Device.php (1)
  • Device (37-194)
src/oauth2-server/src/Manager/DeviceCodeManagerInterface.php (2)
  • save (18-18)
  • findByDeviceCode (20-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test on PHP 8.1 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.2 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.3 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.1 with Swoole 6.0.2
  • GitHub Check: Test on PHP 8.3 with Swoole 5.1.7
  • GitHub Check: Test on PHP 8.2 with Swoole 6.0.2

Comment thread src/oauth2-server/src/Manager/EloquentORM/DeviceCodeManager.php
Comment thread src/oauth2-server/src/Manager/EloquentORM/DeviceCodeManager.php
Comment thread src/oauth2-server/src/Converter/ClientConverter.php
Comment thread src/oauth2-server/src/Converter/ClientConverter.php
Comment thread src/oauth2-server/src/Model/DeviceCodeInterface.php Outdated
Comment thread src/oauth2-server/src/Factory/AuthorizationServerFactory.php
Comment thread src/oauth2-server/src/Model/Device.php
Comment thread src/oauth2-server/src/Model/Device.php Outdated
Comment thread src/oauth2-server/src/Model/Device.php
Comment thread src/oauth2-server/src/Model/Device.php
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/oauth2-server/src/ConfigProvider.php (1)

32-36: 确认命名空间拼写的一致性

注意到导入的命名空间使用了 EloquentORM,但之前的审查评论提到存在 ElquentORM 拼写错误。请确认目录结构和命名空间声明的一致性。

如果目录名称仍为 ElquentORM,需要将其重命名为 EloquentORM 并更新所有相关的命名空间声明和自动加载配置。

🧹 Nitpick comments (1)
src/oauth2-server/src/ConfigProvider.php (1)

99-99: 修正注释中的拼写错误

注释中的 "factroy" 应为 "factory"。

-                // factroy
+                // factory
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71967c1 and 771d4b6.

📒 Files selected for processing (3)
  • src/oauth2-server/src/ConfigProvider.php (1 hunks)
  • src/oauth2-server/src/Controller/AuthorizationController.php (1 hunks)
  • tests/Oauth2Server/InMemoryScopeManagerTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/oauth2-server/src/Controller/AuthorizationController.php
🧰 Additional context used
🪛 GitHub Actions: tests
tests/Oauth2Server/InMemoryScopeManagerTest.php

[error] 1-27: PHP CS Fixer: Multiple formatting issues including missing declare(strict_types=1), blank lines, no_unused_imports, cast_spaces, header_comment, blank_lines_before_namespace, single_blank_line_at_eof.

src/oauth2-server/src/ConfigProvider.php

[error] 71-71: PHP CS Fixer: Missing trailing comma in multiline array and other formatting issues (phpdoc_no_alias_tag, trailing_comma_in_multiline, header_comment, blank_lines_before_namespace).

🔇 Additional comments (1)
src/oauth2-server/src/ConfigProvider.php (1)

67-113: 配置结构设计良好

整体配置提供器的结构清晰,正确注册了 OAuth2 服务器所需的所有组件:

  • 命令行工具覆盖了客户端管理和令牌清理
  • 依赖注入绑定完整,涵盖了模型、管理器、转换器、仓储和工厂
  • 发布配置允许用户自定义配置文件

实现遵循了 Hyperf 框架的约定,使用了适当的接口抽象。

Comment thread tests/Oauth2Server/InMemoryScopeManagerTest.php Outdated
Comment thread src/oauth2-server/src/ConfigProvider.php
Comment thread src/oauth2-server/src/ConfigProvider.php Outdated
@huangdijia huangdijia marked this pull request as ready for review July 18, 2025 02:07
@huangdijia huangdijia merged commit efd6286 into friendsofhyperf:main Jul 18, 2025
8 checks passed
@zds-s zds-s deleted the patch-oauth2-server branch July 18, 2025 02:15
huangdijia added a commit that referenced this pull request Jul 18, 2025
* Add initial implementation of OAuth2 server components

* Refactor OAuth2 server components: update default values for revoked tokens, improve error handling in client retrieval, and enhance redirect URI validation

* Add PHPStan ignore comments for potential type issues in access token, authorization code, client, refresh token, and update client command managers

* Refactor: Mark Grant and RedirectUri classes as final, update type hints in ListClientsCommand for grant, redirect URI, and scope options

* up

* up

* add README.md

* feat: Implement Device Code flow with new Device and DeviceCode entities, converters, and repository

* feat: Implement Device Code flow with new Device and DeviceCode entities, converters, and repository

* up

* feat: Add Device Code grant type to OAuth2Grants

* refactor: Remove debug output and add UpdateClientCommand to ConfigProvider

* Apply cs fix
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants