Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion app/Http/Common/Middleware/AccessTokenMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,51 @@

namespace App\Http\Common\Middleware;

use App\Http\Common\Result;
use App\Http\Common\ResultCode;
use Hyperf\Codec\Json;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Mine\Jwt\JwtInterface;
use Mine\JwtAuth\Middleware\AbstractTokenMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Swow\Psr7\Message\ServerRequestPlusInterface;

final class AccessTokenMiddleware extends AbstractTokenMiddleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$token = $this->parserToken($request);
} catch (RequiredConstraintsViolated $e) {
$isExpired = str_contains($e->getMessage(), 'The token is expired');
$result = new Result(
code: ResultCode::UNAUTHORIZED,
message: $isExpired ? trans('jwt.expired') : trans('jwt.unauthorized'),
);
/** @var \Swow\Psr7\Message\ResponsePlusInterface $response */
$response = \Hyperf\Context\Context::get(ResponseInterface::class);
return $response
->setHeader('Content-Type', 'application/json; charset=utf-8')
->setBody(new SwooleStream(Json::encode($result->toArray())));
}

$this->checkToken->checkJwt($token);

Check failure on line 46 in app/Http/Common/Middleware/AccessTokenMiddleware.php

View workflow job for this annotation

GitHub Actions / build Code coverage report (ubuntu-latest, 8.3, master)

Parameter #1 $token of method Mine\JwtAuth\Interfaces\CheckTokenInterface::checkJwt() expects Lcobucci\JWT\UnencryptedToken, Lcobucci\JWT\Token given.
return $handler->handle(
value(
static function (ServerRequestPlusInterface $request, \Lcobucci\JWT\UnencryptedToken $token) {
return $request->setAttribute('token', $token);
},
$request,
$token
)
);
}

public function getJwt(): JwtInterface
{
return $this->jwtFactory->get();
}
}
}
33 changes: 28 additions & 5 deletions app/Http/Common/Middleware/RefreshTokenMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

namespace App\Http\Common\Middleware;

use App\Http\Common\Result;
use App\Http\Common\ResultCode;
use Hyperf\Codec\Json;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Mine\Jwt\JwtInterface;
use Mine\JwtAuth\Middleware\AbstractTokenMiddleware;
use Psr\Http\Message\ResponseInterface;
Expand All @@ -24,16 +29,25 @@ class RefreshTokenMiddleware extends AbstractTokenMiddleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->checkToken->checkJwt($this->parserToken($request));
try {
$token = $this->parserToken($request);
} catch (RequiredConstraintsViolated $e) {
$isExpired = str_contains($e->getMessage(), 'The token is expired');
$result = new Result(
code: ResultCode::UNAUTHORIZED,
message: $isExpired ? trans('jwt.expired') : trans('jwt.unauthorized'),
);
return $this->buildErrorResponse($request, $result);
Comment on lines +35 to +40
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "RefreshTokenMiddleware.php" | head -5

Repository: mineadmin/MineAdmin

Length of output: 118


🏁 Script executed:

rg "Lcobucci\\JWT" --type php -l | head -10

Repository: mineadmin/MineAdmin

Length of output: 149


🏁 Script executed:

cd app/Http/Common/Middleware && cat -n RefreshTokenMiddleware.php 2>/dev/null || echo "File not found in expected location"

Repository: mineadmin/MineAdmin

Length of output: 3069


🏁 Script executed:

git ls-files "*RefreshTokenMiddleware*" 2>/dev/null || find . -name "RefreshTokenMiddleware.php" -type f 2>/dev/null

Repository: mineadmin/MineAdmin

Length of output: 116


🏁 Script executed:

rg "violations\|constraint" --type php -i | head -20

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

cat composer.json | grep -A 5 "lcobucci/jwt"

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "RequiredConstraintsViolated" --type php -B 2 -A 5

Repository: mineadmin/MineAdmin

Length of output: 3227


🏁 Script executed:

find . -name "composer.json" -type f | head -3

Repository: mineadmin/MineAdmin

Length of output: 78


🏁 Script executed:

cat ./composer.json 2>/dev/null | grep -A 2 "lcobucci" || echo "Not found in root"

Repository: mineadmin/MineAdmin

Length of output: 80


🌐 Web query:

Lcobucci JWT library violations() API constraint checking expiration

💡 Result:

In lcobucci/jwt, violations() is how you inspect which validation constraints failed when you use Validator::assert() and catch RequiredConstraintsViolated. The validator will run all constraints, collect all failures, then throw; violations() returns the collected constraint-violation exceptions. (lcobucci-jwt.readthedocs.io)

To check expiration (exp) you must include a time constraint:

  • v4/v5+: use StrictValidAt (requires iat, nbf, and exp to be present) or LooseValidAt (only checks those claims if present). (lcobucci-jwt.readthedocs.io)
  • StrictValidAt throws a violation if exp is missing and throws “The token is expired” if it’s expired. (raw.githubusercontent.com)

Example (assert + violations, with optional leeway):

use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;

$clock  = SystemClock::fromUTC();
$leeway = new DateInterval('PT60S'); // optional clock skew tolerance
$constraint = new StrictValidAt($clock, $leeway);

try {
    $config->validator()->assert($token, $constraint);
} catch (RequiredConstraintsViolated $e) {
    foreach ($e->violations() as $violation) {
        // each $violation describes the failing constraint (e.g., expired token)
        echo $violation->getMessage(), PHP_EOL;
    }
}

(StrictValidAt/LooseValidAt accept an optional DateInterval leeway; negative leeway is rejected.) (raw.githubusercontent.com)

Citations:


🏁 Script executed:

find . -type f -name "composer.json" -o -name "composer.lock" | head -5

Repository: mineadmin/MineAdmin

Length of output: 78


🏁 Script executed:

rg "lcobucci" --type json | head -5

Repository: mineadmin/MineAdmin

Length of output: 45


🌐 Web query:

Lcobucci JWT violations constraint instance check type expiration

💡 Result:

In lcobucci/jwt, Validator::assert() throws RequiredConstraintsViolated, and you can inspect the individual ConstraintViolation objects via $e->violations() to see which constraint failed and why. [1][2]

How to detect “expired token” vs other failures

Time checks are performed by LooseValidAt / StrictValidAt. When the token is expired they throw a ConstraintViolation with message "The token is expired". [3][4]

In v6+, ConstraintViolation carries the failing constraint as a class-string in $violation->constraint (set by ConstraintViolation::error(...)). [2]

use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\StrictValidAt;

try {
    $validator->assert($token, ...$constraints);
} catch (RequiredConstraintsViolated $e) {
    foreach ($e->violations() as $v) {
        if ($v->constraint === LooseValidAt::class || $v->constraint === StrictValidAt::class) {
            if ($v->getMessage() === 'The token is expired') {
                // expired
            } elseif ($v->getMessage() === 'The token cannot be used yet') {
                // nbf in the future
            } elseif ($v->getMessage() === 'The token was issued in the future') {
                // iat in the future
            }
        }
    }
}

Notes

  • StrictValidAt can also fail because required iat/nbf/exp claims are missing (it throws violations like "Expiration Time" claim missing). [4]
  • LooseValidAt only checks those claims if present. [3]

Sources: [1] [2] [3] [4]


🏁 Script executed:

head -50 ./composer.json

Repository: mineadmin/MineAdmin

Length of output: 1464


🏁 Script executed:

grep -i "lcobucci" ./composer.json

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "lcobucci/jwt" --type json

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

cat ./composer.json | grep -A 200 '"require"' | grep -i jwt

Repository: mineadmin/MineAdmin

Length of output: 126


🏁 Script executed:

find . -type f -path "*/vendor/*" -name "composer.json" | grep -E "lcobucci|jwt" | head -5

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "lcobucci/jwt" --type php | head -5

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

find . -type f -name "*.php" -path "*/Mine/*Jwt*" | head -10

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

find . -type d -name "Mine" | head -5

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

find . -type f -name "*JwtInterface*" -o -name "*JwtAuth*" | head -10

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "class.*JwtInterface\|interface JwtInterface" --type php -A 10 | head -30

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "use Lcobucci" --type php -h | sort -u

Repository: mineadmin/MineAdmin

Length of output: 9338


🏁 Script executed:

rg "validator\(\)" --type php -i -B 3 -A 3 | head -40

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

find . -type f -name "AbstractTokenMiddleware.php" | head -3

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "parserRefreshToken\|parserToken" --type php -B 5 -A 5 | head -50

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

rg "StrictValidAt\|LooseValidAt\|ConstraintViolation" --type php

Repository: mineadmin/MineAdmin

Length of output: 45


🏁 Script executed:

git log --oneline -20 2>/dev/null | head -10

Repository: mineadmin/MineAdmin

Length of output: 100


🏁 Script executed:

git show 3bc02e3 --stat

Repository: mineadmin/MineAdmin

Length of output: 35164


🏁 Script executed:

git show 3bc02e3

Repository: mineadmin/MineAdmin

Length of output: 50379


避免通过异常消息文本判断令牌过期

依赖 str_contains($e->getMessage(), 'The token is expired') 存在脆弱性,会受库版本更新和语言环境变化的影响。建议改为遍历 $e->violations() 检查约束类型:

foreach ($e->violations() as $violation) {
    if ($violation instanceof LooseValidAt || $violation instanceof StrictValidAt) {
        if (str_contains($violation->getMessage(), 'The token is expired')) {
            // 令牌已过期
        }
    }
}

这样既保持向后兼容性,又提高了代码的稳定性。注意此模式同样适用于 AccessTokenMiddleware.php

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Common/Middleware/RefreshTokenMiddleware.php` around lines 35 - 40,
Replace the fragile string-check on the exception message in
RefreshTokenMiddleware (the $isExpired = str_contains($e->getMessage(), 'The
token is expired') logic) with an inspection of the exception violations:
iterate $e->violations(), check each violation for instanceof LooseValidAt or
StrictValidAt and only then determine expiration (e.g., by checking the
violation message or type) to set the Result message; apply the same change to
AccessTokenMiddleware to ensure robust token-expired detection without relying
on exception text.

}

$this->checkToken->checkJwt($token);
return $handler->handle(
value(
static function (ServerRequestPlusInterface $request, UnencryptedToken $token) {
return $request->setAttribute('token', $token);
},
$request,
$this->getJwt()->parserRefreshToken(
$this->getToken($request)
)
$token
)
);
}
Expand All @@ -47,4 +61,13 @@ protected function parserToken(ServerRequestInterface $request): UnencryptedToke
{
return $this->getJwt()->parserRefreshToken($this->getToken($request));
}
}

private function buildErrorResponse(ServerRequestInterface $request, Result $result): ResponseInterface
{
/** @var \Swow\Psr7\Message\ResponsePlusInterface $response */
$response = \Hyperf\Context\Context::get(ResponseInterface::class);
return $response
->setHeader('Content-Type', 'application/json; charset=utf-8')
->setBody(new SwooleStream(Json::encode($result->toArray())));
Comment on lines +65 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

buildErrorResponse$request 未使用
该参数未被使用,建议移除以避免无效依赖与静态分析告警。

🧹 建议修复
-            return $this->buildErrorResponse($request, $result);
+            return $this->buildErrorResponse($result);
...
-    private function buildErrorResponse(ServerRequestInterface $request, Result $result): ResponseInterface
+    private function buildErrorResponse(Result $result): ResponseInterface
     {
         /** `@var` \Swow\Psr7\Message\ResponsePlusInterface $response */
         $response = \Hyperf\Context\Context::get(ResponseInterface::class);
         return $response
             ->setHeader('Content-Type', 'application/json; charset=utf-8')
             ->setBody(new SwooleStream(Json::encode($result->toArray())));
     }
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 65-65: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Common/Middleware/RefreshTokenMiddleware.php` around lines 65 - 71,
The method buildErrorResponse currently accepts an unused ServerRequestInterface
$request parameter; remove this dead parameter from the method signature (change
buildErrorResponse(ServerRequestInterface $request, Result $result) to
buildErrorResponse(Result $result)) and update all callers to pass only the
Result instance; inside the method keep the existing logic that obtains the
response via Context::get(ResponseInterface::class) and returns the JSON
SwooleStream body (preserve references to ResponsePlusInterface, SwooleStream
and Json::encode) so behavior is unchanged while eliminating the unused
dependency.

}
}
Loading