Skip to content

Commit

Permalink
Merge branch 'feature/jwt-authentication' of https://github.com/openf…
Browse files Browse the repository at this point in the history
…oodfoundation/vine into feature/jwt-authentication
  • Loading branch information
ok200paul committed Sep 5, 2024
2 parents c26f4ed + f50d664 commit 3a37c7b
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 104 deletions.
39 changes: 31 additions & 8 deletions app/Console/Commands/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

namespace App\Console\Commands;

use App\Models\AuditItem;
use App\Models\PersonalAccessToken;
use App\Models\Team;
use App\Models\User;
use App\Models\Voucher;
use App\Models\VoucherSet;
use App\Services\PersonalAccessTokenService;
use DateTimeImmutable;
use Illuminate\Console\Command;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token\Builder;

class TestCommand extends Command
Expand All @@ -38,8 +34,35 @@ class TestCommand extends Command
public function handle()
{
$model = PersonalAccessToken::find(5);
$jwt = PersonalAccessTokenService::generateJwtForPersonalAccessToken($model);
$jwt = PersonalAccessTokenService::generateJwtForPersonalAccessToken($model);
dd($jwt);

$tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default()));
$algorithm = new Sha256();
$signingKey = InMemory::plainText('BC0HYlfCbvFcn3BqbcwGkOdTOVilkdn3');

$now = new DateTimeImmutable();
$token = $tokenBuilder
// Configures the issuer (iss claim)
->issuedBy(env('APP_URL'))
// Configures the audience (aud claim)
// ->permittedFor('http://example.org')
// Configures the subject of the token (sub claim)
// ->relatedTo('component1')
// Configures the id (jti claim)
// ->identifiedBy('4f1g23a12aa')
// Configures the time that the token was issue (iat claim)
->issuedAt($now)
// Configures the time that the token can be used (nbf claim)
// ->canOnlyBeUsedAfter($now->modify('+1 minute'))
// Configures the expiration time of the token (exp claim)
->expiresAt($now->modify('+1 hour'))
// Configures a new claim, called "uid"
// ->withClaim('uid', 1)
// Configures a new header, called "foo"
// ->withHeader('foo', 'bar')
// Builds a new token
->getToken($algorithm, $signingKey);

echo $token->toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function store(): JsonResponse

$this->message = ApiResponse::RESPONSE_SAVED->value;
$this->data = [
'token' => $token->plainTextToken,
'token' => $token->plainTextToken,
'secret' => Crypt::decrypt($token->accessToken->secret),
];

Expand Down
6 changes: 6 additions & 0 deletions app/Http/Controllers/Api/V1/ApiMyTeamAuditItemsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ public function index(): JsonResponse

/**
* POST /
*
* @hideFromAPIDocumentation
*
* @return JsonResponse
*/
public function store(): JsonResponse
Expand Down Expand Up @@ -136,7 +138,9 @@ public function show(int $id)
* PUT /{id}
*
* @param string $id
*
* @hideFromAPIDocumentation
*
* @return JsonResponse
*/
public function update(string $id)
Expand All @@ -149,7 +153,9 @@ public function update(string $id)

/**
* DELETE / {id}
*
* @hideFromAPIDocumentation
*
* @param string $id
*
* @return JsonResponse
Expand Down
42 changes: 21 additions & 21 deletions app/Http/Middleware/VerifyApiTokenSignature.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,16 @@
use App\Enums\ApiResponse;
use Carbon\Carbon;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Lcobucci\JWT\Encoding\CannotDecodeContent;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Token\Parser;
use Lcobucci\JWT\Token\UnsupportedHeaderFound;
use Lcobucci\JWT\UnencryptedToken;
use Lcobucci\JWT\Validation\Constraint\RelatedTo;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Validator;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -28,7 +24,8 @@ class VerifyApiTokenSignature
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @param Request $request
* @param Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
Expand All @@ -40,24 +37,23 @@ public function handle(Request $request, Closure $next): Response
*/
if (!EnsureFrontendRequestsAreStateful::fromFrontend($request)) {


if (!$request->user() || !$request->user()->currentAccessToken()) {
$allow = false;
}

if (!$request->hasHeader('X-AUTHORIZATION')) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT->value . ' ' . ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT_JWT_HEADER_REQUIRED->value;
} else {

}
else {

try {

$jwt = $request->header('X-AUTHORIZATION');
$jwtBits = explode(' ', $jwt);
$jwtContents = end($jwtBits);
$parser = new Parser(new JoseEncoder());
$token = $parser->parse(
$jwt = $request->header('X-AUTHORIZATION');
$jwtBits = explode(' ', $jwt);
$jwtContents = end($jwtBits);
$parser = new Parser(new JoseEncoder());
$token = $parser->parse(
$jwtContents
);
$accessTokenSecret = Crypt::decrypt($request->user()->currentAccessToken()->secret);
Expand All @@ -67,13 +63,16 @@ public function handle(Request $request, Closure $next): Response
if ($token->isExpired(now())) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT->value . ' ' . ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT_EXPIRED->value;
} else if (!$token->claims()->has('iat')) {
}
elseif (!$token->claims()->has('iat')) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT->value . ' ' . ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT_IAT_CLAIM_REQUIRED->value;
} else if (!$token->claims()->has('exp')) {
}
elseif (!$token->claims()->has('exp')) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT->value . ' ' . ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT_EXP_CLAIM_REQUIRED->value;
} else {
}
else {

$iat = $token->claims()->get('iat');
$iatCarbon = Carbon::parse($iat);
Expand All @@ -84,10 +83,12 @@ public function handle(Request $request, Closure $next): Response
if ($iatCarbon->isBefore(now()->subMinute())) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT->value . ' ' . ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT_IAT_EXPIRED->value;
} else if ($expCarbon->isAfter($iatCarbon->addMinute())) {
}
elseif ($expCarbon->isAfter($iatCarbon->addMinute())) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT->value . ' ' . ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT_IAT_EXP_TOO_LARGE->value;
} else {
}
else {
$isValid = $validator->validate($token, new SignedWith(new Sha256(), $signingKey));

if (!$isValid) {
Expand All @@ -102,8 +103,8 @@ public function handle(Request $request, Closure $next): Response

}


} catch (CannotDecodeContent|InvalidTokenStructure|UnsupportedHeaderFound $e) {
}
catch (CannotDecodeContent|InvalidTokenStructure|UnsupportedHeaderFound $e) {
$allow = false;
$errorMessage = ApiResponse::RESPONSE_AUTHORIZATION_SIGNATURE_INCORRECT;
}
Expand All @@ -112,7 +113,6 @@ public function handle(Request $request, Closure $next): Response

}


if (!$allow) {
$reply = [
'meta' => [
Expand Down
25 changes: 11 additions & 14 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use DateTimeInterface;
use App\Events\Users\UserWasCreated;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
Expand All @@ -17,7 +17,6 @@
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\NewAccessToken;
use Laravel\Sanctum\Sanctum;

class User extends Authenticatable
{
Expand Down Expand Up @@ -73,7 +72,6 @@ public function currentTeam(): BelongsTo
return $this->belongsTo(Team::class);
}


/**
* Get the access tokens that belong to model.
*
Expand All @@ -84,29 +82,28 @@ public function tokens(): MorphMany
return $this->morphMany(PersonalAccessToken::class, 'tokenable');
}


/**
* Create a new personal access token for the user.
*
* @param string $name
* @param array $abilities
* @param string $name
* @param array $abilities
* @param DateTimeInterface|null $expiresAt
*
* @return NewAccessToken
*/
public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null): NewAccessToken
public function createToken(string $name, array $abilities = ['*'], ?DateTimeInterface $expiresAt = null): NewAccessToken
{

$plainTextToken = $this->generateTokenString();

$token = $this->tokens()->create([
'name' => $name,
'token' => hash('sha256', $plainTextToken),
'secret' => Crypt::encrypt(Str::random(32)),
'abilities' => $abilities,
'expires_at' => $expiresAt,
]);
'name' => $name,
'token' => hash('sha256', $plainTextToken),
'secret' => Crypt::encrypt(Str::random(32)),
'abilities' => $abilities,
'expires_at' => $expiresAt,
]);

return new NewAccessToken($token, $token->getKey() . '|' . $plainTextToken);
}

}
6 changes: 3 additions & 3 deletions app/Services/PersonalAccessTokenService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

namespace App\Services;

use App\Models\AuditItem;
use App\Models\PersonalAccessToken;
use Illuminate\Database\Eloquent\Model;
use DateTimeImmutable;
use Illuminate\Support\Facades\Crypt;
use Lcobucci\JWT\Encoding\ChainedFormatter;
use Lcobucci\JWT\Encoding\JoseEncoder;
Expand All @@ -18,6 +17,7 @@ class PersonalAccessTokenService
* Generate a JWT based on a Personal Access Token
*
* @param PersonalAccessToken|\Laravel\Sanctum\PersonalAccessToken $personalAccessToken
*
* @return string
*/
public static function generateJwtForPersonalAccessToken(PersonalAccessToken|\Laravel\Sanctum\PersonalAccessToken $personalAccessToken): string
Expand All @@ -26,7 +26,7 @@ public static function generateJwtForPersonalAccessToken(PersonalAccessToken|\La
$algorithm = new Sha256();
$patDecryptedSecret = Crypt::decrypt($personalAccessToken->secret);
$signingKey = InMemory::plainText($patDecryptedSecret);
$now = new \DateTimeImmutable();
$now = new DateTimeImmutable();
$token = $tokenBuilder
->issuedBy(config('app.url'))
->issuedAt($now)
Expand Down
Loading

0 comments on commit 3a37c7b

Please sign in to comment.